@exaudeus/workrail 3.35.1 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/config/config-file.js +2 -0
  2. package/dist/console-ui/assets/{index-D7jQyCSD.js → index-o-p__sHJ.js} +1 -1
  3. package/dist/console-ui/index.html +1 -1
  4. package/dist/daemon/workflow-runner.d.ts +5 -0
  5. package/dist/daemon/workflow-runner.js +131 -1
  6. package/dist/manifest.json +39 -31
  7. package/dist/mcp/handlers/v2-advance-events.js +1 -1
  8. package/dist/mcp/handlers/v2-execution/start.d.ts +1 -0
  9. package/dist/mcp/handlers/v2-execution/start.js +3 -2
  10. package/dist/trigger/notification-service.d.ts +42 -0
  11. package/dist/trigger/notification-service.js +164 -0
  12. package/dist/trigger/trigger-listener.js +7 -1
  13. package/dist/trigger/trigger-router.d.ts +3 -1
  14. package/dist/trigger/trigger-router.js +4 -1
  15. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +64 -32
  16. package/dist/v2/durable-core/schemas/session/events.d.ts +20 -10
  17. package/dist/v2/durable-core/schemas/session/events.js +1 -1
  18. package/dist/v2/durable-core/schemas/session/gaps.d.ts +8 -8
  19. package/dist/v2/durable-core/schemas/session/gaps.js +1 -1
  20. package/docs/design/agent-behavior-patterns-discovery.md +312 -0
  21. package/docs/design/agent-engine-communication-discovery.md +390 -0
  22. package/docs/design/agent-loop-architecture-alternatives-discovery.md +531 -0
  23. package/docs/design/agent-loop-error-handling-contract.md +238 -0
  24. package/docs/design/complete-step-approach-validation-discovery.md +344 -0
  25. package/docs/design/daemon-stuck-detection-discovery.md +174 -0
  26. package/docs/design/mcp-server-disconnect-discovery.md +245 -0
  27. package/docs/design/mcp-server-epipe-crash.md +198 -0
  28. package/docs/design/notification-design-candidates.md +131 -0
  29. package/docs/design/notification-design-review.md +84 -0
  30. package/docs/design/notification-implementation-plan.md +181 -0
  31. package/docs/design/spawn-agent-failure-modes.md +161 -0
  32. package/docs/design/spawn-agent-result-handling-implementation-plan.md +186 -0
  33. package/docs/design/stdio-simplification-design-candidates.md +341 -0
  34. package/docs/design/stdio-simplification-design-review.md +93 -0
  35. package/docs/design/stdio-simplification-implementation-plan.md +317 -0
  36. package/docs/design/structured-output-tools-coexist-findings.md +288 -0
  37. package/docs/discovery/coordinator-script-design.md +745 -0
  38. package/docs/discovery/coordinator-ux-discovery.md +471 -0
  39. package/docs/discovery/spawn-agent-failure-modes.md +309 -0
  40. package/docs/discovery/workflow-selection-for-discovery-tasks.md +336 -0
  41. package/docs/discovery/worktrain-status-briefing.md +325 -0
  42. package/docs/discovery/worktrain-status-design-candidates.md +202 -0
  43. package/docs/discovery/worktrain-status-design-review-findings.md +86 -0
  44. package/docs/ideas/backlog.md +688 -1
  45. package/docs/ideas/daemon-structured-output-vs-tool-calls.md +344 -0
  46. package/docs/ideas/design-candidates-backlog-consolidation.md +85 -0
  47. package/docs/ideas/design-candidates-spawn-agent-task.md +178 -0
  48. package/docs/ideas/design-review-findings-backlog-consolidation.md +39 -0
  49. package/docs/ideas/design-review-findings-spawn-agent-task.md +139 -0
  50. package/docs/ideas/implementation_plan_backlog_consolidation.md +117 -0
  51. package/docs/ideas/implementation_plan_spawn_agent.md +217 -0
  52. package/docs/plans/authoring-doc-staleness-enforcement-candidates.md +251 -0
  53. package/docs/plans/authoring-doc-staleness-enforcement-review.md +99 -0
  54. package/docs/plans/authoring-doc-staleness-enforcement.md +463 -0
  55. package/package.json +1 -1
@@ -0,0 +1,139 @@
1
+ # Design Review Findings: spawn_agent Tool Implementation
2
+
3
+ _Concise, actionable findings for main-agent synthesis. Design: Candidate 2 (pre-create session with _preAllocatedStartResponse, then blocking runWorkflow())._
4
+
5
+ > Note: Full discovery-phase review is in `design-review-findings-spawn-agent.md`. This file is for the current coding task review pass.
6
+
7
+ ---
8
+
9
+ ## Tradeoff Review
10
+
11
+ ### T1: Parent clock keeps ticking while child runs
12
+ - Confirmed acceptable. Success criterion 2 ('parent does not advance until child completes') is satisfied even on timeout (parent aborts, not advances).
13
+ - When parent times out, child continues as orphaned session. Work is preserved in session store. Session tree preserves the parent-child link.
14
+ - Mitigation needed: document in tool description.
15
+ - **Status: ACCEPTED.**
16
+
17
+ ### T2: _preAllocatedStartResponse comment needs update
18
+ - Current comment: 'set only by the dispatch HTTP handler.' spawn_agent will be another legitimate internal caller.
19
+ - If not updated, future developer may remove spawn_agent support as accidental usage.
20
+ - **Status: REQUIRED FIX (low effort, Step 1.1).**
21
+
22
+ ### T3: One extra async call in execute()
23
+ - executeStartWorkflow() is ~10-50ms (no LLM call). Negligible for a tool that blocks 1-30 minutes.
24
+ - **Status: ACCEPTED.**
25
+
26
+ ### T4: session_created.data extension
27
+ - Confirmed `z.object({})` uses strip mode (not `.strict()`). Extension with `parentSessionId?: z.string().optional()` is backward-compatible.
28
+ - `buildInitialEvents()` currently hardcodes `data: {}` -- requires threading `parentSessionId` parameter.
29
+ - **Status: REQUIRED, LOW RISK.**
30
+
31
+ ---
32
+
33
+ ## Failure Mode Review
34
+
35
+ ### FM1: Parent timeout while child is running
36
+ - Severity: LOW. Child completes normally, work preserved, session tree intact.
37
+ - Design coverage: adequate. Orphaned child traceable via parentSessionId.
38
+ - **No revision required.**
39
+
40
+ ### FM2: executeStartWorkflow() succeeds, runWorkflow() fails before AgentLoop starts
41
+ - Severity: MEDIUM. Zombie session in store (shows as 'running' indefinitely).
42
+ - Design coverage: partial. Parent gets `{ childSessionId, outcome: 'error', notes: errorMessage }` -- child session is observable. But zombie cleanup is deferred.
43
+ - Mitigation for Phase 1: document as known edge case. Phase 2: session timeout/zombie cleanup.
44
+ - **Status: ACCEPTED for Phase 1.**
45
+
46
+ ### FM3: spawnDepth propagation failure
47
+ - Severity: HIGH if unmitigated. FULLY MITIGATED by using typed `readonly spawnDepth?: number` field on `WorkflowTrigger`.
48
+ - After fix: severity drops to LOW (depth is typed, cannot be accidentally lost).
49
+ - **Status: MITIGATED.**
50
+
51
+ ### FM4: Depth bypass via width (sequential spawning)
52
+ - Severity: LOW for Phase 1. `maxSessionMinutes` on parent is the practical limit.
53
+ - **Status: ACCEPTED, deferred to Phase 2.**
54
+
55
+ ---
56
+
57
+ ## Runner-Up / Simpler Alternative Review
58
+
59
+ **Candidate 1 (direct runWorkflow, no pre-create):** Close alternative. Simpler execute() -- no executeStartWorkflow() call. Loses: `childSessionId` is unknown until after runWorkflow() starts; crash-before-start has no childSessionId to return.
60
+
61
+ **No elements worth borrowing from Candidate 1.** C2 already does everything C1 does plus the session-ID-upfront guarantee.
62
+
63
+ **Could skip session_created.data extension?** Technically yes -- `parentSessionId` in `context_set` events is still durable and queryable. But the extension is ~8 lines total and future-proofs DAG queries. Keep it.
64
+
65
+ ---
66
+
67
+ ## Philosophy Alignment
68
+
69
+ ### Clearly satisfied
70
+ - Errors as data: discriminated union return, no throws
71
+ - DI for boundaries: ctx, apiKey, emitter all injected at construction time
72
+ - Immutability: WorkflowTrigger fully readonly, new fields also readonly
73
+ - Exhaustiveness: WorkflowRunResult match handles all 4 variants
74
+ - Validate at boundaries: depth check at start of execute()
75
+ - YAGNI: Phase 1 only; non-blocking spawn deferred
76
+ - Make illegal states unrepresentable: childSessionId always present (pre-create guarantees it)
77
+
78
+ ### Under tension (acceptable)
79
+ - Architectural fixes over patches: parentSessionId via internalContext is somewhat patch-like. Acceptable because internalContext is an established pattern for daemon-internal injection (is_autonomous, workspacePath). Tension is low.
80
+ - Compose with small pure functions: execute() has two async operations (~50 lines). Complexity is necessary and bounded.
81
+
82
+ ---
83
+
84
+ ## Findings
85
+
86
+ ### Red (blocking)
87
+ _None._
88
+
89
+ ### Orange (required before implementation)
90
+
91
+ **O1: Use `readonly spawnDepth?: number` on WorkflowTrigger (not `context.spawnDepth`)**
92
+ Rationale: generic context map can be silently overwritten by trigger system or other callers, breaking depth enforcement. Typed field makes the invariant explicit and compiler-checked.
93
+ Files: `src/daemon/workflow-runner.ts` (WorkflowTrigger type definition)
94
+ **Status: Design already incorporates this fix.**
95
+
96
+ **O2: Update `_preAllocatedStartResponse` comment to list spawn_agent as legitimate caller**
97
+ Rationale: current comment misleads future developers. Without this update, spawn_agent support could be removed as accidental.
98
+ Files: `src/daemon/workflow-runner.ts` (WorkflowTrigger._preAllocatedStartResponse JSDoc)
99
+ **Status: Must be applied during implementation.**
100
+
101
+ **O3: Thread parentSessionId into buildInitialEvents() for session_created.data**
102
+ Rationale: the `internalContext` injection only reaches `context_set` events, not `session_created.data`. For the typed schema extension to work, `buildInitialEvents()` needs a new optional parameter.
103
+ Files: `src/mcp/handlers/v2-execution/start.ts` (`buildInitialEvents()` signature and call site)
104
+ **Status: Required -- not in original design review, discovered during implementation analysis.**
105
+
106
+ ### Yellow (should-fix, not blocking)
107
+
108
+ **Y1: Document parent-clock behavior in tool description**
109
+ The spawn_agent tool description should note that the parent session's maxSessionMinutes clock runs while the child executes. Workflow authors must configure the parent's timeout to be longer than the expected child duration.
110
+
111
+ **Y2: Document zombie session edge case**
112
+ The spawn_agent tool description should note that if runWorkflow() fails before the AgentLoop starts, a zombie session may exist in the store. Phase 2 will add cleanup.
113
+
114
+ **Y3: maxSubagentDepth source**
115
+ For Phase 1, default to 3 if `WorkflowTrigger.agentConfig?.maxSubagentDepth` is not set. Document in tool description.
116
+
117
+ ---
118
+
119
+ ## Recommended Revisions
120
+
121
+ 1. Use `readonly spawnDepth?: number` on WorkflowTrigger (O1 -- design already incorporates)
122
+ 2. Update `_preAllocatedStartResponse` JSDoc to list spawn_agent as a legitimate internal caller (O2)
123
+ 3. Add `parentSessionId?: string` parameter to `buildInitialEvents()` and thread it into `session_created.data` (O3)
124
+ 4. Add parent-clock behavior documentation to spawn_agent tool description (Y1)
125
+ 5. Add zombie session documentation to spawn_agent tool description (Y2)
126
+ 6. Use `trigger.agentConfig?.maxSubagentDepth ?? 3` as maxDepth (Y3)
127
+
128
+ ---
129
+
130
+ ## Residual Concerns
131
+
132
+ **RC1: Session tree query infrastructure deferred to Phase 2**
133
+ `parentSessionId` is written to the session store but no query path exists to read 'all children of session X'. The console DAG view cannot render the tree until Phase 2. This is by design -- Phase 1 writes the data; Phase 2 builds the reader.
134
+
135
+ **RC2: Zod strictness on session_created.data**
136
+ Confirmed that `z.object({})` uses strip mode (not strict). Extension with `parentSessionId?: z.string().optional()` is backward-compatible. Unverified by an actual migration run -- low risk but unvalidated.
137
+
138
+ **RC3: maxTotalAgentsPerTask guardrail deferred**
139
+ Phase 1 enforces depth only. Wide spawning is not caught by depth limits. Phase 2 adds the concurrency registry. For Phase 1, `maxSessionMinutes` on the parent session is the practical limit on total work done.
@@ -0,0 +1,117 @@
1
+ # Implementation Plan: Backlog Consolidation from docs/coordinator-and-scripts-spec
2
+
3
+ ## 1. Problem Statement
4
+
5
+ Five sections were added to `origin/docs/coordinator-and-scripts-spec` (branch commit `ed9c9aaa`) and never merged to main. Main has since grown to 5330 lines with other content. The branch is stale (2088 lines total) and cannot be merged directly. The missing sections must be inserted at the correct chronological position in main's `docs/ideas/backlog.md`.
6
+
7
+ **Missing sections:**
8
+ 1. `### Scripts-first coordinator: avoid the main agent wherever possible (Apr 15, 2026)` (branch lines 1776-1821)
9
+ 2. `### Full development pipeline: coordinator scripts drive multi-phase autonomous work (Apr 15, 2026)` (branch lines 1822-1922)
10
+ 3. `### Additional coordinator pipeline templates (Apr 15, 2026)` with subsections: Backlog grooming coordinator, Bug investigation + fix coordinator, Incident monitoring coordinator (branch lines 1923-2028)
11
+ 4. `### Interactive ideation: WorkTrain as a thinking partner with full project context (Apr 15, 2026)` (branch lines 2029-2056)
12
+ 5. `### Automatic gap and improvement detection: proactive WorkTrain (Apr 15, 2026)` (branch lines 2057-2088)
13
+
14
+ ## 2. Acceptance Criteria
15
+
16
+ - All five section headings are present in `docs/ideas/backlog.md` on main after the change
17
+ - Line count of `docs/ideas/backlog.md` >= 5640 (5330 + ~313 inserted lines)
18
+ - The sections appear between the Verification/Proof records section and `### Dynamic model selection`
19
+ - No existing content on main is removed or altered
20
+ - No doubled `---` separator at the insertion boundary
21
+
22
+ ## 3. Non-Goals
23
+
24
+ - Modifying any code files
25
+ - Processing `docs/apr-17-18-backlog-updates` branch (user confirmed fully subsumed)
26
+ - Deduplicating conceptually overlapping content (the backlog is a running log, not a deduplicated spec)
27
+ - Changing any existing section in `docs/ideas/backlog.md`
28
+
29
+ ## 4. Philosophy-Driven Constraints
30
+
31
+ - **NEVER push directly to main** -- create feature branch `docs/consolidate-coordinator-specs`, commit there, open PR
32
+ - Commit message: `docs(backlog): consolidate missing coordinator specs from stale branch` (72 chars, no period)
33
+ - No em-dashes in commit message or any new written content
34
+
35
+ ## 5. Invariants
36
+
37
+ - Insertion order: new content appears before `### Dynamic model selection: right model for the right task (Apr 15, 2026)` and after the `---` separator at current main line 1781
38
+ - No `---` separator is added to the start or end of the inserted block (main line 1781 already serves as the boundary separator)
39
+ - All five sections are inserted as a single contiguous block (they form a cohesive unit from a single branch commit)
40
+
41
+ ## 6. Selected Approach + Rationale + Runner-Up
42
+
43
+ **Selected:** Use the Edit tool to insert the extracted block (branch lines 1776-2088) immediately before `### Dynamic model selection: right model for the right task (Apr 15, 2026)` in main's backlog. The `old_string` is the exact heading line; `new_string` is the inserted content followed by the heading.
44
+
45
+ **Rationale:** Only one file to edit. The insertion point is unambiguous and verified. Text insertion is correct because the branch file is a strict subset of main -- a git cherry-pick would regress the file.
46
+
47
+ **Runner-Up:** Insert only the two explicitly requested sections (Full dev pipeline + Backlog grooming coordinator). Lost because all five form a cohesive single-commit block; leaving three out is arbitrary.
48
+
49
+ ## 7. Vertical Slices
50
+
51
+ ### Slice 1: Create feature branch
52
+ - `git checkout -b docs/consolidate-coordinator-specs`
53
+ - Acceptance: branch exists, HEAD is at latest main commit
54
+
55
+ ### Slice 2: Insert missing sections
56
+ - Edit `docs/ideas/backlog.md`: insert branch lines 1776-2088 before `### Dynamic model selection`
57
+ - Acceptance: all five headings present in file, line count >= 5640, no doubled `---`
58
+
59
+ ### Slice 3: Commit
60
+ - `git add docs/ideas/backlog.md`
61
+ - `git commit -m "docs(backlog): consolidate missing coordinator specs from stale branch"`
62
+ - Acceptance: clean commit with only `docs/ideas/backlog.md` changed
63
+
64
+ ### Slice 4: Push and open PR
65
+ - `git push -u origin docs/consolidate-coordinator-specs`
66
+ - `gh pr create` with appropriate title and body
67
+ - Acceptance: PR URL returned, PR is open, targets main
68
+
69
+ ## 8. Test Design
70
+
71
+ **Post-edit verification:**
72
+ ```bash
73
+ wc -l docs/ideas/backlog.md # should be >= 5640
74
+ grep -c "Scripts-first coordinator" docs/ideas/backlog.md # should be 1
75
+ grep -c "Full development pipeline: coordinator scripts drive" docs/ideas/backlog.md # should be 1
76
+ grep -c "Additional coordinator pipeline templates" docs/ideas/backlog.md # should be 1
77
+ grep -c "Interactive ideation: WorkTrain as a thinking partner" docs/ideas/backlog.md # should be 1
78
+ grep -c "Automatic gap and improvement detection" docs/ideas/backlog.md # should be 1
79
+ ```
80
+
81
+ **Separator check:**
82
+ ```bash
83
+ grep -n "^---" docs/ideas/backlog.md | grep -A1 -B1 "178[0-2]" # verify no doubled separator
84
+ ```
85
+
86
+ ## 9. Risk Register
87
+
88
+ | Risk | Likelihood | Impact | Mitigation |
89
+ |------|-----------|--------|-----------|
90
+ | Doubled `---` at boundary | Low | Low (visual) | Branch block verified not to start/end with `---` |
91
+ | Content duplication | None | Medium | All five headings confirmed absent from main via grep |
92
+ | Wrong insertion point | None | Medium | Main line 1782 verified as `### Dynamic model selection` |
93
+
94
+ ## 10. PR Packaging Strategy
95
+
96
+ Single PR: `docs/consolidate-coordinator-specs` -> `main`
97
+ - Title: `docs(backlog): consolidate missing coordinator specs from stale branch`
98
+ - All five sections in one commit, one PR
99
+
100
+ ## 11. Philosophy Alignment Per Slice
101
+
102
+ | Slice | Principle | Status |
103
+ |-------|-----------|--------|
104
+ | Create branch | NEVER push to main | Satisfied -- feature branch created |
105
+ | Insert sections | Surface information | Satisfied -- all missing content recovered |
106
+ | Commit | Commit format `docs(backlog):` | Satisfied |
107
+ | PR | NEVER push to main | Satisfied -- PR, not direct push |
108
+
109
+ ---
110
+
111
+ - `implementationPlan`: complete
112
+ - `slices`: 4
113
+ - `testDesign`: grep-based line count + heading presence checks
114
+ - `estimatedPRCount`: 1
115
+ - `followUpTickets`: none
116
+ - `unresolvedUnknownCount`: 0
117
+ - `planConfidenceBand`: High
@@ -0,0 +1,217 @@
1
+ # Implementation Plan: spawn_agent Tool
2
+
3
+ ## 1. Problem Statement
4
+
5
+ Agents running inside WorkRail daemon sessions currently delegate sub-tasks using
6
+ `mcp__nested-subagent__Task` (external Claude Code MCP tool). This produces no WorkRail
7
+ session, no audit trail, and no structured output. The delegated work is completely invisible
8
+ to WorkRail -- it cannot be traced, replayed, or composed into larger orchestrations.
9
+
10
+ **Three root causes to fix:**
11
+ 1. No native WorkRail tool for spawning child sessions from within a daemon session
12
+ 2. No `parentSessionId` link in the session event log (no parent-child graph)
13
+ 3. No depth enforcement to prevent runaway delegation chains
14
+
15
+ ---
16
+
17
+ ## 2. Acceptance Criteria
18
+
19
+ - [ ] A daemon agent can call `spawn_agent({ workflowId, goal, workspacePath, context? })` from a workflow step
20
+ - [ ] The parent agent blocks until the child session completes (no polling, no fire-and-forget)
21
+ - [ ] The child session is created with `parentSessionId` in the session event log (durable, survives crashes)
22
+ - [ ] The tool returns `{ childSessionId, outcome: 'success'|'error'|'timeout', notes: string }` as JSON
23
+ - [ ] Spawning a child at depth >= `maxSubagentDepth` (default 3) returns a typed error without spawning
24
+ - [ ] `spawnDepth` propagates correctly through chains: root=0, child=1, grandchild=2
25
+ - [ ] Child sessions are created in-process (no HTTP dispatch, no semaphore involvement)
26
+ - [ ] `npm run build` passes with no new TypeScript errors
27
+ - [ ] The `spawn_agent` tool is listed in `BASE_SYSTEM_PROMPT` with usage guidance
28
+
29
+ ---
30
+
31
+ ## 3. Non-Goals
32
+
33
+ - `spawn_session` + `await_sessions` non-blocking parallel spawn (Phase 2)
34
+ - `maxTotalAgentsPerTask` width guardrail (Phase 2)
35
+ - Bare-prompt child sessions without a `workflowId` (Phase 2)
36
+ - Session tree query API (console DAG view reads `parentSessionId`, but no query endpoint in Phase 1)
37
+ - Zombie session cleanup (Phase 2)
38
+ - Changes to `TriggerRouter`, `dispatch()`, or the HTTP dispatch route
39
+ - Changes to the public `V2StartWorkflowInput` MCP schema (external callers are unaffected)
40
+
41
+ ---
42
+
43
+ ## 4. Philosophy-Driven Constraints
44
+
45
+ - **Errors are data**: All 4 `WorkflowRunResult` variants (`success`, `error`, `timeout`, `delivery_failed`) must be handled exhaustively and mapped to structured JSON return values. `execute()` must NOT throw for child failures.
46
+ - **Immutability by default**: New `WorkflowTrigger` fields must be `readonly`.
47
+ - **DI for boundaries**: `ctx`, `apiKey`, `runWorkflowFn`, `emitter` injected at factory construction time. No singletons, no global state.
48
+ - **Validate at boundaries**: Depth check at the START of `execute()`, before any async operations.
49
+ - **Make illegal states unrepresentable**: `childSessionId` must always be present in the result (pre-create guarantees it). `spawnDepth` must be a typed `WorkflowTrigger` field (not in context map).
50
+ - **YAGNI**: Phase 1 only. No bare-prompt mode, no `await_sessions`, no width guardrails.
51
+ - **Document 'why'**: Add WHY comments consistent with existing code style in `workflow-runner.ts`.
52
+
53
+ ---
54
+
55
+ ## 5. Invariants
56
+
57
+ 1. **Semaphore bypass invariant**: `spawn_agent` must call `runWorkflow()` directly. Calling `TriggerRouter.dispatch()` from within a running session would cause semaphore deadlock.
58
+ 2. **_preAllocatedStartResponse invariant**: When `trigger._preAllocatedStartResponse` is set, `runWorkflow()` MUST NOT call `executeStartWorkflow()` again. This invariant is already documented in the `WorkflowTrigger` JSDoc.
59
+ 3. **Depth propagation invariant**: A child session at depth N must always construct its `spawn_agent` tool with `currentDepth = N`. This is enforced by the typed `readonly spawnDepth?: number` field on `WorkflowTrigger`.
60
+ 4. **Schema strip invariant**: `session_created.data` uses `z.object({})` strip mode. Adding `parentSessionId?: string` is a backward-compatible additive change.
61
+ 5. **V2StartWorkflowInput unchanged**: The public MCP input schema for `start_workflow` must not be modified. `parentSessionId` flows via `internalContext` only.
62
+
63
+ ---
64
+
65
+ ## 6. Selected Approach + Rationale + Runner-Up
66
+
67
+ **Selected: Candidate 2 (pre-create session with _preAllocatedStartResponse)**
68
+
69
+ `execute()` calls `executeStartWorkflow(input, ctx, { parentSessionId })`, decodes `childSessionId`
70
+ from the returned `continueToken` via `parseContinueTokenOrFail()`, then calls `runWorkflow()` with
71
+ `_preAllocatedStartResponse: startResult.value.response`. This blocks naturally -- `await runWorkflow()`
72
+ inside `execute()` pauses the parent's tool execution until the child completes.
73
+
74
+ **Rationale**: The `_preAllocatedStartResponse` pattern is already proven in `console-routes.ts`
75
+ (lines 573-624). This is a direct adaptation of stable existing machinery, not invention.
76
+ `childSessionId` is always deterministic (known before child runs). Crash-before-start is
77
+ observable (zombie session in store with `parentSessionId` intact).
78
+
79
+ **Runner-up: Candidate 1 (direct runWorkflow())**
80
+ Simpler (~10 fewer lines). Loses: `childSessionId` is unavailable if the run crashes before
81
+ AgentLoop starts. Pivot to this if `_preAllocatedStartResponse` is ever removed.
82
+
83
+ ---
84
+
85
+ ## 7. Vertical Slices
86
+
87
+ ### Slice 1: WorkflowTrigger schema extension
88
+ **File**: `src/daemon/workflow-runner.ts`
89
+ **Change**: Add `readonly parentSessionId?: string` and `readonly spawnDepth?: number` to the `WorkflowTrigger` interface. Update `_preAllocatedStartResponse` JSDoc to list `spawn_agent` as a legitimate internal caller (O2 fix).
90
+ **Acceptance**: TypeScript compiles. No existing callers broken (new fields are optional). `_preAllocatedStartResponse` comment updated.
91
+ **Risk**: None (additive change).
92
+
93
+ ### Slice 2: session_created.data schema extension
94
+ **File**: `src/v2/durable-core/schemas/session/events.ts`
95
+ **Change**: Extend `session_created.data` from `z.object({})` to `z.object({ parentSessionId: z.string().optional() })`.
96
+ **File**: `src/mcp/handlers/v2-execution/start.ts`
97
+ **Change**: Add `parentSessionId?: string` optional parameter to `buildInitialEvents()`. When provided, include it in the `session_created` event's `data` field. Thread `parentSessionId` from `executeStartWorkflow()` (via `internalContext?.['parentSessionId']`) into `buildInitialEvents()`.
98
+ **Acceptance**: TypeScript compiles. Existing session creation (without `parentSessionId`) produces `data: {}` (unchanged). Session created with `parentSessionId` produces `data: { parentSessionId: 'sess_...' }`.
99
+ **Risk**: Low (Zod strip mode, backward-compatible).
100
+
101
+ ### Slice 3: makeSpawnAgentTool() factory
102
+ **File**: `src/daemon/workflow-runner.ts`
103
+ **Change**: Add `SpawnAgentParams` JSON Schema to `getSchemas()`. Add `makeSpawnAgentTool()` factory function alongside `makeCompleteStepTool()`, `makeContinueWorkflowTool()`, etc.
104
+
105
+ Factory signature:
106
+ ```typescript
107
+ export function makeSpawnAgentTool(
108
+ sessionId: string, // process-local UUID (for logging)
109
+ ctx: V2ToolContext,
110
+ apiKey: string,
111
+ thisWorkrailSessionId: string, // WorkRail sess_xxx ID (becomes parentSessionId)
112
+ currentDepth: number, // spawn depth of the parent session
113
+ maxDepth: number, // max depth before blocking spawn
114
+ runWorkflowFn: typeof runWorkflow,
115
+ emitter?: DaemonEventEmitter,
116
+ ): AgentTool
117
+ ```
118
+
119
+ `execute()` logic:
120
+ 1. Depth check: if `currentDepth >= maxDepth`, return `{ childSessionId: null, outcome: 'error', notes: 'Max spawn depth exceeded ...' }` as JSON
121
+ 2. Call `executeStartWorkflow({ workflowId, goal, workspacePath, context }, ctx, { parentSessionId: thisWorkrailSessionId })` and match result
122
+ 3. On start error: return `{ childSessionId: null, outcome: 'error', notes: errorMessage }` as JSON
123
+ 4. On start success: decode `childSessionId` from `startResult.response.continueToken` via `parseContinueTokenOrFail()`
124
+ 5. Call `runWorkflowFn({ workflowId, goal, workspacePath, context, spawnDepth: currentDepth + 1, _preAllocatedStartResponse: startResult.response }, ctx, apiKey, undefined, emitter)`
125
+ 6. Map `WorkflowRunResult` to `{ childSessionId, outcome, notes }`:
126
+ - `success`: `{ outcome: 'success', notes: result.lastStepNotes ?? '' }`
127
+ - `error`: `{ outcome: 'error', notes: result.message }`
128
+ - `timeout`: `{ outcome: 'timeout', notes: result.message }`
129
+ - `delivery_failed`: `{ outcome: 'error', notes: result.deliveryError }`
130
+ 7. Return `{ content: [{ type: 'text', text: JSON.stringify(resultObj) }] }`
131
+
132
+ **Acceptance**: TypeScript compiles. Depth check fires at limit. All 4 result variants handled. `childSessionId` present in all returns (null only on depth error and start error).
133
+ **Risk**: Low (new function, no existing code changed).
134
+
135
+ ### Slice 4: Inject spawn_agent in runWorkflow()
136
+ **File**: `src/daemon/workflow-runner.ts`
137
+ **Change**: Read `trigger.spawnDepth ?? 0` and `trigger.agentConfig?.maxSubagentDepth ?? 3` in `runWorkflow()`. Add `makeSpawnAgentTool(...)` to the `tools` array, using the decoded `workrailSessionId` as `thisWorkrailSessionId`.
138
+
139
+ Note: `workrailSessionId` is decoded from `startContinueToken` AFTER `executeStartWorkflow()`. The tool must be constructed after this decode, but the existing tool list construction already happens after this point (line ~1914).
140
+
141
+ **Acceptance**: TypeScript compiles. `runWorkflow()` signature unchanged. `spawn_agent` tool is in the tools list passed to `AgentLoop`.
142
+ **Risk**: Low. One new tool added to existing list.
143
+
144
+ ### Slice 5: BASE_SYSTEM_PROMPT update
145
+ **File**: `src/daemon/workflow-runner.ts`
146
+ **Change**: Add `spawn_agent` to the tools section of `BASE_SYSTEM_PROMPT`. Document:
147
+ - When to use (delegate sub-tasks to a child WorkRail session)
148
+ - What it returns (`{ childSessionId, outcome, notes }`)
149
+ - Parent clock warning (maxSessionMinutes keeps ticking)
150
+ - Zombie session note (best-effort; cleanup in Phase 2)
151
+ - Depth limit note (default max depth 3)
152
+ **Acceptance**: Tool listed in system prompt with accurate description.
153
+ **Risk**: None.
154
+
155
+ ---
156
+
157
+ ## 8. Test Design
158
+
159
+ **Note**: No unit tests exist for other tool factories in `workflow-runner.ts` (all integration-style). Adding unit tests for `makeSpawnAgentTool()` is aspirational; the acceptance criterion for Phase 1 is `npm run build` passing.
160
+
161
+ **What to verify manually**:
162
+ 1. TypeScript compiles cleanly (`npm run build`)
163
+ 2. Depth check: create a trigger with `spawnDepth: 3`, verify tool returns error immediately
164
+ 3. `_preAllocatedStartResponse` path: verify child session is created in store before `runWorkflow()` starts
165
+
166
+ **If existing tests exist**, run them with `npm test` after each slice.
167
+
168
+ ---
169
+
170
+ ## 9. Risk Register
171
+
172
+ | Risk | Severity | Mitigation |
173
+ |---|---|---|
174
+ | Zombie session (executeStartWorkflow succeeds, runWorkflow fails before AgentLoop) | MEDIUM | Document in tool description, Phase 2 cleanup |
175
+ | Parent timeout while child runs | LOW | Document in tool description, user configures maxSessionMinutes |
176
+ | session_created.data Zod strictness | LOW | Confirmed strip mode; unverified by migration test |
177
+ | _preAllocatedStartResponse removed in future refactor | LOW | Update JSDoc (O2 fix) to protect against this |
178
+ | workrailSessionId null at tool construction time | LOW | Tool construction happens AFTER decode; if decode fails, skip spawn_agent tool or construct with empty string |
179
+
180
+ ---
181
+
182
+ ## 10. PR Packaging Strategy
183
+
184
+ **SinglePR**: `feat/daemon-spawn-agent-tool`
185
+
186
+ All 5 slices go into one PR. The slices are interdependent (Slice 4 requires Slice 3, Slice 3 uses Slice 2, etc.). Splitting across multiple PRs would leave the codebase in a broken intermediate state.
187
+
188
+ PR title: `feat(workflows): add spawn_agent tool for in-process child session delegation`
189
+
190
+ ---
191
+
192
+ ## 11. Philosophy Alignment Per Slice
193
+
194
+ | Slice | Principle | Status |
195
+ |---|---|---|
196
+ | Slice 1 (WorkflowTrigger extension) | Immutability: new fields are readonly | Satisfied |
197
+ | Slice 1 | Make illegal states unrepresentable: spawnDepth typed, not in context map | Satisfied |
198
+ | Slice 2 (schema extension) | Errors are data: Zod strip mode means extension is non-breaking | Satisfied |
199
+ | Slice 3 (makeSpawnAgentTool) | Errors are data: all variants return JSON, no throws | Satisfied |
200
+ | Slice 3 | Exhaustiveness: all 4 WorkflowRunResult variants handled | Satisfied |
201
+ | Slice 3 | DI for boundaries: ctx, apiKey, emitter injected | Satisfied |
202
+ | Slice 3 | Validate at boundaries: depth check first | Satisfied |
203
+ | Slice 4 (inject in runWorkflow) | YAGNI: no bare-prompt, no width guardrails | Satisfied |
204
+ | Slice 4 | Semaphore bypass invariant: direct runWorkflow(), not dispatch() | Satisfied |
205
+ | Slice 5 (system prompt) | Document 'why': parent clock and zombie warnings in tool description | Satisfied |
206
+
207
+ ---
208
+
209
+ ## 12. Plan Metrics
210
+
211
+ - `implementationPlan`: 5 slices across 3 files
212
+ - `slices`: Slice 1 (WorkflowTrigger), Slice 2 (schema + buildInitialEvents), Slice 3 (makeSpawnAgentTool), Slice 4 (inject in runWorkflow), Slice 5 (BASE_SYSTEM_PROMPT)
213
+ - `testDesign`: npm run build + manual depth check + manual session creation verification
214
+ - `estimatedPRCount`: 1
215
+ - `followUpTickets`: Phase 2 (spawn_session + await_sessions), zombie cleanup, session tree query API, maxTotalAgentsPerTask guardrail
216
+ - `unresolvedUnknownCount`: 0 (all open questions from design phase resolved)
217
+ - `planConfidenceBand`: High