@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.
- package/dist/cli/commands/init.js +0 -3
- package/dist/cli-worktrain.js +48 -11
- package/dist/cli.js +0 -18
- package/dist/config/app-config.d.ts +0 -16
- package/dist/config/app-config.js +0 -14
- package/dist/config/config-file.js +0 -3
- package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
- package/dist/console-ui/assets/index-DwfWMKvv.js +28 -0
- package/dist/console-ui/index.html +2 -2
- package/dist/context-assembly/deps.d.ts +8 -0
- package/dist/context-assembly/deps.js +2 -0
- package/dist/context-assembly/index.d.ts +6 -0
- package/dist/context-assembly/index.js +50 -0
- package/dist/context-assembly/infra.d.ts +3 -0
- package/dist/context-assembly/infra.js +154 -0
- package/dist/context-assembly/types.d.ts +30 -0
- package/dist/context-assembly/types.js +2 -0
- package/dist/coordinators/pr-review.d.ts +20 -1
- package/dist/coordinators/pr-review.js +189 -4
- package/dist/daemon/daemon-events.d.ts +9 -1
- package/dist/daemon/soul-template.d.ts +2 -2
- package/dist/daemon/soul-template.js +11 -1
- package/dist/daemon/workflow-runner.d.ts +14 -1
- package/dist/daemon/workflow-runner.js +406 -25
- package/dist/di/container.js +1 -25
- package/dist/di/tokens.d.ts +0 -3
- package/dist/di/tokens.js +0 -3
- package/dist/domain/execution/state.d.ts +6 -6
- package/dist/engine/engine-factory.js +0 -1
- package/dist/infrastructure/console-defaults.d.ts +1 -0
- package/dist/infrastructure/console-defaults.js +4 -0
- package/dist/infrastructure/session/index.d.ts +0 -1
- package/dist/infrastructure/session/index.js +1 -3
- package/dist/manifest.json +138 -122
- package/dist/mcp/handlers/session.d.ts +1 -0
- package/dist/mcp/handlers/session.js +61 -13
- package/dist/mcp/handlers/v2-workflow.d.ts +2 -2
- package/dist/mcp/output-schemas.d.ts +234 -234
- package/dist/mcp/server.js +1 -18
- package/dist/mcp/tools.d.ts +2 -2
- package/dist/mcp/transports/http-entry.js +0 -2
- package/dist/mcp/transports/stdio-entry.js +1 -2
- package/dist/mcp/types.d.ts +0 -2
- package/dist/mcp/v2/tools.d.ts +24 -24
- package/dist/trigger/daemon-console.d.ts +2 -0
- package/dist/trigger/daemon-console.js +1 -1
- package/dist/trigger/trigger-listener.d.ts +2 -0
- package/dist/trigger/trigger-listener.js +3 -1
- package/dist/trigger/trigger-router.d.ts +4 -3
- package/dist/trigger/trigger-router.js +4 -3
- package/dist/trigger/trigger-store.js +17 -4
- package/dist/v2/durable-core/schemas/artifacts/assessment.d.ts +2 -2
- package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +2 -2
- package/dist/v2/durable-core/schemas/artifacts/loop-control.d.ts +6 -6
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +6 -6
- package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +56 -56
- package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +83 -83
- package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +1024 -1024
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +2336 -2336
- package/dist/v2/durable-core/schemas/session/dag-topology.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/events.d.ts +339 -339
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +30 -30
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +3 -3
- package/dist/v2/usecases/console-routes.d.ts +2 -1
- package/dist/v2/usecases/console-routes.js +29 -5
- package/dist/v2/usecases/console-service.js +14 -0
- package/dist/v2/usecases/console-types.d.ts +1 -0
- package/docs/authoring.md +16 -16
- package/docs/design/context-assembly-design-candidates.md +199 -0
- package/docs/design/context-assembly-implementation-plan.md +211 -0
- package/docs/design/context-assembly-review-findings.md +112 -0
- package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
- package/docs/design/coordinator-message-queue-drain-review.md +120 -0
- package/docs/design/coordinator-message-queue-drain.md +289 -0
- package/docs/design/shaping-workflow-external-research.md +119 -0
- package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
- package/docs/discovery/late-bound-goals-review.md +82 -0
- package/docs/discovery/late-bound-goals.md +118 -0
- package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
- package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
- package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
- package/docs/ideas/backlog.md +356 -0
- package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
- package/docs/ideas/design-candidates-session-tree-view.md +196 -0
- package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
- package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
- package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
- package/package.json +2 -1
- package/spec/authoring-spec.json +16 -16
- package/spec/shape.schema.json +178 -0
- package/spec/workflow-tags.json +232 -47
- package/workflows/coding-task-workflow-agentic.json +491 -480
- package/workflows/wr.shaping.json +182 -0
- package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
- package/dist/console-ui/assets/index-CXWCAonr.js +0 -28
- package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
- package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
- package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
- package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
- package/dist/infrastructure/session/HttpServer.d.ts +0 -60
- package/dist/infrastructure/session/HttpServer.js +0 -912
- package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
- 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.
|