/** * V3 Data Service — Automatic V3 entity creation via EventBus hooks * * Subscribes to existing backend events and automatically creates/updates * V3 entities (WorkItem, Request, Mission). This service is fire-and-forget: * errors are logged but never block the main flow. * * Design principle: Orchestrator has zero knowledge of V3 concepts. * V3 data is produced as a side effect of normal operations. * * Events consumed: * - task:delegated -> creates WorkItem in TaskPool * - task:completed -> updates WorkItem status to 'done' * - task:failed -> updates WorkItem status to 'failed' * - goal:set -> creates Mission with conservative policy * - message:user_work -> creates Request * * @module services/v3/v3-data.service */ import { EventEmitter } from 'events'; import { type IntentCategory, type IntentLevel } from '../../types/v2/request.types.js'; import { type DecompositionPhase } from './request-completion.js'; /** * Minimum content length for a user message to create a Request. * P2-1: Relaxed from 5 → 2 to stop dropping legitimate short commands * like "fix", "help", "go", "OK". Only truly empty or single-char * messages (e.g., ".", "k") are filtered out. */ export declare const MIN_REQUEST_CONTENT_LENGTH = 2; /** * Grace period (in ms) before an open Request with no WorkItems is * auto-closed as dangling. P2-1: Added to prevent premature closure — * the orchestrator may need time to process the request and delegate. * Default: 5 minutes. */ export declare const DANGLING_REQUEST_GRACE_MS: number; /** * Payload emitted when a task is delegated to an agent. */ export interface TaskDelegatedEvent { taskId: string; title: string; description?: string; assignedTo: string; priority?: string; projectPath: string; milestone?: string; requestId?: string; timestamp: string; } /** * Payload emitted when a task is completed. */ export interface TaskCompletedEvent { type: string; taskId?: string; sessionName: string; previousValue?: string; newValue: string; changedField?: string; timestamp: string; } /** * Payload emitted when a goal is set. */ export interface GoalSetEvent { goal: string; projectPath: string; setBy: string; timestamp: string; } /** * Payload emitted when a task is blocked. */ export interface TaskBlockedEvent { taskId?: string; sessionName: string; reason?: string; timestamp: string; } /** * Payload emitted when a user message is ready for processing. */ export interface UserWorkMessageEvent { messageId: string; conversationId: string; content: string; source: string; timestamp: string; } /** * V3DataService automatically creates V3 data entities by subscribing to * backend EventBus events. It wraps every handler in try-catch to ensure * failures never impact the main workflow. * * @example * ```typescript * const v3Data = new V3DataService(eventBus, '/path/to/project'); * // That's it — events are now being listened to. * ``` */ export declare class V3DataService { private readonly eventBus; private readonly logger; private readonly storageService; private readonly projectPath; private readonly missionsDir; /** * Creates and initializes the V3DataService. * * Subscribes to EventBus domain events. (Legacy ProjectTask file watcher * was retired with the V3↔.md bridge deprecation per * `specs/2026-05-06-projecttask-md-deprecation.md`.) * * @param eventBus - The EventEmitter that publishes domain events * @param projectPath - Absolute path to the project root */ constructor(eventBus: EventEmitter, projectPath: string); /** * Finds the WorkItem matching a task event using a two-tier strategy: * 1. Match by projectTaskId (most precise) * 2. Fall back to sessionName + status (running first, then queued) * * If the matched WorkItem is 'queued', it is transitioned to 'running' * since completeItem/failItem require 'running' status. * * @param taskId - The project task ID (may be undefined) * @param sessionName - The agent session name * @param logContext - Context string for debug logging * @returns The matched WorkItem (with status fix applied), or null */ private findMatchingWorkItem; /** * Handles task:delegated — creates a WorkItem in the TaskPool. * * @param event - Task delegation event payload */ private onTaskDelegated; /** * Handles task:completed — updates the matching WorkItem to 'done'. * * Uses a two-tier matching strategy: * 1. Match by projectTaskId (most precise — correlates to the .crewly/tasks/ file ID) * 2. Fall back to sessionName + status (running or queued, since WorkItems * created via delegation start as 'queued' and may never be claimed) * * @param event - Task completion event payload */ private onTaskCompleted; /** * Handles task:failed — first failure triggers `failItem`, then we * decide retry-vs-escalate based on the WI's own `retryCount` budget. * * Two-stage flow: * 1. Always flip running → failed via taskPool.failItem (records the * error, releases the claim). This is the ground truth state. * 2. If `retryCount < maxRetries` → requeueAfterFailure (failed → * queued, retryCount++). The original target keeps the WI. * 3. Otherwise → leave it in failed and dispose of it through * `taskPool.disposeFailedWorkItem`, which escalates to ORC (so it * can surface the failure to the user and ask for a new plan) AND * stamps a `terminal` disposition on the WI. The stamp is what stops * the reconciler's stranded-item safety net from escalating the * same WI a second time five minutes later. * * Cascade-to-parent-Request happens ONLY on the terminal path * (exhausted retries). A retry-eligible WI doesn't cascade, since * the Request might still complete on the next attempt. * * Steve 2026-05-22: prior to this fix the system treated every * `task:failed` as terminal — retryCount/maxRetries were dead fields, * Mission/Request would cascade-fail on a single transient failure, * and the user only learned about it by digging in the Work Items * UI. New behaviour: retry up to `maxRetries`, then escalate. * * @param event - Task failure event payload */ private onTaskFailed; /** * Handles goal:set — creates a Mission with conservative policy. * * @param event - Goal set event payload */ private onGoalSet; /** * Handles message:user_work — creates a Request from a user message. * * @param event - User work message event payload */ private onUserWorkMessage; /** * Handles task:blocked — updates the matching WorkItem to 'blocked'. * * Uses the same two-tier matching strategy as onTaskCompleted. * * @param event - Task blocked event payload */ private onTaskBlocked; /** * Recomputes Request.status from the aggregate state of its child WorkItems. * * Rules: * - All WorkItems done → Request = done * - Any WorkItem running → Request = running * - All WorkItems queued → no change (work delegated but not started) * - Any WorkItem blocked, none running → Request = blocked * - All WorkItems failed/cancelled → Request = cancelled * - Some done + some queued (no running) → Request = running * * @param requestId - The parent Request ID (may be undefined for unlinked WorkItems) */ private cascadeRequestStatus; /** * Decomposes a mission objective into V3 WorkItems. * * Per `specs/2026-05-06-task-management-v1-deprecation.md`, the legacy * `.crewly/tasks/delegated/open/*.md` filesystem write has been removed — * V3 task-pool is the sole source of truth for delegated work. Each * planned sub-task lands as a queued WorkItem with the long-form brief * carried in `briefMarkdown` (the same content that previously lived in * the .md body). * * @param missionId - The parent mission ID (stored on each WI and in the * brief's Context section) * @param objective - The mission objective text * @param projectPath - Reserved for future use (e.g. resolving relative * skill paths in the brief). Currently unused but * kept in the signature so existing callers don't * have to be updated in lockstep. * @returns Array of created WorkItem IDs in plan order */ static decomposeMissionToTasks(missionId: string, objective: string, _projectPath: string): Promise; /** * Lists all missions from disk. * * @returns Array of Mission objects */ private listMissions; /** * Stops listening to events. Used for cleanup/testing. */ dispose(): void; } /** * Checks if a message is non-actionable (confirmation, chit-chat, greeting). * These messages should not create Requests or WorkItems. * * @param content - The user message text (trimmed) * @returns True if the message is non-actionable */ export declare function isNonActionableMessage(content: string): boolean; /** * Auto-classify request intent from message content. * Runs synchronously — no AI call, just keyword matching. * * Bug A v0 (2026-05-06): delegates to the canonical * `classifyIntentLevel` + `classifyIntentCategory` from * `backend/src/types/intent-task.types.ts` (which read from the * code-of-record rules at * `backend/src/services/intent-task/intent-classifier.rules.ts`). * * Why delegation rather than maintaining a parallel rule-set: prior to * this PR, the two classifyIntent paths drifted. The Mission-side classifier * had ~40 patterns + length floor; the Request-side classifier had ~7 patterns * with no length floor. This caused classification skew across surfaces. * * The local pre-filter `isNonActionableMessage` is preserved because it's * scoped to v3-data.service callers (Request creation flow) and short- * circuits L0 for chit-chat without invoking the heavier classifier. * * @param content - The user message text * @returns Object with intentCategory and intentLevel */ export declare function classifyIntent(content: string): { intentCategory: IntentCategory; intentLevel: IntentLevel; }; /** * Planned task structure used during mission decomposition. */ export interface PlannedTask { /** Short task title */ title: string; /** Detailed task description */ description: string; /** Measurable acceptance criteria */ acceptanceCriteria: string[]; /** Task priority */ priority: 'high' | 'medium' | 'low'; /** * Titles of other planned tasks this one depends on. The fan-out caller * (request-decompose subscriber, mission executor) is expected to map * these titles to the resolved WorkItem IDs and write them into * `WorkItem.dependsOn` before queueing. * * **Why titles, not indices.** Titles survive plan() reordering and * make the dependency intent self-documenting in the plan output. * * **Sequential plans.** A planner that wants strict A→B→C ordering * sets `B.dependsOnTitles = [A.title]` and `C.dependsOnTitles = [B.title]`. * Without this, all three tasks queue concurrently and a worker can * claim Review before Execute is done — observed in the 2026-05-08 * dogfood, where Sam claimed Review while Plan/Execute were still * blocked. */ dependsOnTitles?: string[]; /** * Role of this task in the plan. `plan` and `review` are bookkeeping around * the work and never, on their own, complete a Request; `execute` is the * work. Stamped onto `WorkItem.metadata.decompositionPhase` at fan-out. * Absent = the task is the work. */ phase?: DecompositionPhase; } /** * Determines whether an objective is complex enough for multi-step decomposition. * Complexity heuristics: word count, presence of conjunctions/lists, multiple * action verbs, or explicit multi-step indicators. * * @param objective - The objective text * @returns True if the objective warrants multiple tasks */ export declare function isComplexObjective(objective: string): boolean; /** * Decomposes a mission objective into actionable tasks using keyword matching. * * P0-1 fix: Uses intelligent decomposition instead of always producing 3 tasks. * - Simple/direct objectives → 1 task * - Complex/multi-step objectives → 2-3 tasks * * Recognises three broad categories: * - **Build** (create/implement/build/add/feature) → single task or design+implement+test * - **Fix** (fix/debug/resolve/repair/patch) → single task or investigate+fix+verify * - **Default** (anything else) → single task or plan+execute+review * * @param objective - The mission objective text * @returns Array of PlannedTask objects (1-3 items) */ export declare function planTasksFromObjective(objective: string): PlannedTask[]; /** * Generates a summarized title for a Request instead of copying raw content. * * For short messages (under 60 chars), uses the content directly with an * intent prefix. For longer messages, extracts the first sentence or clause * and prepends the intent prefix. * * @param content - The trimmed user message * @param intentCategory - The classified intent category * @returns A concise, informative title (max 80 chars) */ export declare function generateRequestTitle(content: string, intentCategory: IntentCategory): string; //# sourceMappingURL=v3-data.service.d.ts.map