@exaudeus/workrail 3.40.0 → 3.42.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 (105) hide show
  1. package/dist/cli/commands/init.js +0 -3
  2. package/dist/cli-worktrain.js +48 -11
  3. package/dist/cli.js +0 -18
  4. package/dist/config/app-config.d.ts +0 -16
  5. package/dist/config/app-config.js +0 -14
  6. package/dist/config/config-file.js +0 -3
  7. package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
  8. package/dist/console-ui/assets/index-DwfWMKvv.js +28 -0
  9. package/dist/console-ui/index.html +2 -2
  10. package/dist/context-assembly/deps.d.ts +8 -0
  11. package/dist/context-assembly/deps.js +2 -0
  12. package/dist/context-assembly/index.d.ts +6 -0
  13. package/dist/context-assembly/index.js +50 -0
  14. package/dist/context-assembly/infra.d.ts +3 -0
  15. package/dist/context-assembly/infra.js +154 -0
  16. package/dist/context-assembly/types.d.ts +30 -0
  17. package/dist/context-assembly/types.js +2 -0
  18. package/dist/coordinators/pr-review.d.ts +20 -1
  19. package/dist/coordinators/pr-review.js +189 -4
  20. package/dist/daemon/daemon-events.d.ts +9 -1
  21. package/dist/daemon/soul-template.d.ts +2 -2
  22. package/dist/daemon/soul-template.js +11 -1
  23. package/dist/daemon/workflow-runner.d.ts +14 -1
  24. package/dist/daemon/workflow-runner.js +406 -25
  25. package/dist/di/container.js +1 -25
  26. package/dist/di/tokens.d.ts +0 -3
  27. package/dist/di/tokens.js +0 -3
  28. package/dist/domain/execution/state.d.ts +6 -6
  29. package/dist/engine/engine-factory.js +0 -1
  30. package/dist/infrastructure/console-defaults.d.ts +1 -0
  31. package/dist/infrastructure/console-defaults.js +4 -0
  32. package/dist/infrastructure/session/index.d.ts +0 -1
  33. package/dist/infrastructure/session/index.js +1 -3
  34. package/dist/manifest.json +138 -122
  35. package/dist/mcp/handlers/session.d.ts +1 -0
  36. package/dist/mcp/handlers/session.js +61 -13
  37. package/dist/mcp/handlers/v2-workflow.d.ts +2 -2
  38. package/dist/mcp/output-schemas.d.ts +234 -234
  39. package/dist/mcp/server.js +1 -18
  40. package/dist/mcp/tools.d.ts +2 -2
  41. package/dist/mcp/transports/http-entry.js +0 -2
  42. package/dist/mcp/transports/stdio-entry.js +1 -2
  43. package/dist/mcp/types.d.ts +0 -2
  44. package/dist/mcp/v2/tools.d.ts +24 -24
  45. package/dist/trigger/daemon-console.d.ts +2 -0
  46. package/dist/trigger/daemon-console.js +1 -1
  47. package/dist/trigger/trigger-listener.d.ts +2 -0
  48. package/dist/trigger/trigger-listener.js +3 -1
  49. package/dist/trigger/trigger-router.d.ts +4 -3
  50. package/dist/trigger/trigger-router.js +4 -3
  51. package/dist/trigger/trigger-store.js +17 -4
  52. package/dist/v2/durable-core/schemas/artifacts/assessment.d.ts +2 -2
  53. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +2 -2
  54. package/dist/v2/durable-core/schemas/artifacts/loop-control.d.ts +6 -6
  55. package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +6 -6
  56. package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +56 -56
  57. package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +83 -83
  58. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +1024 -1024
  59. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +2336 -2336
  60. package/dist/v2/durable-core/schemas/session/dag-topology.d.ts +6 -6
  61. package/dist/v2/durable-core/schemas/session/events.d.ts +339 -339
  62. package/dist/v2/durable-core/schemas/session/gaps.d.ts +30 -30
  63. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  64. package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
  65. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +3 -3
  66. package/dist/v2/usecases/console-routes.d.ts +2 -1
  67. package/dist/v2/usecases/console-routes.js +29 -5
  68. package/dist/v2/usecases/console-service.js +14 -0
  69. package/dist/v2/usecases/console-types.d.ts +1 -0
  70. package/docs/authoring.md +16 -16
  71. package/docs/design/context-assembly-design-candidates.md +199 -0
  72. package/docs/design/context-assembly-implementation-plan.md +211 -0
  73. package/docs/design/context-assembly-review-findings.md +112 -0
  74. package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
  75. package/docs/design/coordinator-message-queue-drain-review.md +120 -0
  76. package/docs/design/coordinator-message-queue-drain.md +289 -0
  77. package/docs/design/shaping-workflow-external-research.md +119 -0
  78. package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
  79. package/docs/discovery/late-bound-goals-review.md +82 -0
  80. package/docs/discovery/late-bound-goals.md +118 -0
  81. package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
  82. package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
  83. package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
  84. package/docs/ideas/backlog.md +356 -0
  85. package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
  86. package/docs/ideas/design-candidates-session-tree-view.md +196 -0
  87. package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
  88. package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
  89. package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
  90. package/package.json +2 -1
  91. package/spec/authoring-spec.json +16 -16
  92. package/spec/shape.schema.json +178 -0
  93. package/spec/workflow-tags.json +232 -47
  94. package/workflows/coding-task-workflow-agentic.json +491 -480
  95. package/workflows/wr.shaping.json +182 -0
  96. package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
  97. package/dist/console-ui/assets/index-CXWCAonr.js +0 -28
  98. package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
  99. package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
  100. package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
  101. package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
  102. package/dist/infrastructure/session/HttpServer.d.ts +0 -60
  103. package/dist/infrastructure/session/HttpServer.js +0 -912
  104. package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
  105. package/workflows/coding-task-workflow-agentic.v2.json +0 -324
@@ -0,0 +1,241 @@
1
+ # Implementation Plan: Coordinator Message Queue Drain
2
+
3
+ ## 1. Problem Statement
4
+
5
+ `worktrain tell "<message>"` appends to `~/.workrail/message-queue.jsonl` but the PR review
6
+ coordinator (`runPrReviewCoordinator`) never reads this file. Messages sent from a phone,
7
+ terminal, or automation (e.g., "stop", "skip-pr 42") are silently ignored. The coordinator
8
+ must drain this queue at the start of each cycle and act on actionable messages before spawning
9
+ any agent.
10
+
11
+ ## 2. Acceptance Criteria
12
+
13
+ AC1. When `stop` appears as the first meaningful word in a queued message (matched by
14
+ `/^\s*stop\b/i`), the coordinator exits cleanly without reviewing any PR, and appends an
15
+ outbox notification that includes the full triggering message text and timestamp.
16
+
17
+ AC2. When `skip-pr N` appears in a queued message (matched by `/\bskip[- ]pr[\s#]+(\d+)/i`),
18
+ PR #N is removed from the list before Stage 1 review dispatch. An outbox notification is
19
+ appended confirming the skip.
20
+
21
+ AC3. When `add-pr N` appears in a queued message (matched by `/\badd[- ]pr[\s#]+(\d+)/i`),
22
+ PR #N is added to the list (with Set dedup to prevent duplicates). An outbox notification
23
+ is appended confirming the addition.
24
+
25
+ AC4. Messages that match no recognized pattern are skipped silently (treated as notes).
26
+
27
+ AC5. After draining, the cursor in `~/.workrail/message-queue-cursor.json` is updated so
28
+ processed messages are not re-processed on the next coordinator invocation.
29
+
30
+ AC6. If `~/.workrail/message-queue.jsonl` does not exist (ENOENT), the drain returns a no-op
31
+ result and the coordinator proceeds normally.
32
+
33
+ AC7. Malformed JSONL lines (unparseable JSON) are skipped without crashing the coordinator.
34
+ A stderr warning is emitted for each skipped malformed line.
35
+
36
+ AC8. All drain I/O (readFile, appendFile, homedir, joinPath, now, generateId) is injected via
37
+ `CoordinatorDeps`. No direct `fs` imports are added to `pr-review.ts`.
38
+
39
+ AC9. Unit tests for `drainMessageQueue()` use fake deps (in-memory file map). No real filesystem
40
+ access in tests.
41
+
42
+ ## 3. Non-Goals
43
+
44
+ - No `reprioritize` message kind in this PR
45
+ - No workspace routing (workspaceHint matching) -- all messages are consumed regardless of hint
46
+ - No structured `kind` field on `QueuedMessage` (Candidate C) -- that is a follow-up issue
47
+ - No truncation or compaction of consumed messages (queue remains append-only)
48
+ - No real-time / `--watch` mode
49
+ - No multi-coordinator fan-out (single coordinator consumes the queue)
50
+ - No integration test (unit tests with fakes are sufficient)
51
+
52
+ ## 4. Philosophy-Driven Constraints
53
+
54
+ - Errors as data: `drainMessageQueue` returns `DrainResult`, never throws
55
+ - All I/O injected: `CoordinatorDeps` gains `readFile` and `appendFile`; zero direct fs imports
56
+ - Immutability: `DrainResult` and all new interfaces are fully readonly
57
+ - Prefer fakes over mocks: tests use in-memory fake deps
58
+ - Validate at boundaries: JSONL parsing, ENOENT, cursor desync handled at the read boundary
59
+ - Document WHY: function header explains the cursor pattern and text-matching tradeoff
60
+
61
+ ## 5. Invariants
62
+
63
+ I1. `message-queue.jsonl` is never written or truncated by the coordinator (append-only)
64
+ I2. The coordinator drains the queue BEFORE Stage 1 (PR discovery) -- never mid-agent-run
65
+ I3. `stop: true` in `DrainResult` takes absolute precedence; coordinator must check stop before
66
+ acting on `skipPrNumbers` or `addPrNumbers`
67
+ I4. The cursor advances only AFTER successful outbox writes (best-effort; cursor write failure
68
+ does not block drain -- same pattern as worktrain-inbox.ts)
69
+ I5. ENOENT on message-queue.jsonl = no messages = coordinator proceeds normally (not an error)
70
+ I6. Cursor desync guard: if `cursor > totalLines`, reset to 0 (queue was wiped)
71
+
72
+ ## 6. Selected Approach & Rationale
73
+
74
+ **Selected: Candidate B** -- `drainMessageQueue()` pure function with cursor + text parsing.
75
+
76
+ **Rationale:** Direct adaptation of the `worktrain-inbox.ts` cursor pattern (already tested, same
77
+ `InboxCursor` shape `{ lastReadCount: number }`). Additive to `CoordinatorDeps`. Text parsing is
78
+ narrow (`^\\s*stop\\b`) and consistent with how `parseFindingsFromNotes()` works in the same file.
79
+
80
+ **Runner-up: Candidate C** (structured `kind` field on `QueuedMessage`). Loses because it
81
+ requires a schema change to the public CLI interface (`worktrain tell`), which is out of scope.
82
+ Filed as a follow-up.
83
+
84
+ ## 7. Vertical Slices
85
+
86
+ ### Slice 1: Extend `CoordinatorDeps` and add `DrainResult` type
87
+
88
+ **Files:** `src/coordinators/pr-review.ts`
89
+
90
+ **Work:**
91
+ - Add `readFile: (path: string) => Promise<string>` to `CoordinatorDeps`
92
+ - Add `appendFile: (path: string, content: string) => Promise<void>` to `CoordinatorDeps`
93
+ - Add `mkdir: (path: string, options: { recursive: boolean }) => Promise<string | undefined>` to `CoordinatorDeps`
94
+ - Define `DrainResult` interface (readonly: stop, stopReason, skipPrNumbers, addPrNumbers, messagesProcessed)
95
+
96
+ **Done when:** TypeScript compiles with new interface fields. No runtime behavior change yet.
97
+
98
+ **Note:** Updating fake deps in `coordinator-pr-review.test.ts` is part of this slice (compile-
99
+ time requirement).
100
+
101
+ ---
102
+
103
+ ### Slice 2: Implement `drainMessageQueue()`
104
+
105
+ **Files:** `src/coordinators/pr-review.ts`
106
+
107
+ **Work:**
108
+ - New exported function `drainMessageQueue(deps, workrailDir)` -- deps is the coordinator deps
109
+ subset; workrailDir defaults to `deps.joinPath(deps.homedir(), '.workrail')`
110
+ - Reads `message-queue.jsonl` (ENOENT -> return empty result)
111
+ - Reads cursor from `message-queue-cursor.json` (missing/corrupt -> 0)
112
+ - Applies cursor desync guard (cursor > totalLines -> reset to 0)
113
+ - Parses new lines (slice from cursor), skips malformed with stderr warning
114
+ - For each parsed `QueuedMessage`:
115
+ - `^\\s*stop\\b/i` match -> set stop=true, record stopReason=message.message
116
+ - `/\\bskip[- ]pr[\\s#]+([0-9]+)/i` match -> add to skipSet
117
+ - `/\\badd[- ]pr[\\s#]+([0-9]+)/i` match -> add to addSet
118
+ - Otherwise: skip (informational note)
119
+ - After processing all new messages:
120
+ - For each actionable message: appendFile to outbox.jsonl with confirmation text
121
+ - Append stderr `[INFO coord:drain kind=... message="..." ts=...]` per actionable message
122
+ - Update cursor file (non-fatal on failure)
123
+ - Return `DrainResult`
124
+
125
+ **Done when:** Function exists, TypeScript compiles, unit tests pass.
126
+
127
+ ---
128
+
129
+ ### Slice 3: Integrate drain into `runPrReviewCoordinator()`
130
+
131
+ **Files:** `src/coordinators/pr-review.ts`
132
+
133
+ **Work:**
134
+ - Call `drainMessageQueue(deps)` at the top of `runPrReviewCoordinator()` (before Stage 1 log)
135
+ - Check `drainResult.stop` immediately:
136
+ - If true: log stop reason, write report (empty/aborted), return early with all zeros
137
+ - Apply `drainResult.skipPrNumbers` to remove PRs from the discovered list (after Stage 1)
138
+ - Apply `drainResult.addPrNumbers` to add PRs to the list (with Set dedup, before Stage 1)
139
+ - Log drain activity: `[drain] processed N messages, skip=[...], add=[...]` if messagesProcessed > 0
140
+
141
+ **Done when:** Integration passes existing coordinator unit tests + new drain integration test.
142
+
143
+ ---
144
+
145
+ ### Slice 4: Wire new deps in `cli-worktrain.ts`
146
+
147
+ **Files:** `src/cli-worktrain.ts`
148
+
149
+ **Work:**
150
+ - Add `readFile: (p: string) => fs.promises.readFile(p, 'utf-8')` to CoordinatorDeps wiring
151
+ - Add `appendFile: (p: string, content: string) => fs.promises.appendFile(p, content, 'utf-8')`
152
+ to CoordinatorDeps wiring
153
+ - Add `mkdir: (p: string, opts: { recursive: boolean }) => fs.promises.mkdir(p, opts)` to
154
+ CoordinatorDeps wiring
155
+
156
+ **Done when:** `worktrain run pr-review --dry-run` compiles and runs without error.
157
+
158
+ ---
159
+
160
+ ### Slice 5: Unit tests for `drainMessageQueue()`
161
+
162
+ **Files:** `tests/unit/coordinator-pr-review.test.ts`
163
+
164
+ **Work:**
165
+ - Add `readFile` and `appendFile` to the existing fake CoordinatorDeps helper
166
+ - New `describe('drainMessageQueue')` block covering:
167
+ - ENOENT -> returns empty DrainResult (messagesProcessed=0, stop=false)
168
+ - Stop message at start of message text -> stop=true, stopReason set
169
+ - Stop NOT triggered when 'stop' appears mid-sentence ("please stop overthinking" -- note: this
170
+ still fires with `^\\s*stop` since it doesn't start the message; test confirms this is the
171
+ designed behavior)
172
+ - skip-pr with PR number -> skipPrNumbers contains the number
173
+ - add-pr with PR number -> addPrNumbers contains the number
174
+ - Malformed JSONL lines skipped, messagesProcessed counts only valid lines
175
+ - Cursor advances after drain
176
+ - Cursor desync guard resets to 0 when cursor > totalLines
177
+ - Multiple messages: stop takes precedence regardless of order in queue
178
+ - Note-only messages: no action, cursor advances, messagesProcessed = N
179
+
180
+ **Done when:** All new tests pass; no existing tests broken.
181
+
182
+ ## 8. Test Design
183
+
184
+ **Strategy:** Fake deps only (in-memory Map for files, Set for dirs). No real filesystem.
185
+
186
+ **Key test helpers:**
187
+ ```ts
188
+ interface FakeDrainFs {
189
+ files: Map<string, string>;
190
+ }
191
+
192
+ function makeDrainDeps(fs: FakeDrainFs): Pick<CoordinatorDeps, 'readFile' | 'appendFile' | 'mkdir' | 'homedir' | 'joinPath' | 'now' | 'generateId' | 'stderr'>
193
+ ```
194
+
195
+ **Critical test cases:**
196
+ - `stop` as sole message: stop=true, outbox has triggering text
197
+ - `skip-pr 42` after a note: skipPrNumbers=[42], messagesProcessed=2
198
+ - Two `skip-pr` for same PR: deduplicated in Set (skipPrNumbers=[42] not [42, 42])
199
+ - Cursor = 5, file has 5 lines: messagesProcessed=0 (all previously read)
200
+ - Cursor = 10, file has 5 lines: cursor reset to 0, all 5 processed
201
+
202
+ ## 9. Risk Register
203
+
204
+ | Risk | Likelihood | Impact | Mitigation |
205
+ |---|---|---|---|
206
+ | `stop` false positive on note message | Low | Medium | `^\\s*stop\\b` anchor; outbox shows triggering text |
207
+ | Cursor file write failure | Very Low | Low | Non-fatal; next run re-reads from 0 (desync reset) |
208
+ | Outbox write failure during stop | Very Low | Low | Non-fatal; stderr log is backup |
209
+ | `readFile`/`appendFile` not wired in cli-worktrain.ts | Low | High | Slice 4 is explicit; TypeScript will catch missing fields at compile time |
210
+
211
+ ## 10. PR Packaging Strategy
212
+
213
+ Single PR on branch `feat/coordinator-message-queue`. All 5 slices in one PR -- they are
214
+ tightly coupled (type change -> function -> integration -> wiring -> tests). Separating them
215
+ would create a non-compiling intermediate state.
216
+
217
+ ## 11. Philosophy Alignment Per Slice
218
+
219
+ | Slice | Principle | Status |
220
+ |---|---|---|
221
+ | 1 | Immutability by default | Satisfied -- all new fields are readonly |
222
+ | 1 | Explicit domain types | Tension -- DrainResult uses boolean stop not a discriminated union; documented |
223
+ | 2 | Errors are data | Satisfied -- DrainResult is a value; ENOENT returns empty result |
224
+ | 2 | Dependency injection | Satisfied -- all I/O via injected deps |
225
+ | 2 | Validate at boundaries | Satisfied -- malformed JSONL skipped at parse boundary |
226
+ | 3 | Determinism over cleverness | Satisfied -- same queue + cursor = same result |
227
+ | 4 | Compose with small pure functions | Satisfied -- drainMessageQueue is pure at logic level |
228
+ | 5 | Prefer fakes over mocks | Satisfied -- fake deps, no vi.mock() |
229
+
230
+ ## 12. Follow-Up Tickets
231
+
232
+ 1. **Add `kind` field to `QueuedMessage` for structured dispatch** (Candidate C) -- unblocks
233
+ automated tooling writing to the message queue without text fragility.
234
+ 2. **`worktrain tell --help` should list recognized coordinator command patterns** -- discovery
235
+ for users who don't know what command words the coordinator recognizes.
236
+
237
+ ## Summary
238
+
239
+ - `estimatedPRCount`: 1
240
+ - `unresolvedUnknownCount`: 0
241
+ - `planConfidenceBand`: High
@@ -0,0 +1,120 @@
1
+ # Design Review Findings: Coordinator Message Queue Drain
2
+
3
+ **Design reviewed:** Candidate B from `coordinator-message-queue-drain.md`
4
+ (drainMessageQueue with cursor + text parsing)
5
+
6
+ ---
7
+
8
+ ## Tradeoff Review
9
+
10
+ ### T1: Stringly-typed dispatch (free-form text parsing)
11
+
12
+ Accepted tradeoff. The `^\\s*stop\\b/i` anchor pattern is narrower than bare `stop` matching
13
+ and covers realistic CLI usage. The risk of false-positive halt is real but diagnosable -- the
14
+ outbox notification includes the triggering message text. Condition for no longer acceptable:
15
+ automated tooling writing to the queue. Explicitly documented as a pivot trigger for Candidate C.
16
+
17
+ ### T2: New cursor file on disk
18
+
19
+ Fully acceptable. Same format as `InboxCursor`; desync guard handles truncation; write failure
20
+ is non-fatal. No new schema maintenance burden.
21
+
22
+ ### T3: Outbox notifications for all actionable messages
23
+
24
+ Fully acceptable. Outbox write failure is non-fatal; stderr provides a backup diagnostic.
25
+ Including notifications for all actions (not just `stop`) is the right call -- users need the
26
+ feedback loop.
27
+
28
+ ---
29
+
30
+ ## Failure Mode Review
31
+
32
+ | FM | Description | Mitigation | Residual risk |
33
+ |---|---|---|---|
34
+ | FM1 | `stop` fires on note message | `^\\s*stop\\b` anchor; outbox shows triggering text | Low -- diagnosable and recoverable |
35
+ | FM2 | Cursor desync after queue wipe | Reset to 0 if cursor > totalLines | Low -- re-triggers past stop if present; outbox makes it visible |
36
+ | FM3 | Duplicate add-pr | Set dedup before Stage 1 | None |
37
+ | FM4 | Outbox write failure during stop | Non-fatal; stderr fallback | None -- stop still honored |
38
+ | FM5 | ENOENT (no queue file) | Return empty DrainResult | None -- expected on fresh install |
39
+
40
+ **Highest-risk failure mode:** FM1. Must include triggering message text and timestamp in the
41
+ outbox notification and stderr log -- this is a required implementation detail, not optional.
42
+
43
+ ---
44
+
45
+ ## Runner-Up / Simpler Alternative Review
46
+
47
+ **Candidate C strengths borrowed:** Structured parse result logged to stderr (`[INFO drain:kind=stop
48
+ message=...]`) -- same diagnostic value as a `kind` field at zero schema cost.
49
+
50
+ **Simpler variant (skip outbox notifications):** Rejected -- silent halt is a UX regression.
51
+
52
+ **Simpler variant (skip `add-pr`):** Viable as a scope reduction. Included in this PR because the
53
+ implementation cost is ~10 lines, and `skip-pr` without `add-pr` is asymmetric.
54
+
55
+ ---
56
+
57
+ ## Philosophy Alignment
58
+
59
+ **Clearly satisfied:** Immutability, errors as values, DI, validate at boundaries, determinism,
60
+ fakes over mocks, small pure functions, document WHY.
61
+
62
+ **Under tension:**
63
+ - "Explicit domain types over primitives" -- free-form text dispatch. Acceptable: pre-existing
64
+ schema constraint, documented as follow-up.
65
+ - "Make illegal states unrepresentable" -- `DrainResult` can represent `stop: true` with
66
+ non-empty `skipPrNumbers`. Acceptable: `stop` check is first at call site; documented.
67
+
68
+ ---
69
+
70
+ ## Findings
71
+
72
+ ### YELLOW: `stop` regex false-positive on note messages
73
+
74
+ The `^\\s*stop\\b/i` pattern is significantly better than bare `stop` matching, but it will still
75
+ fire on a message like "stop and think about this before merging." No additional regex constraint
76
+ is practical without excluding valid stop forms. The mitigation (outbox + stderr with triggering
77
+ message text) is the correct and sufficient response.
78
+
79
+ **Recommended revision:** None to the pattern itself. Ensure the outbox notification reads:
80
+ `WorkTrain coordinator stopped by queued message: "[full message text]" (queued at [timestamp])`
81
+ rather than a generic "coordinator stopped" message.
82
+
83
+ ### YELLOW: `DrainResult` allows `stop: true` + non-empty `skipPrNumbers`
84
+
85
+ The call site must check `stop` before anything else. If a future maintainer adds code between
86
+ the drain call and the `stop` check, or moves the check, the skip/add arrays could be acted on
87
+ before the stop is honored.
88
+
89
+ **Recommended revision:** Add a JSDoc invariant on `DrainResult`: "When `stop` is true, all
90
+ other fields are informational only. The coordinator MUST honor `stop` before inspecting
91
+ `skipPrNumbers` or `addPrNumbers`." Also add a comment at the call site.
92
+
93
+ ### YELLOW (minor): No structured parse log to stderr
94
+
95
+ Without logging which pattern matched and for which message, diagnosing unexpected behavior
96
+ requires reading the outbox. A one-line stderr log per actionable message helps during
97
+ development and debugging.
98
+
99
+ **Recommended revision:** For each actionable message (stop, skip-pr, add-pr), emit:
100
+ `[INFO coord:drain kind=stop handle=... message="..." ts=...]` to `deps.stderr`.
101
+
102
+ ---
103
+
104
+ ## Recommended Revisions (summary)
105
+
106
+ 1. Outbox notification for `stop` must include the full triggering message text and timestamp.
107
+ 2. Add JSDoc invariant on `DrainResult` documenting that `stop: true` takes absolute precedence.
108
+ 3. Add a `[INFO coord:drain]` stderr log line for each actionable message (diagnostics).
109
+
110
+ None of these revisions change the architecture. All are implementation-level details.
111
+
112
+ ---
113
+
114
+ ## Residual Concerns
115
+
116
+ 1. **Schema follow-up not filed yet.** A GitHub issue or backlog entry for adding a `kind`
117
+ field to `QueuedMessage` (Candidate C path) should be created as part of this PR.
118
+ 2. **No integration test.** Unit tests with fake deps are sufficient for the drain logic, but
119
+ an end-to-end test (write to real queue file, run coordinator, verify outbox) is not planned.
120
+ This is acceptable for a developer CLI tool.
@@ -0,0 +1,289 @@
1
+ # Design Candidates: Coordinator Message Queue Drain
2
+
3
+ **Task:** The PR review coordinator never reads `~/.workrail/message-queue.jsonl`, so
4
+ messages queued via `worktrain tell` (from phone, terminal, or automation) are silently ignored.
5
+ This document captures the design investigation for draining that queue inside the coordinator.
6
+
7
+ ---
8
+
9
+ ## Problem Understanding
10
+
11
+ ### Core tensions
12
+
13
+ 1. **Append-only invariant vs. consumed-message tracking.** The queue file must never be
14
+ truncated or rewritten -- the `worktrain-tell` command's documented invariant. But without
15
+ tracking which messages were processed, the coordinator re-processes the entire history on
16
+ every invocation. A cursor file (same pattern as `inbox-cursor.json`) resolves this cleanly
17
+ but adds a second file to manage.
18
+
19
+ 2. **Stringly-typed messages vs. explicit domain types.** `QueuedMessage.message` is free-form
20
+ text. The repo philosophy demands explicit domain types, but no `kind` field exists in the
21
+ current schema. Text parsing at the coordinator's read boundary is the only option within
22
+ the current schema -- it is not a patch, it is adapting to a pre-existing constraint.
23
+
24
+ 3. **Coordinator statefulness vs. single-pass design.** The coordinator is invoked once per run
25
+ today, not as a persistent loop. A cursor handles both cases correctly: repeat invocations
26
+ see only new messages; a one-time invocation drains everything queued since last run.
27
+
28
+ 4. **`stop` signal semantics vs. partial progress.** A `stop` in the queue must halt before any
29
+ spawn. But `stop` might appear alongside `skip-pr 42` in the same drain batch. `stop` takes
30
+ absolute precedence -- no partial processing, coordinator exits cleanly and writes an outbox
31
+ acknowledgment.
32
+
33
+ ### Likely seam
34
+
35
+ The real seam is the top of `runPrReviewCoordinator()`, immediately before Stage 1 (PR discovery).
36
+ This matches the backlog intent: "coordinator loop checks message-queue at the start of each cycle
37
+ before spawning new agents." The coordinator is the right owner, not a shared utility, because
38
+ message routing is coordinator-specific logic.
39
+
40
+ ### What makes this hard
41
+
42
+ Not technically difficult. The risks are:
43
+ - Forgetting to handle ENOENT (queue file doesn't exist yet = no messages, not a crash)
44
+ - Cursor desync: if the queue is wiped, cursor > total lines; reset to 0 (same guard as `inbox-cursor.json`)
45
+ - Text matching fragility: `stop` in "stop overthinking this" triggers coordinator halt
46
+
47
+ ---
48
+
49
+ ## Philosophy Constraints
50
+
51
+ From `CLAUDE.md` and observed repo patterns:
52
+
53
+ - **Errors are values, never thrown** -- `pr-review.ts` uses `Result<T, string>` throughout.
54
+ The drain result uses a plain `DrainResult` struct (stop is not an error, it is a valid outcome).
55
+ - **All I/O injected via deps** -- new `drainMessageQueue()` must accept deps, not import `fs`.
56
+ - **Immutability by default** -- all interface fields are `readonly`.
57
+ - **Prefer fakes over mocks** -- tests use in-memory fake deps, no `vi.mock()`.
58
+ - **Validate at boundaries, trust inside** -- malformed JSONL lines are skipped at the parse
59
+ boundary; core routing logic trusts parsed data.
60
+ - **Document WHY, not WHAT** -- comments explain rationale, not mechanics.
61
+
62
+ **Conflict:** "Explicit domain types over primitives" is under pressure from the free-form message
63
+ text. The mitigation is narrow keyword patterns and clear documentation. This conflict is not
64
+ resolved in this PR -- a `kind` field on `QueuedMessage` is the proper fix but changes the
65
+ public CLI interface (out of scope here).
66
+
67
+ ---
68
+
69
+ ## Impact Surface
70
+
71
+ Changes that must stay consistent if this design is implemented:
72
+
73
+ - **`CoordinatorDeps` interface** in `src/coordinators/pr-review.ts`: gains `readFile` and
74
+ `appendFile`. These are additive -- no existing caller is broken.
75
+ - **`cli-worktrain.ts` pr-review action**: must wire `readFile` and `appendFile` into the deps
76
+ object (two new lines in the composition root).
77
+ - **`tests/unit/coordinator-pr-review.test.ts`**: every fake `CoordinatorDeps` object needs the
78
+ two new fields. Mechanical but must not be missed.
79
+ - **`discoverConsolePort` deps** (mini-subset type): no change needed; it already has `readFile`.
80
+
81
+ New files introduced on disk (runtime, not source):
82
+ - `~/.workrail/message-queue-cursor.json` -- created on first coordinator run after this ships.
83
+
84
+ ---
85
+
86
+ ## Candidates
87
+
88
+ ### Candidate A -- Minimal: full-history drain, no cursor, timestamp filter
89
+
90
+ **Summary:** On each coordinator run, read all messages in `message-queue.jsonl`, discard messages
91
+ older than the coordinator's start time, act on the remainder.
92
+
93
+ **Tensions resolved:** Simplest change; no new cursor file.
94
+
95
+ **Tensions accepted:** Stale messages re-processed if clock skew or same-second invocations.
96
+ A `stop` message from two days ago can halt a coordinator run today if the clock check is ambiguous.
97
+
98
+ **Boundary:** Inline in `runPrReviewCoordinator()`, no new function or file.
99
+
100
+ **Why this boundary is wrong:** The timestamp filter is not reliable enough. Same-second writes,
101
+ NTP jumps, or leap-second events can cause a current `stop` to be discarded or a stale `stop` to
102
+ fire. The cursor is strictly more correct.
103
+
104
+ **Failure mode:** Stale `stop` from a previous session kills today's coordinator run. No recovery
105
+ path -- the coordinator just exits. Users have to manually inspect the queue to understand why.
106
+
107
+ **Repo-pattern relationship:** Departs -- `worktrain-inbox.ts` uses a cursor precisely to avoid
108
+ the re-processing problem. This candidate ignores the established pattern.
109
+
110
+ **Gains:** Zero new files.
111
+
112
+ **Gives up:** Correctness. Behavior depends on queue history, not just current inputs -- violates
113
+ "determinism over cleverness."
114
+
115
+ **Scope judgment:** Too narrow -- solves the immediate symptom but breaks on any real usage.
116
+
117
+ **Philosophy fit:** Conflicts with "determinism over cleverness." Does not honor "validate at
118
+ boundaries" (stale messages leak through).
119
+
120
+ **Verdict: Rejected.** Stale message re-processing is a correctness bug, not a tradeoff.
121
+
122
+ ---
123
+
124
+ ### Candidate B -- Best-fit: `drainMessageQueue()` with cursor, narrow text parsing
125
+
126
+ **Summary:** Add a pure function `drainMessageQueue(deps, opts)` to `src/coordinators/pr-review.ts`.
127
+ It reads new lines since `~/.workrail/message-queue-cursor.json`, parses message text for `stop` /
128
+ `skip-pr N` / `add-pr N` using narrow regex patterns, writes outbox acknowledgments for actionable
129
+ messages, advances the cursor. Called at the top of `runPrReviewCoordinator()` before Stage 1.
130
+
131
+ **Tensions resolved:**
132
+ - Append-only invariant respected (cursor tracks progress, queue file never modified)
133
+ - Stale message re-processing eliminated by cursor
134
+ - ENOENT handled (no queue = empty drain result = coordinator proceeds normally)
135
+ - `stop` takes absolute precedence
136
+
137
+ **Tensions accepted:**
138
+ - Text parsing is not type-safe; fragile to natural language variation
139
+
140
+ **Boundary solved at:** New exported function in `src/coordinators/pr-review.ts`.
141
+
142
+ **Why this boundary is best-fit:** Message routing is coordinator-specific. The drain reads a
143
+ coordinator-managed cursor file and writes outbox notifications -- both are coordinator
144
+ responsibilities. Extracting to a shared utility would create coupling without benefit (no other
145
+ coordinator exists today).
146
+
147
+ **Key data structures:**
148
+
149
+ ```ts
150
+ export interface DrainResult {
151
+ readonly stop: boolean;
152
+ readonly stopReason: string | null;
153
+ readonly skipPrNumbers: readonly number[];
154
+ readonly addPrNumbers: readonly number[];
155
+ readonly messagesProcessed: number;
156
+ }
157
+ ```
158
+
159
+ Cursor shape: `{ lastReadCount: number }` -- identical to `InboxCursor` in `worktrain-inbox.ts`.
160
+
161
+ New `CoordinatorDeps` fields:
162
+ ```ts
163
+ readonly readFile: (path: string) => Promise<string>;
164
+ readonly appendFile: (path: string, content: string) => Promise<void>;
165
+ ```
166
+
167
+ Parsing patterns:
168
+ - stop: `/\bstop\b/i`
169
+ - skip-pr: `/\bskip[- ]pr[\s#]+([0-9]+)/i`
170
+ - add-pr: `/\badd[- ]pr[\s#]+([0-9]+)/i`
171
+
172
+ **Failure mode:** A note message like "stop overthinking this" triggers coordinator halt. Mitigation:
173
+ word-boundary requirement limits false positives; documented as known behavior with workaround
174
+ ("add-pr" or "note:" prefix for non-command messages).
175
+
176
+ **Repo-pattern relationship:** Follows `worktrain-inbox.ts` cursor pattern exactly; follows
177
+ `CoordinatorDeps` injection pattern exactly.
178
+
179
+ **Gains:** Correct deduplication; clean separation; fully testable with fakes.
180
+
181
+ **Gives up:** Type-safe dispatch. A `kind` field would be cleaner.
182
+
183
+ **Impact surface:** `CoordinatorDeps` (additive), `cli-worktrain.ts` (2 new dep wires),
184
+ `coordinator-pr-review.test.ts` (2 new fake dep fields).
185
+
186
+ **Scope judgment:** Best-fit.
187
+
188
+ **Philosophy fit:** Honors immutability (readonly result), DI for boundaries, errors as values,
189
+ validate at boundaries. Partial conflict with "explicit domain types" (documented and accepted).
190
+
191
+ **Verdict: Recommended.**
192
+
193
+ ---
194
+
195
+ ### Candidate C -- Broader: structured `kind` field on `QueuedMessage`
196
+
197
+ **Summary:** Extend `QueuedMessage` with `readonly kind?: 'stop' | 'skip-pr' | 'add-pr' | 'note'`
198
+ and `readonly payload?: Record<string, unknown>`. Update `worktrain-tell.ts` to accept `--kind`
199
+ flag. Coordinator drains on `kind` field instead of text parsing.
200
+
201
+ **Tensions resolved:** Eliminates the stringly-typed tension entirely. Discriminated union on
202
+ `kind` makes routing exhaustive and type-safe.
203
+
204
+ **Tensions accepted:** Schema change affects the public CLI interface. Existing `tell` invocations
205
+ omitting `--kind` fall back to `kind: 'note'` (safe), but natural language commands no longer work
206
+ (`worktrain tell "stop"` becomes a note, not a stop signal).
207
+
208
+ **Boundary solved at:** `QueuedMessage` type in `worktrain-tell.ts` + coordinator drain in
209
+ `pr-review.ts` + CLI parser in `cli-worktrain.ts`.
210
+
211
+ **Why this boundary is too broad:** Adds `kind` to `QueuedMessage` -- a public interface change.
212
+ The `tell` command is documented as accepting any free-form text. Adding a required semantic field
213
+ is a separate design decision that should be preceded by discussion of the CLI UX.
214
+
215
+ **Failure mode:** Users who currently type `worktrain tell "stop the agent"` find it ignored
216
+ unless they learn to use `--kind stop`. The ergonomic regression is silent.
217
+
218
+ **Repo-pattern relationship:** Honors "explicit domain types" and "make illegal states
219
+ unrepresentable" from philosophy. Departs from current free-form-text CLI design.
220
+
221
+ **Gains:** Type-safe dispatch, no regex fragility, forward-compatible for new action kinds.
222
+
223
+ **Gives up:** Natural language ergonomics; requires more CLI plumbing.
224
+
225
+ **Scope judgment:** Too broad for this task.
226
+
227
+ **Philosophy fit:** Strongly honors explicit domain types, discriminated unions, exhaustiveness.
228
+ Conflicts with YAGNI -- adds schema complexity before the feature is proven.
229
+
230
+ **Verdict: Out of scope for this PR. File a follow-up issue.**
231
+
232
+ ---
233
+
234
+ ## Comparison and Recommendation
235
+
236
+ | | A (timestamp) | B (cursor + text) | C (structured kind) |
237
+ |---|---|---|---|
238
+ | Stale message safety | Weak | Strong | Strong |
239
+ | Schema change | No | No | Yes |
240
+ | Scope fit | Too narrow | Best-fit | Too broad |
241
+ | Testability | Full | Full | Full |
242
+ | Text-parse fragility | Avoided (no parse) | Narrow regexes | Eliminated |
243
+ | Repo-pattern alignment | Poor | Exact | Partial |
244
+ | Philosophy fit | Weak | Good (with caveat) | Strong |
245
+
246
+ **Recommendation: Candidate B.**
247
+
248
+ Candidate A fails on correctness. Candidate C solves the right problem but changes the wrong
249
+ boundary for this task. Candidate B is a direct adaptation of the existing `worktrain-inbox.ts`
250
+ cursor pattern to the coordinator context -- it introduces no new architectural ideas, just
251
+ applies the established approach.
252
+
253
+ ---
254
+
255
+ ## Self-Critique
256
+
257
+ **Strongest argument against Candidate B:**
258
+
259
+ The text-matching approach creates an implicit, undiscoverable API. Users sending messages from
260
+ phones have no way to know that `stop` means stop but `halt` does not. There is no help text,
261
+ no validation, no error message for unrecognized commands. This is a real UX problem.
262
+
263
+ **What would tip the decision toward Candidate C:**
264
+
265
+ Evidence that multiple clients (mobile app, automation scripts) need to send structured commands.
266
+ At that point, the text-parsing approach becomes a reliability liability. The right test: if
267
+ a second coordinator (e.g., a work-queue coordinator) also needs to consume the message queue,
268
+ Candidate C's structured dispatch becomes clearly necessary.
269
+
270
+ **Invalidating assumption:**
271
+
272
+ Candidate B assumes the word-boundary `stop` regex is specific enough. If users commonly type
273
+ messages like "stop worrying and trust the process" via phone, the stop regex will fire. Mitigation:
274
+ require the stop keyword to appear as the first meaningful token in the message, or require a
275
+ command prefix (e.g., `/stop`). This can be tightened without changing the architecture.
276
+
277
+ ---
278
+
279
+ ## Open Questions for the Main Agent
280
+
281
+ 1. Should the drain function write an outbox notification for every actionable message, or only
282
+ for `stop` (where the coordinator is halting and the user needs confirmation)? Suggested:
283
+ write for all actionable messages (stop, skip-pr, add-pr) to close the feedback loop.
284
+
285
+ 2. The `stop` signal exits cleanly -- should the coordinator report which messages caused the
286
+ stop in its final report? Suggested: yes, log the message text and timestamp in the run log.
287
+
288
+ 3. Should `add-pr` messages add new PRs to the list before or after deduplication? Suggested:
289
+ add them to `prs` before Stage 1 begins, guarding against duplicates with a Set.