@exaudeus/workrail 3.38.0 → 3.40.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-worktrain.js +231 -0
- package/dist/console-ui/assets/{index-BtOJj6Xy.js → index-CXWCAonr.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/pr-review.d.ts +62 -0
- package/dist/coordinators/pr-review.js +575 -0
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +6 -3
- package/dist/manifest.json +58 -34
- package/dist/mcp/output-schemas.d.ts +10 -10
- package/dist/mcp/tools.d.ts +12 -12
- package/dist/trigger/trigger-router.js +9 -2
- package/dist/types/workflow-source.d.ts +0 -1
- package/dist/types/workflow-source.js +3 -6
- package/dist/types/workflow.d.ts +1 -1
- package/dist/types/workflow.js +1 -2
- package/dist/v2/durable-core/domain/artifact-contract-validator.js +66 -0
- package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +25 -0
- package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.js +31 -0
- package/dist/v2/durable-core/schemas/artifacts/index.d.ts +3 -1
- package/dist/v2/durable-core/schemas/artifacts/index.js +14 -1
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +41 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +30 -0
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +236 -236
- package/dist/v2/durable-core/schemas/session/events.d.ts +50 -50
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +4 -4
- package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
- package/dist/v2/usecases/console-routes.js +178 -0
- package/docs/design/coordinator-artifact-protocol-design-candidates.md +155 -0
- package/docs/design/coordinator-artifact-protocol-design-review.md +103 -0
- package/docs/design/coordinator-artifact-protocol-implementation-plan.md +259 -0
- package/docs/discovery/coordinator-design-review.md +73 -0
- package/docs/discovery/coordinator-script-design.md +96 -679
- package/docs/discovery/hypothesis-challenge-report.md +44 -0
- package/docs/discovery/simulation-report.md +85 -0
- package/docs/ideas/backlog.md +158 -100
- package/package.json +1 -1
- package/workflows/mr-review-workflow.agentic.v2.json +5 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Implementation Plan: Coordinator Artifact Protocol
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-18
|
|
4
|
+
**Branch:** `feat/coordinator-artifact-protocol`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The PR review coordinator (`src/coordinators/pr-review.ts`) extracts review severity from completed review sessions by running a keyword scan on free-form step notes. The coordinator ignores the `artifacts[]` field that `GET /api/v2/sessions/:id/nodes/:nodeId` already returns. This makes severity extraction brittle and unmeasurable.
|
|
11
|
+
|
|
12
|
+
The fix: define a `wr.review_verdict` artifact schema, update the final handoff step to emit it, update `getAgentResult()` to return artifacts alongside notes, and update the coordinator to try the artifact path before the keyword scan.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Acceptance Criteria
|
|
17
|
+
|
|
18
|
+
1. `npm run build` completes with 0 TypeScript errors
|
|
19
|
+
2. `tests/unit/coordinator-pr-review.test.ts` passes (all existing tests + new `readVerdictArtifact` tests)
|
|
20
|
+
3. `readVerdictArtifact([{ kind: 'wr.review_verdict', verdict: 'clean', ... }])` returns `{ severity: 'clean', source: 'artifact', ... }`
|
|
21
|
+
4. `readVerdictArtifact([])` returns `null`
|
|
22
|
+
5. `readVerdictArtifact([{ kind: 'wr.review_verdict', verdict: 'INVALID' }])` returns `null` and logs WARN
|
|
23
|
+
6. `CoordinatorDeps.getAgentResult` return type is `Promise<{ recapMarkdown: string | null; artifacts: readonly unknown[] }>`
|
|
24
|
+
7. `WorkflowRunSuccess` has optional field `lastStepArtifacts?: readonly unknown[]`
|
|
25
|
+
8. `mr-review-workflow.agentic.v2.json` phase-6-final-handoff has `outputContract: { contractRef: 'wr.contracts.review_verdict', required: false }`
|
|
26
|
+
9. `isValidContractRef('wr.contracts.review_verdict')` returns `true`
|
|
27
|
+
10. `validateArtifactContract([{ kind: 'wr.review_verdict', verdict: 'clean', ... }], { contractRef: 'wr.contracts.review_verdict' })` returns `{ valid: true, artifact: ... }`
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Non-Goals
|
|
32
|
+
|
|
33
|
+
- Do NOT add a `/api/v2/sessions/:id/artifacts` server-side aggregation endpoint
|
|
34
|
+
- Do NOT change `required: false` to `required: true` (post-graduation decision)
|
|
35
|
+
- Do NOT remove the keyword-scan fallback from `parseFindingsFromNotes`
|
|
36
|
+
- Do NOT add a `coordinatorProtocol` field to the workflow JSON (deferred)
|
|
37
|
+
- Do NOT add artifacts to `spawn_agent` return value (post-MVP)
|
|
38
|
+
- Do NOT make `source` required on `ReviewFindings` (breaking change deferred)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Philosophy Constraints
|
|
43
|
+
|
|
44
|
+
- **Make illegal states unrepresentable:** `verdict`, `source`, `confidence` use closed enums
|
|
45
|
+
- **Validate at boundaries:** Zod `safeParse` in `readVerdictArtifact()`; engine validation via `validateArtifactContract()`
|
|
46
|
+
- **Errors are data:** `readVerdictArtifact()` returns `ReviewFindings | null`, not throws
|
|
47
|
+
- **Functional/declarative:** `readVerdictArtifact()` is a pure function
|
|
48
|
+
- **Prefer fakes over mocks:** New tests use `makeFakeDeps()` pattern
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Invariants
|
|
53
|
+
|
|
54
|
+
1. `required: false` in outputContract -- never block sessions during transition
|
|
55
|
+
2. Schema registration (`ARTIFACT_CONTRACT_REFS`) MUST be done before workflow JSON update (compiler validates at load time via `isValidContractRef()`)
|
|
56
|
+
3. Keyword-scan fallback MUST remain live in `parseFindingsFromNotes`
|
|
57
|
+
4. All call sites of `CoordinatorDeps.getAgentResult` MUST handle `{ recapMarkdown, artifacts }` shape
|
|
58
|
+
5. `readVerdictArtifact()` MUST log `[WARN coord:reason=artifact_parse_failed]` when kind matches but safeParse fails
|
|
59
|
+
6. Per-node HTTP fetch failures MUST be caught individually (not by outer try/catch)
|
|
60
|
+
7. `makeContinueWorkflowTool` AND `makeCompleteStepTool` MUST both pass artifacts to `onComplete`
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Selected Approach
|
|
65
|
+
|
|
66
|
+
**Candidate A:** Three ordered changes, all additive, following existing repo patterns exactly.
|
|
67
|
+
|
|
68
|
+
**Rationale:** Zero new infrastructure; follows `loop-control.ts` schema pattern; follows `WorkflowRunSuccess.lastStepNotes` conditional spread pattern; follows `makeFakeDeps()` testing pattern; backward compatible via `required: false` + keyword-scan fallback.
|
|
69
|
+
|
|
70
|
+
**Runner-up:** Tip-node only artifact read. Disqualified by task spec 'CRITICAL: must aggregate artifacts across ALL session nodes'.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Slices
|
|
75
|
+
|
|
76
|
+
### Slice 1: Schema registration (prerequisite for all other changes)
|
|
77
|
+
|
|
78
|
+
**Files:**
|
|
79
|
+
- `src/v2/durable-core/schemas/artifacts/review-verdict.ts` (NEW)
|
|
80
|
+
- `src/v2/durable-core/schemas/artifacts/index.ts` (update)
|
|
81
|
+
- `src/v2/durable-core/domain/artifact-contract-validator.ts` (update)
|
|
82
|
+
|
|
83
|
+
**Work:**
|
|
84
|
+
1. Create `review-verdict.ts` following `loop-control.ts` pattern:
|
|
85
|
+
- `REVIEW_VERDICT_CONTRACT_REF = 'wr.contracts.review_verdict' as const`
|
|
86
|
+
- `ReviewVerdictArtifactV1Schema = z.object({ kind: z.literal('wr.review_verdict'), verdict: z.enum(['clean', 'minor', 'blocking']), confidence: z.enum(['high', 'medium', 'low']), findings: z.array(z.object({ severity: z.enum(['critical', 'major', 'minor', 'nit']), summary: z.string().min(1) }).strict()), summary: z.string().min(1) }).strict()`
|
|
87
|
+
- `isReviewVerdictArtifact()` type guard
|
|
88
|
+
- `parseReviewVerdictArtifact()` convenience function
|
|
89
|
+
2. Update `index.ts`: export all new symbols, add `'wr.contracts.review_verdict'` to `ARTIFACT_CONTRACT_REFS`
|
|
90
|
+
3. Update `artifact-contract-validator.ts`: import new symbols, add `case REVIEW_VERDICT_CONTRACT_REF:` to switch with `validateReviewVerdictContract()` helper
|
|
91
|
+
|
|
92
|
+
**Done when:** `isValidContractRef('wr.contracts.review_verdict')` returns `true`; `validateArtifactContract([{ kind: 'wr.review_verdict', ... }], { contractRef: 'wr.contracts.review_verdict' })` returns `{ valid: true, artifact: ... }`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Slice 2: Fix onComplete callback signature
|
|
97
|
+
|
|
98
|
+
**Files:**
|
|
99
|
+
- `src/daemon/workflow-runner.ts`
|
|
100
|
+
|
|
101
|
+
**Work:**
|
|
102
|
+
1. Change `onComplete` closure definition (line 2096) from `(notes: string | undefined): void` to `(notes: string | undefined, artifacts?: readonly unknown[]): void`
|
|
103
|
+
2. Add `let lastStepArtifacts: readonly unknown[] | undefined;` near `let lastStepNotes`
|
|
104
|
+
3. Update `onComplete` body to set `lastStepArtifacts = artifacts`
|
|
105
|
+
4. Add `lastStepArtifacts?: readonly unknown[]` to `WorkflowRunSuccess` interface
|
|
106
|
+
5. Update `makeCompleteStepTool` call to `onComplete(notes)` -> `onComplete(notes, params.artifacts as readonly unknown[] | undefined)` (line 1249)
|
|
107
|
+
6. Update `makeContinueWorkflowTool` call to `onComplete(params.notesMarkdown)` -> `onComplete(params.notesMarkdown, params.artifacts as readonly unknown[] | undefined)` (line 1046)
|
|
108
|
+
7. Update the final `return` in `runWorkflow()` (line 2622) to spread `lastStepArtifacts` conditionally
|
|
109
|
+
|
|
110
|
+
**Done when:** `WorkflowRunSuccess` has `lastStepArtifacts` field; both tool factory call sites pass artifacts; `npm run build` passes.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Slice 3: Update getAgentResult to return artifacts
|
|
115
|
+
|
|
116
|
+
**Files:**
|
|
117
|
+
- `src/cli-worktrain.ts`
|
|
118
|
+
|
|
119
|
+
**Work:**
|
|
120
|
+
1. Change `getAgentResult: async (sessionHandle: string): Promise<string | null>` -> `Promise<{ recapMarkdown: string | null; artifacts: readonly unknown[] }>`
|
|
121
|
+
2. In the implementation body:
|
|
122
|
+
- After reading `runs[0]`, read `runs[0].nodes` as `Array<{ nodeId: string; [key: string]: unknown }>` (with null check)
|
|
123
|
+
- Walk all nodes, fetch each node detail with individual `try/catch`:
|
|
124
|
+
```
|
|
125
|
+
for (const node of nodes) {
|
|
126
|
+
try {
|
|
127
|
+
const nodeRes = await fetch(nodeUrl + '/' + node.nodeId)
|
|
128
|
+
// collect artifacts from nodeData['artifacts']
|
|
129
|
+
} catch { /* log WARN, continue */ }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
- Return `{ recapMarkdown: recap, artifacts: collectedArtifacts }` (or `{ recapMarkdown: null, artifacts: [] }` on failure)
|
|
133
|
+
3. Early-return failures must also return `{ recapMarkdown: null, artifacts: [] }` instead of `null`
|
|
134
|
+
|
|
135
|
+
**Done when:** Return type is `Promise<{ recapMarkdown: string | null; artifacts: readonly unknown[] }>`; TypeScript compile-time errors at call sites force updates.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Slice 4: Update coordinator to use artifact path
|
|
140
|
+
|
|
141
|
+
**Files:**
|
|
142
|
+
- `src/coordinators/pr-review.ts`
|
|
143
|
+
|
|
144
|
+
**Work:**
|
|
145
|
+
1. Import `ReviewVerdictArtifactV1Schema` from artifacts schema
|
|
146
|
+
2. Update `CoordinatorDeps.getAgentResult` return type to match new shape
|
|
147
|
+
3. Add `source?: 'artifact' | 'keyword_scan'` to `ReviewFindings` interface
|
|
148
|
+
4. Add `readVerdictArtifact(artifacts: readonly unknown[]): ReviewFindings | null` pure function:
|
|
149
|
+
- Walk artifacts array
|
|
150
|
+
- For each, check `(raw as any).kind === 'wr.review_verdict'`
|
|
151
|
+
- If kind matches, call `ReviewVerdictArtifactV1Schema.safeParse(raw)`
|
|
152
|
+
- On success: return `{ severity: v.verdict, findingSummaries: v.findings.map(f => f.summary), raw: JSON.stringify(v), source: 'artifact' }`
|
|
153
|
+
- On failure: log `[WARN coord:reason=artifact_parse_failed]`, continue to next artifact
|
|
154
|
+
- If no valid artifact found and artifacts.length > 0: log `[INFO coord:source=keyword_scan reason=no_valid_artifact artifactCount=N]`
|
|
155
|
+
- Return `null`
|
|
156
|
+
5. Update both call sites in `runPrReviewCoordinator()`:
|
|
157
|
+
- `const { recapMarkdown: notes, artifacts } = await deps.getAgentResult(handle);`
|
|
158
|
+
- `const findingsResult = readVerdictArtifact(artifacts) ? ok(readVerdictArtifact(artifacts)!) : parseFindingsFromNotes(notes);`
|
|
159
|
+
- Log `[INFO coord:source=artifact]` or `[INFO coord:source=keyword_scan]`
|
|
160
|
+
6. Add divergence check (O2): if artifact verdict and keyword-scan severity disagree, log WARN
|
|
161
|
+
7. Update traceability JSON block to include `source` field
|
|
162
|
+
|
|
163
|
+
**Done when:** Coordinator tries artifact path first; keyword-scan fallback works; logging emits; `npm run build` passes.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### Slice 5: Update mr-review workflow
|
|
168
|
+
|
|
169
|
+
**Files:**
|
|
170
|
+
- `workflows/mr-review-workflow.agentic.v2.json`
|
|
171
|
+
|
|
172
|
+
**Work:**
|
|
173
|
+
1. In `phase-6-final-handoff` step, add `outputContract: { "contractRef": "wr.contracts.review_verdict", "required": false }`
|
|
174
|
+
2. Append to the step `prompt` field the artifact emission instruction:
|
|
175
|
+
```
|
|
176
|
+
\n\nAfter completing your notes, emit a structured verdict via complete_step artifacts[] parameter. Use exactly this schema:\n{ "kind": "wr.review_verdict", "verdict": "clean|minor|blocking", "confidence": "high|medium|low", "findings": [{ "severity": "critical|major|minor|nit", "summary": "one-line description" }], "summary": "one-line overall summary" }\nFor a clean review with no findings, use findings: [].
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Done when:** Workflow JSON validates via `npm run build`; `isValidContractRef('wr.contracts.review_verdict')` returns `true` (prerequisite: Slice 1 must be done first).
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Slice 6: Tests
|
|
184
|
+
|
|
185
|
+
**Files:**
|
|
186
|
+
- `tests/unit/coordinator-pr-review.test.ts`
|
|
187
|
+
|
|
188
|
+
**Work:**
|
|
189
|
+
1. Update `makeFakeDeps()` to return `{ recapMarkdown: string | null; artifacts: readonly unknown[] }` from `getAgentResult` (change return type from `string | null`)
|
|
190
|
+
2. Update `ReviewFindings` literal objects in `buildFixGoal` tests to add `source: 'artifact'` or `source: 'keyword_scan'` (or leave as optional -- `source?` means no update needed)
|
|
191
|
+
3. Add new `describe('readVerdictArtifact')` block:
|
|
192
|
+
- `it('returns ReviewFindings with source artifact for valid artifact')`
|
|
193
|
+
- `it('returns null for invalid schema (wrong verdict enum)')`
|
|
194
|
+
- `it('returns null for empty artifacts array')`
|
|
195
|
+
- `it('returns null for artifact with different kind')`
|
|
196
|
+
- `it('returns first valid artifact when multiple present')`
|
|
197
|
+
4. Import `readVerdictArtifact` from `pr-review.js`
|
|
198
|
+
|
|
199
|
+
**Done when:** All existing tests pass; 5 new `readVerdictArtifact` tests pass.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Test Design
|
|
204
|
+
|
|
205
|
+
**Unit tests (pure function):**
|
|
206
|
+
- `readVerdictArtifact` with valid `wr.review_verdict` artifact -> returns `ReviewFindings` with `severity` mapped from `verdict`, `source: 'artifact'`
|
|
207
|
+
- `readVerdictArtifact` with invalid schema (wrong enum) -> returns `null`
|
|
208
|
+
- `readVerdictArtifact` with empty array -> returns `null`
|
|
209
|
+
- `readVerdictArtifact` with artifact of different `kind` -> returns `null` (no false positives)
|
|
210
|
+
- `readVerdictArtifact` with valid + invalid artifacts -> returns valid one (first match wins)
|
|
211
|
+
|
|
212
|
+
**Integration tests (fake deps):**
|
|
213
|
+
- Existing `runPrReviewCoordinator` tests must pass with updated `getAgentResult` return type
|
|
214
|
+
- The fake `getAgentResult` returns `{ recapMarkdown: 'APPROVE ...', artifacts: [] }` by default
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Risk Register
|
|
219
|
+
|
|
220
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
221
|
+
|------|-----------|--------|------------|
|
|
222
|
+
| Missing `makeContinueWorkflowTool` onComplete update | Low | Silent -- artifacts not forwarded from continue_workflow path | Manual verification; code comment at both call sites |
|
|
223
|
+
| Per-node HTTP fetch error aborting aggregation | Low | Graceful fallback to keyword scan | Per-node try/catch (Slice 3 R2) |
|
|
224
|
+
| LLM emits extra fields in artifact (`.strict()` reject) | Medium | Zod fail -> WARN log -> keyword scan fallback | Acceptable during `required: false` transition |
|
|
225
|
+
| `runs[0].nodes` undefined or empty | Low | Empty artifact array -> keyword scan fallback | Null check in Slice 3 |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## PR Packaging Strategy
|
|
230
|
+
|
|
231
|
+
Single PR: `feat/coordinator-artifact-protocol`
|
|
232
|
+
|
|
233
|
+
All 6 slices in one PR. Changes are tightly coupled (schema + validator + coordinator must be consistent). Breaking the PR into multiple would require interface stubs that add noise.
|
|
234
|
+
|
|
235
|
+
**PR description structure:**
|
|
236
|
+
1. Summary: what was done and why
|
|
237
|
+
2. Change 1 (schema), Change 2 (onComplete), Change 3 (coordinator + workflow)
|
|
238
|
+
3. Test plan: `npm run build`, `npx vitest run tests/unit/coordinator-pr-review.test.ts`
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Philosophy Alignment
|
|
243
|
+
|
|
244
|
+
| Slice | Principle | Status |
|
|
245
|
+
|-------|-----------|--------|
|
|
246
|
+
| 1 (schema) | Make illegal states unrepresentable | Satisfied -- closed enums, kind literal |
|
|
247
|
+
| 1 (schema) | Validate at boundaries | Satisfied -- Zod strict schema |
|
|
248
|
+
| 2 (onComplete) | Immutability by default | Satisfied -- `readonly unknown[]` |
|
|
249
|
+
| 3 (getAgentResult) | Errors are data | Satisfied -- returns `{ recapMarkdown: null, artifacts: [] }` not null |
|
|
250
|
+
| 4 (coordinator) | Functional/declarative | Satisfied -- `readVerdictArtifact()` is pure |
|
|
251
|
+
| 4 (coordinator) | Make illegal states unrepresentable | Tension -- `source?` optional; accepted tradeoff |
|
|
252
|
+
| 6 (tests) | Prefer fakes over mocks | Satisfied -- `makeFakeDeps()` pattern |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## planConfidenceBand: High
|
|
257
|
+
|
|
258
|
+
- unresolvedUnknownCount: 0
|
|
259
|
+
- followUpTickets: Y1 (make source required post-graduation), Y2 (remove keyword scan post-graduation), spawn_agent artifacts gap (post-MVP)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Design Review Findings: PR Review Coordinator
|
|
2
|
+
|
|
3
|
+
*Review completed: 2026-04-18*
|
|
4
|
+
|
|
5
|
+
## Tradeoff Review
|
|
6
|
+
|
|
7
|
+
| Tradeoff | Verdict | Conditions for Failure |
|
|
8
|
+
|----------|---------|------------------------|
|
|
9
|
+
| Port discovery duplicated from worktrain-spawn.ts | Acceptable | Lock file names change (low risk, bounded) |
|
|
10
|
+
| Two-tier parser is heuristic | Acceptable for MVP | False escalation rate > 20% of clean PRs |
|
|
11
|
+
| Fix-agent loop max 3 passes | Acceptable | If 3 passes is too few for real codebases (tunable) |
|
|
12
|
+
|
|
13
|
+
## Failure Mode Review
|
|
14
|
+
|
|
15
|
+
| Failure Mode | Coverage | Risk |
|
|
16
|
+
|-------------|----------|------|
|
|
17
|
+
| null recapMarkdown -> unknown -> escalate | Full | Low -- conservative, correct |
|
|
18
|
+
| Fix loop 3 passes, persistent minor -> escalate | Full | Low |
|
|
19
|
+
| ECONNREFUSED -> clear error exit | Partial (needs timeout) | Low |
|
|
20
|
+
| Keyword false positive -> false escalation | Partial (needs negation check) | Medium |
|
|
21
|
+
| gh pr merge failure -> escalate without retry | Full | Low |
|
|
22
|
+
| Zombie session (null childSessionId) | Full | Low |
|
|
23
|
+
|
|
24
|
+
## Runner-Up / Simpler Alternative Review
|
|
25
|
+
|
|
26
|
+
Candidate A (subprocess model) has nothing worth borrowing. No beneficial hybrid exists. Candidate B is the correct shape. One naming improvement identified: `postResult` -> `writeFile(path, content)` for clarity.
|
|
27
|
+
|
|
28
|
+
## Philosophy Alignment
|
|
29
|
+
|
|
30
|
+
- All 8 CLAUDE.md principles satisfied
|
|
31
|
+
- Two minor tensions: mutable loop counter (acceptable, local), two-tier parser as patch (acceptable, follow-up filed)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Findings
|
|
36
|
+
|
|
37
|
+
### ORANGE: Keyword false positive risk -- negation context not handled
|
|
38
|
+
|
|
39
|
+
The keyword scanner will match `BLOCKING` in contexts like 'this is not technically blocking'. This is the most likely source of false escalations in practice.
|
|
40
|
+
|
|
41
|
+
**Recommended revision:** Before returning `'blocking'`, check that the `BLOCKING` keyword is not preceded by a negation within ~30 chars: `/\b(?:not|no|without)\b.{0,30}\bblocking\b/i` -> if this matches, do not classify as blocking. Apply same check to CRITICAL.
|
|
42
|
+
|
|
43
|
+
### ORANGE: No fetch timeout on HTTP calls
|
|
44
|
+
|
|
45
|
+
`spawnSession()` and `getAgentResult()` use bare `fetch()` with no timeout. If the daemon is running but unresponsive, the coordinator hangs indefinitely.
|
|
46
|
+
|
|
47
|
+
**Recommended revision:** Add `AbortSignal.timeout(30_000)` to all dispatch and session/node fetch calls. Catch `AbortError` and return `err('Daemon request timed out after 30s')`.
|
|
48
|
+
|
|
49
|
+
### YELLOW: `postResult` dep name is unclear
|
|
50
|
+
|
|
51
|
+
The `postResult(notes: string)` name is ambiguous -- does it post to Slack? Write to a file? Create a GitHub comment?
|
|
52
|
+
|
|
53
|
+
**Recommended revision:** Rename to `writeFile(path: string, content: string): Promise<void>`. The coordinator decides the report file path. This matches the UX spec: `Full report: ./coordinator-pr-review-2026-04-18.md`.
|
|
54
|
+
|
|
55
|
+
### YELLOW: Rule 3 adaptation not explicit in original 5 robustness rules spec
|
|
56
|
+
|
|
57
|
+
The original Rule 3 (go/no-go time check) was designed for daemon sessions with known `maxSessionMinutes`. The CLI coordinator has no such parameter.
|
|
58
|
+
|
|
59
|
+
**Recommended revision:** Explicit Rule 3 adaptation: `const coordinatorStartMs = deps.now()` at startup; before each `spawnSession()` call, check `deps.now() - coordinatorStartMs > 70 * 60 * 1000` (70 min = 90 min coordinator max - 20 min buffer) and refuse to spawn if exceeded.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Recommended Revisions
|
|
64
|
+
|
|
65
|
+
1. Add negation context check in keyword parser (`/\b(?:not|no|without)\b.{0,30}\bblocking\b/i`)
|
|
66
|
+
2. Add `AbortSignal.timeout(30_000)` to fetch calls
|
|
67
|
+
3. Rename `postResult` to `writeFile(path, content)`
|
|
68
|
+
4. Add explicit wall-clock Rule 3 adaptation to implementation spec
|
|
69
|
+
|
|
70
|
+
## Residual Concerns
|
|
71
|
+
|
|
72
|
+
- The two-tier parser's keyword scan is a heuristic. False escalation rate unknown until tested against real mr-review outputs. Follow-up: update mr-review-workflow to emit `## COORDINATOR_OUTPUT` JSON block.
|
|
73
|
+
- The coordinator's own timeout (90 minutes) is hardcoded. Should be configurable via `--max-runtime` flag if needed. Not for MVP.
|