@exaudeus/workrail 3.66.0 → 3.68.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 (150) hide show
  1. package/dist/application/services/compiler/template-registry.js +10 -1
  2. package/dist/application/validation.js +1 -1
  3. package/dist/cli/commands/worktrain-init.js +1 -1
  4. package/dist/console/standalone-console.js +4 -1
  5. package/dist/console-ui/assets/{index-BynU38Vu.js → index-CyzltI6D.js} +1 -1
  6. package/dist/console-ui/index.html +1 -1
  7. package/dist/coordinators/modes/full-pipeline.js +4 -4
  8. package/dist/coordinators/modes/implement-shared.js +5 -5
  9. package/dist/coordinators/modes/implement.js +4 -4
  10. package/dist/coordinators/pr-review.js +4 -4
  11. package/dist/daemon/workflow-runner.d.ts +1 -0
  12. package/dist/daemon/workflow-runner.js +1 -0
  13. package/dist/infrastructure/storage/schema-validating-workflow-storage.d.ts +21 -2
  14. package/dist/infrastructure/storage/schema-validating-workflow-storage.js +48 -0
  15. package/dist/manifest.json +41 -41
  16. package/dist/mcp/handlers/v2-workflow.js +24 -7
  17. package/dist/mcp/output-schemas.d.ts +36 -0
  18. package/dist/mcp/output-schemas.js +11 -1
  19. package/dist/mcp/workflow-protocol-contracts.js +2 -2
  20. package/dist/v2/projections/session-metrics.d.ts +1 -1
  21. package/dist/v2/projections/session-metrics.js +16 -35
  22. package/dist/v2/usecases/console-routes.d.ts +2 -2
  23. package/docs/authoring-v2.md +4 -4
  24. package/docs/changelog-recent.md +3 -3
  25. package/docs/configuration.md +1 -1
  26. package/docs/design/adaptive-coordinator-context-candidates.md +1 -1
  27. package/docs/design/adaptive-coordinator-context.md +1 -1
  28. package/docs/design/adaptive-coordinator-routing-candidates.md +18 -18
  29. package/docs/design/adaptive-coordinator-routing-review.md +1 -1
  30. package/docs/design/adaptive-coordinator-routing.md +34 -34
  31. package/docs/design/agent-cascade-protocol.md +2 -2
  32. package/docs/design/console-daemon-separation-discovery.md +323 -0
  33. package/docs/design/context-assembly-design-candidates.md +1 -1
  34. package/docs/design/context-assembly-implementation-plan.md +1 -1
  35. package/docs/design/context-assembly-layer.md +2 -2
  36. package/docs/design/context-assembly-review-findings.md +1 -1
  37. package/docs/design/coordinator-access-audit.md +293 -0
  38. package/docs/design/coordinator-architecture-audit.md +62 -0
  39. package/docs/design/coordinator-error-handling-audit.md +240 -0
  40. package/docs/design/coordinator-testability-audit.md +426 -0
  41. package/docs/design/daemon-architecture-discovery.md +1 -1
  42. package/docs/design/daemon-console-separation-discovery.md +242 -0
  43. package/docs/design/daemon-memory-audit.md +203 -0
  44. package/docs/design/design-candidates-console-daemon-separation.md +256 -0
  45. package/docs/design/design-candidates-discovery-loop-fix.md +141 -0
  46. package/docs/design/design-review-findings-console-daemon-separation.md +106 -0
  47. package/docs/design/design-review-findings-discovery-loop-fix.md +81 -0
  48. package/docs/design/discovery-loop-fix-candidates.md +161 -0
  49. package/docs/design/discovery-loop-fix-design-review.md +106 -0
  50. package/docs/design/discovery-loop-fix-validation.md +258 -0
  51. package/docs/design/discovery-loop-investigation-A.md +188 -0
  52. package/docs/design/discovery-loop-investigation-B.md +287 -0
  53. package/docs/design/exploration-workflow-candidates.md +205 -0
  54. package/docs/design/exploration-workflow-design-review.md +166 -0
  55. package/docs/design/exploration-workflow-discovery.md +443 -0
  56. package/docs/design/ide-context-files-candidates.md +231 -0
  57. package/docs/design/ide-context-files-design-review.md +85 -0
  58. package/docs/design/ide-context-files.md +615 -0
  59. package/docs/design/implementation-plan-discovery-loop-fix.md +199 -0
  60. package/docs/design/implementation-plan-queue-poll-rotation.md +102 -0
  61. package/docs/design/in-process-http-audit.md +190 -0
  62. package/docs/design/layer3b-ghost-nodes-design-candidates.md +2 -2
  63. package/docs/design/loadSessionNotes-candidates.md +108 -0
  64. package/docs/design/loadSessionNotes-test-coverage-discovery.md +297 -0
  65. package/docs/design/loadSessionNotes-test-coverage-session4.md +209 -0
  66. package/docs/design/loadSessionNotes-test-coverage-v3.md +321 -0
  67. package/docs/design/probe-session-design-candidates.md +261 -0
  68. package/docs/design/probe-session-phase0.md +490 -0
  69. package/docs/design/routines-guide.md +7 -7
  70. package/docs/design/session-metrics-attribution-candidates.md +250 -0
  71. package/docs/design/session-metrics-attribution-design-review.md +115 -0
  72. package/docs/design/session-metrics-attribution-discovery.md +319 -0
  73. package/docs/design/session-metrics-candidates.md +227 -0
  74. package/docs/design/session-metrics-design-review.md +104 -0
  75. package/docs/design/session-metrics-discovery.md +454 -0
  76. package/docs/design/spawn-session-debug.md +202 -0
  77. package/docs/design/trigger-validator-candidates.md +214 -0
  78. package/docs/design/trigger-validator-review.md +109 -0
  79. package/docs/design/trigger-validator-shaping-phase0.md +239 -0
  80. package/docs/design/trigger-validator.md +454 -0
  81. package/docs/design/v2-core-design-locks.md +2 -2
  82. package/docs/design/workflow-extension-points.md +15 -15
  83. package/docs/design/workflow-id-validation-at-startup.md +1 -1
  84. package/docs/design/workflow-id-validation-implementation-plan.md +2 -2
  85. package/docs/design/workflow-trigger-lifecycle-audit.md +175 -0
  86. package/docs/design/worktrain-task-queue-candidates.md +5 -5
  87. package/docs/design/worktrain-task-queue.md +4 -4
  88. package/docs/discovery/coordinator-script-design.md +1 -1
  89. package/docs/discovery/coordinator-ux-discovery.md +3 -3
  90. package/docs/discovery/simulation-report.md +1 -1
  91. package/docs/discovery/workflow-modernization-discovery.md +326 -0
  92. package/docs/discovery/workflow-selection-for-discovery-tasks.md +33 -33
  93. package/docs/discovery/worktrain-status-briefing.md +1 -1
  94. package/docs/discovery/wr-discovery-goal-reframing.md +1 -1
  95. package/docs/docker.md +1 -1
  96. package/docs/ideas/backlog.md +227 -0
  97. package/docs/ideas/third-party-workflow-setup-design-thinking.md +1 -1
  98. package/docs/integrations/claude-code.md +5 -5
  99. package/docs/integrations/firebender.md +1 -1
  100. package/docs/plans/agentic-orchestration-roadmap.md +2 -2
  101. package/docs/plans/mr-review-workflow-redesign.md +9 -9
  102. package/docs/plans/ui-ux-workflow-design-candidates.md +4 -4
  103. package/docs/plans/ui-ux-workflow-discovery.md +2 -2
  104. package/docs/plans/workflow-categories-candidates.md +8 -8
  105. package/docs/plans/workflow-categories-discovery.md +4 -4
  106. package/docs/plans/workflow-modernization-design.md +430 -0
  107. package/docs/plans/workflow-staleness-detection-candidates.md +11 -11
  108. package/docs/plans/workflow-staleness-detection-review.md +4 -4
  109. package/docs/plans/workflow-staleness-detection.md +9 -9
  110. package/docs/plans/workrail-platform-vision.md +3 -3
  111. package/docs/reference/agent-context-cleaner-snippet.md +1 -1
  112. package/docs/reference/agent-context-guidance.md +4 -4
  113. package/docs/reference/context-optimization.md +2 -2
  114. package/docs/roadmap/now-next-later.md +2 -2
  115. package/docs/roadmap/open-work-inventory.md +16 -16
  116. package/docs/workflows.md +31 -31
  117. package/package.json +1 -1
  118. package/spec/workflow-tags.json +47 -47
  119. package/workflows/adaptive-ticket-creation.json +16 -16
  120. package/workflows/architecture-scalability-audit.json +22 -22
  121. package/workflows/bug-investigation.agentic.v2.json +3 -3
  122. package/workflows/classify-task-workflow.json +1 -1
  123. package/workflows/coding-task-workflow-agentic.json +6 -6
  124. package/workflows/cross-platform-code-conversion.v2.json +8 -8
  125. package/workflows/document-creation-workflow.json +8 -8
  126. package/workflows/documentation-update-workflow.json +8 -8
  127. package/workflows/intelligent-test-case-generation.json +2 -2
  128. package/workflows/learner-centered-course-workflow.json +2 -2
  129. package/workflows/mr-review-workflow.agentic.v2.json +4 -4
  130. package/workflows/personal-learning-materials-creation-branched.json +8 -8
  131. package/workflows/presentation-creation.json +5 -5
  132. package/workflows/production-readiness-audit.json +1 -1
  133. package/workflows/relocation-workflow-us.json +31 -31
  134. package/workflows/routines/context-gathering.json +1 -1
  135. package/workflows/routines/design-review.json +1 -1
  136. package/workflows/routines/execution-simulation.json +1 -1
  137. package/workflows/routines/feature-implementation.json +3 -3
  138. package/workflows/routines/final-verification.json +1 -1
  139. package/workflows/routines/hypothesis-challenge.json +1 -1
  140. package/workflows/routines/ideation.json +1 -1
  141. package/workflows/routines/parallel-work-partitioning.json +3 -3
  142. package/workflows/routines/philosophy-alignment.json +2 -2
  143. package/workflows/routines/plan-analysis.json +1 -1
  144. package/workflows/routines/plan-generation.json +1 -1
  145. package/workflows/routines/tension-driven-design.json +6 -6
  146. package/workflows/scoped-documentation-workflow.json +26 -26
  147. package/workflows/ui-ux-design-workflow.json +14 -14
  148. package/workflows/workflow-diagnose-environment.json +1 -1
  149. package/workflows/workflow-for-workflows.json +32 -77
  150. package/workflows/workflow-for-workflows.v2.json +0 -788
@@ -0,0 +1,199 @@
1
+ # Implementation Plan: Discovery Loop Fix
2
+
3
+ **Date:** 2026-04-19
4
+ **Branch:** `fix/discovery-loop-timeout-and-label`
5
+ **Commit:** `fix(coordinator): thread session timeouts, inspect PipelineOutcome, add sidecar idempotency`
6
+
7
+ ---
8
+
9
+ ## 1. Problem Statement
10
+
11
+ WorkTrain's pipeline re-ran `wr.discovery` on issue #393 at least 73 times over 19+ hours. Three root causes create an infinite re-selection loop:
12
+
13
+ 1. `spawnSession()` passes no `agentConfig` -- sessions inherit `DEFAULT_SESSION_TIMEOUT_MINUTES=30`, but the coordinator waits 55 minutes. Sessions die at 30m; coordinator times out at 55m and escalates.
14
+ 2. `PipelineOutcome` is silently discarded in `polling-scheduler.ts` -- no label is applied on escalation, so the issue is re-selected on every poll cycle.
15
+ 3. `checkIdempotency()` sidecar scan is dead -- `persistTokens()` never writes a `context` field, so every session file is treated as `'clear'`.
16
+
17
+ ---
18
+
19
+ ## 2. Acceptance Criteria
20
+
21
+ - `spawnSession` with `agentConfig.maxSessionMinutes` threads through to `runWorkflowFn` (Fix 1)
22
+ - On `PipelineOutcome.kind === 'escalated'`, `applyGitHubLabel` is called with `worktrain:in-progress` (Fix 2)
23
+ - On `PipelineOutcome.kind === 'success'`, no label is applied (Fix 2)
24
+ - Issue-ownership sidecar is written before dispatch and deleted on completion (Fix 3)
25
+ - Expired sidecar (TTL exceeded) returns `'clear'` from `checkIdempotency` (Fix 3)
26
+ - `npm run build` passes with no errors
27
+ - `npx vitest run` passes with no regressions
28
+
29
+ ---
30
+
31
+ ## 3. Non-Goals
32
+
33
+ - Do NOT touch `src/mcp/`
34
+ - N-strike mechanism (label applied only after N escalations) -- deferred
35
+ - Coordinator-owns-termination refactor -- deferred
36
+ - Unassigning bot from issue on escalation -- not in spec
37
+ - Daemon startup sidecar cleanup -- deferred
38
+ - Exhaustive TypeScript switch on PipelineOutcome kinds -- deferred (if-check satisfies spec)
39
+
40
+ ---
41
+
42
+ ## 4. Philosophy-Driven Constraints
43
+
44
+ - All new interface fields are `readonly`
45
+ - `applyGitHubLabel` uses injected `fetchFn` (not `globalThis.fetch` directly)
46
+ - Sidecar write failure must NOT block dispatch (fire-and-forget, log warn)
47
+ - `applyGitHubLabel` failure must NOT block poll cycle cleanup (log warn, continue)
48
+ - ESM imports use `.js` extension
49
+ - New types: discriminated union `PipelineOutcome` already exists -- use it, don't re-create
50
+
51
+ ---
52
+
53
+ ## 5. Invariants
54
+
55
+ 1. **Fix 1 + Fix 2 ship together** -- deploying Fix 2 without Fix 1 causes every FULL-mode issue to escalate (30m timeout) and get permanently labeled
56
+ 2. **Label = `worktrain:in-progress`** -- already in `queueConfig.excludeLabels` (polling-scheduler.ts:505); using a new label requires operator config change with no enforcement
57
+ 3. **Sidecar TTL = `DISCOVERY_TIMEOUT_MS + 60_000`** ms -- handles crash case; expired sidecar = eligible for re-dispatch
58
+ 4. **Conservative idempotency**: malformed sidecar = 'active' (never allow double-dispatch)
59
+ 5. **Sidecar written before dispatch, deleted in both .then() and .catch()**
60
+
61
+ ---
62
+
63
+ ## 6. Selected Approach
64
+
65
+ **Candidate A**: Exact spec implementation.
66
+
67
+ - Fix 1: Add optional 5th param `agentConfig?` to `CoordinatorDeps.spawnSession` in `pr-review.ts`, thread through `trigger-listener.ts` to `routerRef.dispatch()`, pass correct timeouts at 4 spawn sites in `full-pipeline.ts`
68
+ - Fix 2: Change `Promise<unknown>` to `Promise<PipelineOutcome>` in `polling-scheduler.ts`, inspect outcome, add `applyGitHubLabel` private method
69
+ - Fix 3: Write `queue-issue-<N>.json` sidecar in `doPollGitHubQueue`, extend `checkIdempotency` to check sidecar files by filename pattern with TTL check
70
+
71
+ **Runner-up**: Candidate B (separate sidecar function). Lost: creates coordination risk with two callsites.
72
+
73
+ ---
74
+
75
+ ## 7. Vertical Slices
76
+
77
+ ### Slice 1: Fix 1 -- Thread maxSessionMinutes through spawnSession
78
+
79
+ **Files changed**:
80
+ - `src/coordinators/pr-review.ts` -- add optional `agentConfig?: { readonly maxSessionMinutes?: number; readonly maxTurns?: number }` as 5th param to `CoordinatorDeps.spawnSession`
81
+ - `src/coordinators/adaptive-pipeline.ts` -- `AdaptiveCoordinatorDeps` extends `CoordinatorDeps`, so this is automatically propagated; no change needed here
82
+ - `src/trigger/trigger-listener.ts` -- add `agentConfig?: { readonly maxSessionMinutes?: number; readonly maxTurns?: number }` as 5th param to `spawnSession` closure, forward to `routerRef.dispatch({ ..., agentConfig })`
83
+ - `src/coordinators/modes/full-pipeline.ts` -- pass `{ maxSessionMinutes: Math.ceil(DISCOVERY_TIMEOUT_MS / 60_000) }` (=55) at discovery spawn, `{ maxSessionMinutes: Math.ceil(SHAPING_TIMEOUT_MS / 60_000) }` (=35) at shaping spawn, `{ maxSessionMinutes: Math.ceil(REVIEW_TIMEOUT_MS / 60_000) }` (=25) at UX design spawn, `{ maxSessionMinutes: Math.ceil(CODING_TIMEOUT_MS / 60_000) }` (=65) at coding spawn
84
+
85
+ **Done when**: TypeScript compiles cleanly; `spawnSession` at all call sites passes the correct agentConfig
86
+
87
+ ### Slice 2: Fix 2 -- Inspect PipelineOutcome and apply worktrain:in-progress label
88
+
89
+ **Files changed**:
90
+ - `src/trigger/polling-scheduler.ts`:
91
+ - Add import: `import type { PipelineOutcome } from '../coordinators/adaptive-pipeline.js';`
92
+ - Add import: `import { DISCOVERY_TIMEOUT_MS } from '../coordinators/adaptive-pipeline.js';`
93
+ - Change `Promise<unknown>` to `Promise<PipelineOutcome>` at L605-610
94
+ - Change `.then(() => {` to `.then((outcome: PipelineOutcome) => {`
95
+ - Add label application for `outcome.kind === 'escalated' || outcome.kind === 'dry_run'`
96
+ - Add private method `applyGitHubLabel(issueNumber: number, label: string, token: string, repo: string): Promise<void>`
97
+ - POST `https://api.github.com/repos/${repo}/issues/${issueNumber}/labels`
98
+ - Body: `JSON.stringify({ labels: [label] })`
99
+ - Headers: `Authorization: Bearer ${token}`, `Accept: application/vnd.github+json`, `Content-Type: application/json`
100
+ - Uses `(this.fetchFn as QueueFetchFn | undefined) ?? globalThis.fetch`
101
+ - Non-fatal: catch error, console.warn, return
102
+
103
+ **Done when**: On escalated/dry_run outcome, `applyGitHubLabel` is called with `('worktrain:in-progress', queueConfig.token, source.repo)`
104
+
105
+ ### Slice 3: Fix 3 -- Write/delete sidecar and extend checkIdempotency
106
+
107
+ **Files changed**:
108
+ - `src/trigger/polling-scheduler.ts`:
109
+ - Before dispatch call: write `queue-issue-<N>.json` to `sessionsDir` with `{ issueNumber, triggerId, dispatchedAt: Date.now(), ttlMs: DISCOVERY_TIMEOUT_MS + 60_000 }`
110
+ - In `.then()` handler: delete the sidecar file (fire-and-forget, ignore errors)
111
+ - In `.catch()` handler: delete the sidecar file (fire-and-forget, ignore errors)
112
+ - `src/trigger/adapters/github-queue-poller.ts`:
113
+ - In `checkIdempotency`, before the main scan loop, check if `queue-issue-${issueNumber}.json` exists in `sessionsDir`
114
+ - If file exists: parse it, check `dispatchedAt + ttlMs > Date.now()` -- if true (not expired), return 'active'; if false (expired), return 'clear' for this file
115
+ - On any parse error for the sidecar: return 'active' (conservative)
116
+ - Update JSDoc to describe sidecar file format
117
+
118
+ **Done when**: Sidecar is written before dispatch, deleted on completion; `checkIdempotency` returns 'active' for active sidecar and 'clear' for expired sidecar
119
+
120
+ ### Slice 4: Tests
121
+
122
+ **New file**: `tests/unit/discovery-loop-fix.test.ts`
123
+
124
+ **Test cases**:
125
+
126
+ 1. `spawnSession with agentConfig.maxSessionMinutes threads through to dispatch call`
127
+ - Create a fake `routerRef` that captures dispatch calls
128
+ - Call `coordinatorDeps.spawnSession('wr.discovery', goal, workspace, undefined, { maxSessionMinutes: 55 })`
129
+ - Assert dispatch was called with `agentConfig: { maxSessionMinutes: 55 }`
130
+ - Note: This tests trigger-listener.ts via integration or by testing the coordinator deps implementation directly
131
+
132
+ 2. `On PipelineOutcome.kind === escalated, applyGitHubLabel is called with worktrain:in-progress`
133
+ - Create a fake router with `dispatchAdaptivePipeline` returning `{ kind: 'escalated', escalationReason: { phase: 'discovery', reason: 'timeout' } }`
134
+ - Create a fake fetchFn that captures calls
135
+ - Run `doPollGitHubQueue` (via private method cast)
136
+ - Assert fetchFn was called with URL containing `/labels` and body containing `worktrain:in-progress`
137
+
138
+ 3. `On PipelineOutcome.kind === success, no label is applied`
139
+ - Same setup but `dispatchAdaptivePipeline` returns `{ kind: 'merged', prUrl: 'https://github.com/...' }`
140
+ - Assert fetchFn was NOT called with a labels URL
141
+
142
+ 4. `Issue-ownership sidecar is written before dispatch and deleted on completion`
143
+ - Create a tmpDir for sessionsDir
144
+ - Run `doPollGitHubQueue`
145
+ - Assert sidecar file exists after dispatch setup but is deleted after completion
146
+ - Note: Since dispatch is async, check sidecar presence inside the `dispatchAdaptivePipeline` mock
147
+
148
+ 5. `Expired sidecar (TTL exceeded) returns 'clear' from checkIdempotency`
149
+ - Write a sidecar file with `{ issueNumber: 42, triggerId: 'x', dispatchedAt: 0, ttlMs: 1 }` (already expired)
150
+ - Call `checkIdempotency(42, tmpDir)`
151
+ - Assert result is `'clear'`
152
+
153
+ ---
154
+
155
+ ## 8. Test Design
156
+
157
+ **File**: `tests/unit/discovery-loop-fix.test.ts`
158
+ **Framework**: Vitest with `vi.fn()` fakes
159
+ **Fixtures**: tmpDir for session files, fake fetchFn capturing calls, fake router returning specific PipelineOutcome kinds
160
+
161
+ Key patterns from existing tests:
162
+ - Private method access: `(scheduler as unknown as { doPollGitHubQueue(...) }).doPollGitHubQueue(...)`
163
+ - Fake router: `{ dispatchAdaptivePipeline: async (...) => { return { kind: 'escalated', ... } } }`
164
+ - Queue config: use `vi.mock` or provide a mock `loadQueueConfig`
165
+
166
+ Note: The `spawnSession` agentConfig threading test (test case 1) may be better placed in an integration test that creates a real `trigger-listener.ts` environment. Given the complexity, a unit test that mocks the router and verifies the dispatch call args is acceptable.
167
+
168
+ ---
169
+
170
+ ## 9. Risk Register
171
+
172
+ | Risk | Likelihood | Impact | Mitigation |
173
+ |---|---|---|---|
174
+ | GitHub token expired at label application time | Low | Issue loops again | Warn log is the signal; no code mitigation needed |
175
+ | Sidecar accumulation if delete fails | Low | Manual cleanup needed | TTL handles cleanup after 56m |
176
+ | TypeScript error from 5th optional param on test fakes | Low | Build fails | vi.fn() mocks don't enforce param count |
177
+ | Import of DISCOVERY_TIMEOUT_MS creates circular dep | Low | Build fails | adaptive-pipeline.ts has no deps on polling-scheduler.ts |
178
+
179
+ ---
180
+
181
+ ## 10. PR Packaging Strategy
182
+
183
+ **Single PR** on branch `fix/discovery-loop-timeout-and-label`
184
+ All 3 fixes + tests in one PR. Fixes 1 and 2 are coupled -- they cannot be split.
185
+
186
+ ---
187
+
188
+ ## 11. Philosophy Alignment
189
+
190
+ | Principle | Slice | Status |
191
+ |---|---|---|
192
+ | Immutability by default | 1 (agentConfig fields readonly) | Satisfied |
193
+ | Type safety | 2 (Promise<PipelineOutcome>) | Satisfied |
194
+ | Errors are data | 2 (applyGitHubLabel fire-and-forget) | Tension -- acceptable for non-fatal I/O |
195
+ | Dependency injection | 2 (fetchFn injected) | Satisfied |
196
+ | Exhaustiveness | 2 (all 3 outcome kinds handled) | Satisfied |
197
+ | YAGNI | All | Satisfied -- no extra abstractions |
198
+ | Document why not what | 3 (JSDoc update for checkIdempotency) | Satisfied |
199
+ | Determinism | 3 (TTL check adds wall-clock dep) | Tension -- acceptable for crash recovery |
@@ -0,0 +1,102 @@
1
+ # Implementation Plan: queue-poll.jsonl rotation
2
+
3
+ _Date: 2026-04-21_
4
+
5
+ ## 1. Problem Statement
6
+
7
+ `~/.workrail/queue-poll.jsonl` grows without bound. At 5-minute polling intervals it accumulates ~8.7 MB/month; at 1-minute intervals ~87 MB/month. The daemon memory audit rates this Critical. Additionally, the `worktrain logs --follow` command uses an offset-based reader that assumes the file only grows -- after rotation (when the file is replaced), the stale offset permanently stops the reader from showing new events.
8
+
9
+ ## 2. Acceptance Criteria
10
+
11
+ 1. `queue-poll.jsonl` never exceeds ~10 MB + one write cycle.
12
+ 2. `queue-poll.jsonl.1` exists after the first rotation and contains the most recent pre-rotation entries.
13
+ 3. `worktrain logs --follow` continues showing events after rotation (offset reset on shrink detection).
14
+ 4. Rotation failures log a warning via `console.warn` and do not crash or stop polling.
15
+ 5. The 'permanent file that never rotates' comment in `src/cli-worktrain.ts` is updated.
16
+ 6. `npx tsc --noEmit` passes.
17
+ 7. `npx vitest run` passes.
18
+
19
+ ## 3. Non-Goals
20
+
21
+ - No configurable size threshold (hardcoded 10 MB).
22
+ - No date-named rotation files (backup is `queue-poll.jsonl.1` only).
23
+ - No changes to `daemon.stderr.log`.
24
+ - `worktrain logs` does NOT show backup file content.
25
+ - No multiple backup generations (no `.2`, `.3`, etc.).
26
+
27
+ ## 4. Philosophy-Driven Constraints
28
+
29
+ - **Errors are data**: rotation failures use `console.warn`, never throw.
30
+ - **YAGNI**: no helper function extracted (single use case).
31
+ - **Architectural fixes over patches**: update the 'permanent file' comment so code and documentation stay in sync.
32
+ - **Determinism**: stat before append ensures rotation decision is based on current state.
33
+
34
+ ## 5. Invariants
35
+
36
+ - I1: File size checked BEFORE each append (stat before appendFile).
37
+ - I2: If size >= 10 MB, rename to `.1` (overwriting existing backup) before appending.
38
+ - I3: Reader: if `stat.size < queuePollOffset`, reset `queuePollOffset = 0`.
39
+ - I4: Rotation is fire-and-forget -- inner try/catch around stat/rename; outer try/catch for the full function.
40
+ - I5: ENOENT on stat is caught by inner try/catch and falls through to appendFile (creates file).
41
+
42
+ ## 6. Selected Approach + Rationale + Runner-Up
43
+
44
+ **Selected**: Candidate A -- inline stat+rename in `appendQueuePollLog` + shrink detection in `--follow` loop.
45
+
46
+ **Rationale**: Minimal footprint, follows existing fire-and-forget pattern exactly, zero new abstractions. Both writer and reader fixes in the correct location.
47
+
48
+ **Runner-up**: Candidate B (extracted `rotateIfNeeded` helper). Lost because YAGNI -- no other callers need the function.
49
+
50
+ ## 7. Vertical Slices
51
+
52
+ ### Slice 1: Writer fix (`src/trigger/polling-scheduler.ts`)
53
+ - Add `const MAX_QUEUE_POLL_FILE_SIZE = 10 * 1024 * 1024` constant before the class.
54
+ - Rewrite `appendQueuePollLog` to: stat file, rename to `.1` if size >= threshold, then append.
55
+ - Update or remove the existing comment about never rotating (if any).
56
+ - Done when: `appendQueuePollLog` rotates the file at >= 10 MB and the backup exists.
57
+
58
+ ### Slice 2: Reader fix (`src/cli-worktrain.ts`)
59
+ - Update the comment at lines 685 and 892-893 from 'permanent file that never rotates' to reflect rotation.
60
+ - Add shrink detection before `readNewLines(queuePollPath, queuePollOffset)` in the `--follow` loop: `if (stat.size < queuePollOffset) { queuePollOffset = 0; }`.
61
+ - Done when: `--follow` resets offset on file shrinkage and continues showing events.
62
+
63
+ ### Slice 3: Verification
64
+ - Run `npx tsc --noEmit` -- must pass.
65
+ - Run `npx vitest run` -- must pass.
66
+
67
+ ## 8. Test Design
68
+
69
+ The existing `tests/unit/polling-scheduler.test.ts` does not mock `os.homedir()`, making it difficult to test `appendQueuePollLog` rotation in isolation without significant test infrastructure changes. The pitch does not require new unit tests for the rotation logic (only 'CI passes'). Verification is through TypeScript compilation and the existing test suite.
70
+
71
+ If future tests are added for rotation, they should mock `fs.stat`, `fs.rename`, and `fs.appendFile` using vitest's `vi.mock` or inject a file-system abstraction.
72
+
73
+ ## 9. Risk Register
74
+
75
+ | Risk | Severity | Mitigation |
76
+ |---|---|---|
77
+ | Concurrent rotation race | Yellow | Acknowledged and accepted per pitch. At most 1-2 log lines lost. |
78
+ | EACCES causing unbounded growth | Yellow | console.warn via try/catch. Acceptable for diagnostic log. |
79
+ | Reader fix missed (writer-only PR) | Red | Both slices MUST ship in the same PR. |
80
+
81
+ ## 10. PR Packaging Strategy
82
+
83
+ **SinglePR**: `fix/etienneb/queue-poll-rotation`
84
+ - Commit: `fix(engine): add size-capped rotation for queue-poll.jsonl at 10 MB`
85
+ - Both slices in one commit.
86
+ - MUST NOT be split into writer-only and reader-only PRs.
87
+
88
+ ## 11. Philosophy Alignment per Slice
89
+
90
+ ### Slice 1 (Writer)
91
+ - Errors are data -> satisfied (console.warn not throw)
92
+ - YAGNI -> satisfied (no helper extracted)
93
+ - Determinism -> satisfied (stat before append)
94
+ - Architectural fixes over patches -> satisfied (not a special case, changes the invariant)
95
+
96
+ ### Slice 2 (Reader)
97
+ - Architectural fixes over patches -> satisfied (shrink detection is the correct invariant change)
98
+ - Document why not what -> satisfied (comment update explains rotation now happens)
99
+ - Errors are data -> N/A (statSync in try block for reader)
100
+
101
+ ### Slice 3 (Verification)
102
+ - Type safety as first line of defense -> satisfied (tsc --noEmit)
@@ -0,0 +1,190 @@
1
+ # In-Process HTTP Audit: Daemon Calling Its Own API
2
+
3
+ **Date:** 2026-04-19
4
+ **Scope:** WorkTrain daemon coordinator deps -- calls from inside the daemon process to the daemon's own HTTP console (port 3456) or webhook (port 3200) servers.
5
+
6
+ ---
7
+
8
+ ## Summary
9
+
10
+ The daemon's in-process `coordinatorDeps` block (wired in `src/trigger/trigger-listener.ts:410-752`) contains **two functions** that make HTTP calls to the daemon's own console server at port 3456. Both functions should use `ConsoleService` directly instead.
11
+
12
+ **Bugs confirmed: 2**
13
+ **False positives excluded: CLI HTTP calls (correct design)**
14
+
15
+ ---
16
+
17
+ ## 1. Every HTTP-to-Self Call Found
18
+
19
+ ### Bug 1: `awaitSessions` -- HTTP polling for session status
20
+
21
+ **File:line:** `src/trigger/trigger-listener.ts:499-540`
22
+
23
+ **What it does:**
24
+
25
+ ```typescript
26
+ awaitSessions: async (handles: readonly string[], timeoutMs: number) => {
27
+ const { executeWorktrainAwaitCommand } = await import('../cli/commands/worktrain-await.js');
28
+ await executeWorktrainAwaitCommand(
29
+ {
30
+ fetch: (url: string) => globalThis.fetch(url), // <-- real HTTP fetch
31
+ ...
32
+ },
33
+ {
34
+ sessions: [...handles].join(','),
35
+ port: DAEMON_CONSOLE_PORT, // 3456
36
+ ...
37
+ },
38
+ );
39
+ ```
40
+
41
+ `executeWorktrainAwaitCommand` calls `pollSession()` in `worktrain-await.ts` which builds:
42
+
43
+ ```
44
+ GET http://127.0.0.1:3456/api/v2/sessions/<sessionHandle>
45
+ ```
46
+
47
+ ...every 3 seconds until the session status is terminal (`complete`, `complete_with_gaps`, `blocked`, `dormant`).
48
+
49
+ **Is it broken today?** Yes. Two active failure modes:
50
+ 1. **Port unavailable:** If another process holds port 3456, or if `startDaemonConsole()` hasn't been called yet, the `fetch()` calls return `ECONNREFUSED` and all sessions are reported as `failed`.
51
+ 2. **Race condition:** A session created in-process by `spawnSession()` may not yet be visible to the HTTP layer when the first poll fires. The HTTP layer reads from the same session store, but there is no synchronization guarantee between the in-process write and the HTTP server's view.
52
+
53
+ **Used by:** `full-pipeline.ts` (discovery, shaping, ux-gate, coding sessions), `implement.ts` (ux-gate, coding), `implement-shared.ts` (review, fix-agent, audit, re-review), `pr-review.ts` (review, fix, re-review).
54
+
55
+ ---
56
+
57
+ ### Bug 2: `getAgentResult` -- HTTP fetching for artifacts and recap
58
+
59
+ **File:line:** `src/trigger/trigger-listener.ts:542-609`
60
+
61
+ **What it does:**
62
+
63
+ ```typescript
64
+ getAgentResult: async (sessionHandle: string) => {
65
+ // Step 1: get session detail
66
+ const sessionUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}`;
67
+ const sessionRes = await globalThis.fetch(sessionUrl, { signal: AbortSignal.timeout(30_000) });
68
+ // ... extracts runs[0].preferredTipNodeId and all nodeIds ...
69
+
70
+ // Step 2: fetch each node
71
+ const baseNodeUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}/nodes/`;
72
+ for (const nodeId of nodeIdsToFetch) {
73
+ const nodeRes = await globalThis.fetch(baseNodeUrl + encodeURIComponent(nodeId), ...);
74
+ // ... extracts recapMarkdown and artifacts ...
75
+ }
76
+ }
77
+ ```
78
+
79
+ Makes 1 + N HTTP calls (1 for session detail, N for each node) to the daemon's own console server to collect `recapMarkdown` (for verdict keyword-scan fallback) and `artifacts` (for typed verdict reading via `readVerdictArtifact`).
80
+
81
+ **Is it broken today?** Yes. Same failure modes as Bug 1. Additionally:
82
+ - Artifacts written during in-process session execution may not be flushed to the HTTP-visible layer before `getAgentResult` is called
83
+ - Each node fetch has a 30-second timeout; for sessions with many nodes this adds latency
84
+
85
+ **Used by:** `full-pipeline.ts` (after discovery, to read handoff artifact), `implement-shared.ts` (after review, to read verdict artifact and recap).
86
+
87
+ ---
88
+
89
+ ## 2. Priority Ranking
90
+
91
+ | Rank | Bug | Impact | Blocks pipeline today? |
92
+ |------|-----|--------|----------------------|
93
+ | 1 | `awaitSessions` HTTP polling (Bug 1) | All pipelines hang or report sessions as failed if port 3456 unavailable | Yes |
94
+ | 2 | `getAgentResult` HTTP fetching (Bug 2) | Discovery handoff context missing; review verdict read fails if port 3456 unavailable | Yes -- silently degrades to empty artifacts / keyword-scan only |
95
+
96
+ Both bugs are blocking, not cosmetic. Bug 1 is higher priority because it affects every pipeline phase (all spawned sessions must be awaited). Bug 2 causes a cascade: if `getAgentResult` returns empty artifacts after a coding session, the review verdict cannot be read via the typed path and falls back to keyword-scan on `recapMarkdown`, which is itself also empty -- causing an escalation with reason `review verdict parse failed`.
97
+
98
+ ---
99
+
100
+ ## 3. Recommended Fix for Each
101
+
102
+ ### Fix for Bug 1: `awaitSessions` -- In-process polling via ConsoleService
103
+
104
+ **Replace** the `executeWorktrainAwaitCommand` delegation with a direct polling loop using `ConsoleService.getSessionDetail()`.
105
+
106
+ **In-process equivalent:** `ConsoleService.getSessionDetail(sessionId)` returns `ConsoleSessionDetail` which includes `runs[0].status: ConsoleRunStatus`. The status values (`complete`, `complete_with_gaps`, `blocked`) map directly to the existing `statusToOutcome()` logic in `worktrain-await.ts`.
107
+
108
+ **What to build:**
109
+ - Construct a `ConsoleService` instance in `startTriggerListener()` using the `ctx.v2` deps already available (`ctx.v2.sessionStore`, `ctx.v2.snapshotStore`, `ctx.v2.pinnedStore`, `ctx.v2.dataDir`, `ctx.v2.directoryListing`)
110
+ - Replace `awaitSessions` with an in-process polling loop that calls `consoleService.getSessionDetail(handle)` every ~3 seconds until `runs[0].status` is terminal
111
+ - Handle `dormant` separately: since `ConsoleService.getSessionDetail()` returns `ConsoleRunStatus` (not `ConsoleSessionStatus`), `dormant` won't appear in `runs[0].status`. The polling loop's own timeout covers the dormant case -- sessions that go quiet will be caught by `timeoutMs`
112
+ - Wire the `ConsoleService` instance into `coordinatorDeps` via closure (same pattern as `routerRef`)
113
+
114
+ **Key files:**
115
+ - `src/trigger/trigger-listener.ts` -- replace `awaitSessions` implementation
116
+ - `src/v2/usecases/console-service.ts` -- `ConsoleService.getSessionDetail()` -- no changes needed
117
+ - `src/v2/usecases/console-types.ts` -- `ConsoleSessionDetail`, `ConsoleRunStatus` -- no changes needed
118
+
119
+ ---
120
+
121
+ ### Fix for Bug 2: `getAgentResult` -- In-process node detail via ConsoleService
122
+
123
+ **Replace** the two-phase HTTP fetch with `ConsoleService.getSessionDetail()` (for node IDs) and `ConsoleService.getNodeDetail()` (for per-node recap and artifacts).
124
+
125
+ **In-process equivalent:**
126
+ - `ConsoleService.getSessionDetail(sessionId)` returns `ConsoleSessionDetail.runs[0].nodes: readonly ConsoleDagNode[]` where each `ConsoleDagNode` has `nodeId` and `isPreferredTip`
127
+ - `ConsoleService.getNodeDetail(sessionId, nodeId)` returns `ConsoleNodeDetail` with `recapMarkdown: string | null` and `artifacts: readonly ConsoleArtifact[]` -- exactly what the current HTTP implementation extracts
128
+
129
+ **What to build:**
130
+ - Reuse the same `ConsoleService` instance constructed for Bug 1 fix
131
+ - Replace `getAgentResult` with an in-process version that calls `getSessionDetail` then `getNodeDetail` for each node
132
+ - Preserve the existing logic: collect artifacts from all nodes; collect `recapMarkdown` from the preferred-tip node only
133
+
134
+ **Key files:**
135
+ - `src/trigger/trigger-listener.ts` -- replace `getAgentResult` implementation
136
+ - `src/v2/usecases/console-service.ts` -- `ConsoleService.getNodeDetail()` -- no changes needed
137
+ - `src/v2/usecases/console-types.ts` -- `ConsoleNodeDetail` -- no changes needed
138
+
139
+ ---
140
+
141
+ ## 4. The Correct Architecture
142
+
143
+ ### Current (broken) wiring
144
+
145
+ ```
146
+ TriggerListener (daemon process)
147
+ ├── startTriggerListener() constructs coordinatorDeps
148
+ │ ├── awaitSessions: calls executeWorktrainAwaitCommand
149
+ │ │ └── polls http://127.0.0.1:3456/api/v2/sessions/<id> ← HTTP-to-self
150
+ │ └── getAgentResult: calls globalThis.fetch
151
+ │ └── fetches http://127.0.0.1:3456/api/v2/sessions/<id>/nodes/... ← HTTP-to-self
152
+ └── startDaemonConsole() constructs ConsoleService (separately)
153
+ └── ConsoleService.getSessionDetail() ← in-process, not used by coordinatorDeps
154
+ └── ConsoleService.getNodeDetail() ← in-process, not used by coordinatorDeps
155
+ ```
156
+
157
+ ### Target (correct) wiring
158
+
159
+ ```
160
+ TriggerListener (daemon process)
161
+ └── startTriggerListener() constructs:
162
+ ├── consoleService = new ConsoleService({ ctx.v2.sessionStore, ... })
163
+ └── coordinatorDeps
164
+ ├── awaitSessions: in-process polling loop
165
+ │ └── consoleService.getSessionDetail(handle).runs[0].status ← no HTTP
166
+ └── getAgentResult: in-process node reading
167
+ ├── consoleService.getSessionDetail(handle).runs[0].nodes ← no HTTP
168
+ └── consoleService.getNodeDetail(handle, nodeId) ← no HTTP
169
+ ```
170
+
171
+ ### Why the same ConsoleService instance works for both
172
+
173
+ `ConsoleService` is a stateless projection reader -- it reads from `ctx.v2.sessionStore` (the same append-only event log written by the in-process session execution). Sessions created in-process by `spawnSession()` write their events to the same store that `ConsoleService` reads. There is no HTTP layer in this path -- the data is immediately available after commit.
174
+
175
+ ### What should NOT use in-process access
176
+
177
+ The CLI `run pr-review` command (`src/cli-worktrain.ts:1265-1501`) is a **separate process** from the daemon. Its `spawnSession`, `awaitSessions`, and `getAgentResult` implementations correctly use HTTP to communicate with a running daemon. These are NOT bugs and should not be changed.
178
+
179
+ ### AdaptiveCoordinatorDeps interface notes
180
+
181
+ The `AdaptiveCoordinatorDeps` interface (`src/coordinators/adaptive-pipeline.ts:131-169`) and the `CoordinatorDeps` interface it extends (`src/coordinators/pr-review.ts:131+`) define the dep function signatures but make no assumptions about transport. The interface is correct as-is. Only the concrete implementations in `trigger-listener.ts` need to change.
182
+
183
+ ---
184
+
185
+ ## Excluded from Scope
186
+
187
+ - WorkRail MCP server HTTP calls (out of scope per investigation brief)
188
+ - CLI `run pr-review` HTTP calls (external process, correct design)
189
+ - Webhook server (port 3200) -- no HTTP-to-self calls to this port found
190
+ - `polling-scheduler.ts`, `trigger-router.ts` -- no HTTP-to-self calls found
@@ -16,7 +16,7 @@ The console's session detail view shows a DAG of workflow execution nodes via `R
16
16
 
17
17
  ### Core Tensions
18
18
 
19
- 1. **Labels vs simplicity**: Step labels (human-readable titles like "Phase 0: Triage and classify") are what make ghost nodes useful. But resolving them requires the compiled workflow, which is a backend I/O operation. Skipping labels is simpler but produces raw step IDs (`routine-context-gathering-depth`) that users can't parse.
19
+ 1. **Labels vs simplicity**: Step labels (human-readable titles like "Phase 0: Triage and classify") are what make ghost nodes useful. But resolving them requires the compiled workflow, which is a backend I/O operation. Skipping labels is simpler but produces raw step IDs (`wr.routine-context-gathering-depth`) that users can't parse.
20
20
 
21
21
  2. **Type safety vs ease**: Adding `isGhost: boolean` to `ConsoleDagNode` is one line but violates "make illegal states unrepresentable" -- ghost steps have no `hasRecap`, `hasFailedValidations`, `isTip`, `parentNodeId`, `createdAtEventIndex`, etc. A separate `ConsoleGhostStep` interface is correct but requires touching both mirrored type files.
22
22
 
@@ -91,7 +91,7 @@ The `buildLineageDagModel` signature does NOT need to change -- ghost positionin
91
91
  **Summary**: Extract skipped step IDs from `evaluated_condition` SKIP trace items on the frontend; render ghost nodes without step labels (show raw step ID).
92
92
 
93
93
  **Tensions resolved**: Zero backend changes. No mirrored type file sync needed.
94
- **Tensions accepted**: No step labels. Ghost nodes show raw IDs like `routine-context-gathering-depth`.
94
+ **Tensions accepted**: No step labels. Ghost nodes show raw IDs like `wr.routine-context-gathering-depth`.
95
95
 
96
96
  **Boundary**: `session-detail-use-cases.ts` -- new pure function `getSkippedStepsFromTrace(items: readonly ConsoleExecutionTraceItem[]): readonly string[]` returning step IDs. `RunLineageDag.tsx` -- new sub-feature D `useMemo` computes ghost positions from active lineage model, renders absolute-positioned `GhostNodeOverlay` components.
97
97
 
@@ -0,0 +1,108 @@
1
+ # Candidate Directions: loadSessionNotes Test Coverage
2
+
3
+ **Context:** Issue #393. A complete test implementation (14 tests, all passing) already exists
4
+ uncommitted on disk. The design question is: which approach to land?
5
+
6
+ ---
7
+
8
+ ## Candidate A: Ship the existing implementation as-is (Direct export + vi.mock)
9
+
10
+ **Summary:** Export `loadSessionNotes` from `src/daemon/workflow-runner.ts` (already done in
11
+ working tree) and ship `tests/unit/workflow-runner-load-session-notes.test.ts` (already written,
12
+ 14/14 passing) as a single focused PR closing issue #393.
13
+
14
+ **Why it fits the path:** This is the `design_first` fast-exit conclusion. The design decision
15
+ was already made by a prior session. Shipping it closes the triggering issue and stops the
16
+ "done but not shipped" daemon loop.
17
+
18
+ **Strongest evidence for it:**
19
+ - Implementation is 100% complete, verified, and ready. Zero additional work required.
20
+ - `workflow-runner-spawn-agent.test.ts` establishes the vi.mock + vi.hoisted precedent for the
21
+ same pattern of module-level dependency stubbing.
22
+ - All 7 success criteria (Phase 1f) are satisfied.
23
+
24
+ **Strongest risk against it:**
25
+ - The export adds a name to `workflow-runner.ts`'s public surface that wasn't there before.
26
+ If the module is later refactored, the export may need to move. Low probability — the
27
+ function is already stable and the module is protected.
28
+ - vi.mock is considered a weaker pattern than fakes per project philosophy. The test is
29
+ correct but not architecturally ideal.
30
+
31
+ **When it wins:** When the goal is closing #393 cleanly with zero additional implementation
32
+ risk. This is the correct choice for an autonomous agent operating with surgical constraints.
33
+
34
+ ---
35
+
36
+ ## Candidate B: Extract first, then ship (Module extraction + pure fake tests)
37
+
38
+ **Summary:** Move `loadSessionNotes` (and its constants `MAX_SESSION_NOTE_CHARS`,
39
+ `MAX_SESSION_RECAP_NOTES`) into a new module `src/daemon/session-recap-loader.ts`. Export
40
+ from there. Update the single import in `workflow-runner.ts`. Rewrite
41
+ `tests/unit/workflow-runner-load-session-notes.test.ts` to import from the new module and
42
+ use real fakes instead of vi.mock.
43
+
44
+ **Why it fits the path:** This is the "stronger reframe" the design_first path asks for.
45
+ It solves the underlying architectural concern: `workflow-runner.ts` is already large
46
+ (3500+ lines), and session recap loading is a separable concern. Better module boundaries
47
+ reduce future vi.mock dependency in tests.
48
+
49
+ **Strongest evidence for it:**
50
+ - Prior session's discovery (see design doc) explicitly recommended Option A (extraction)
51
+ over Option B (direct export).
52
+ - Project philosophy: "prefer fakes over mocks" and "compose with small, pure functions."
53
+ - A separate module makes `loadSessionNotes` testable without any vi.mock overhead.
54
+ - `src/daemon/session-recap-loader.ts` would be a natural home alongside
55
+ `session-recap` related code.
56
+
57
+ **Strongest risk against it:**
58
+ - Requires undoing the already-implemented working-tree changes and writing ~150 additional
59
+ lines across two files.
60
+ - `src/daemon/` is listed as a protected directory in AGENTS.md — autonomous modification
61
+ requires the change to be strictly surgical. Extraction is more invasive than export.
62
+ - Every day this work sits undone, issue #393 stays open and the daemon fires more
63
+ discovery sessions. Candidate B takes longer.
64
+ - The test file written for Candidate A (vi.mock) is perfectly functional and follows
65
+ established precedent.
66
+
67
+ **When it wins:** When the project owner explicitly prefers module extraction over direct
68
+ export for architectural cleanliness, and is willing to accept a slightly larger PR.
69
+
70
+ ---
71
+
72
+ ## What would change the verdict
73
+
74
+ Switch from Candidate A to Candidate B if **either** of these is true:
75
+ 1. The project owner confirms that `src/daemon/session-recap-loader.ts` was the intended
76
+ approach before implementation started (i.e., Option A from the prior session was
77
+ the authoritative decision, not merely a recommendation).
78
+ 2. A code review of the PR for Candidate A explicitly requests module extraction as a
79
+ blocking concern before merge.
80
+
81
+ In the absence of either signal, Candidate A wins: it is already implemented, already
82
+ tested, follows established project precedent, and closes the issue immediately.
83
+
84
+ ---
85
+
86
+ ## Candidate C (considered, not recommended): Make contracts visible via Result types
87
+
88
+ **Summary:** Change `loadSessionNotes` to return `Result<readonly string[], Error>` instead
89
+ of silently returning `[]` on failure. Force the caller to decide how to handle each failure
90
+ mode explicitly. Tests then assert on `Result` discriminants rather than `[]` shortcircuits.
91
+
92
+ **Why it is genuinely different:** This reframes the problem from "test the silent-fail
93
+ function" to "make the function's contracts visible at the type level." It addresses the
94
+ root cause: the function hides its failure modes from callers and from tests.
95
+
96
+ **Why it is not recommended:**
97
+ - Requires changing `loadSessionNotes` signature AND the `Promise.all` caller at line 3498
98
+ in `src/daemon/workflow-runner.ts` (protected file, more invasive than export-only).
99
+ - The three failure paths are ALL best-effort recoveries — there is no meaningful action
100
+ the caller can take differently based on which failure occurred. Silent `[]` IS the correct
101
+ contract for a best-effort context-injection helper.
102
+ - Result types add value when the caller needs to branch. Here, the caller always passes
103
+ `[]` to `buildSessionRecap`, which returns `''` for empty. The Result type would be
104
+ immediately `.unwrapOr([])`'d — adding type complexity with no behavioral change.
105
+ - Scope creep relative to issue #393.
106
+
107
+ **When it wins:** Never, for this specific problem. Might be correct in a future refactor
108
+ that redesigns the session recap injection architecture more broadly.