@exaudeus/workrail 3.66.0 → 3.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/services/compiler/template-registry.js +10 -1
- package/dist/application/validation.js +1 -1
- package/dist/cli/commands/worktrain-init.js +1 -1
- package/dist/console/standalone-console.js +4 -1
- package/dist/console-ui/assets/{index-BynU38Vu.js → index-CyzltI6D.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/full-pipeline.js +4 -4
- package/dist/coordinators/modes/implement-shared.js +5 -5
- package/dist/coordinators/modes/implement.js +4 -4
- package/dist/coordinators/pr-review.js +4 -4
- package/dist/daemon/workflow-runner.d.ts +1 -0
- package/dist/daemon/workflow-runner.js +1 -0
- package/dist/infrastructure/storage/schema-validating-workflow-storage.d.ts +21 -2
- package/dist/infrastructure/storage/schema-validating-workflow-storage.js +48 -0
- package/dist/manifest.json +41 -41
- package/dist/mcp/handlers/v2-workflow.js +24 -7
- package/dist/mcp/output-schemas.d.ts +36 -0
- package/dist/mcp/output-schemas.js +11 -1
- package/dist/mcp/workflow-protocol-contracts.js +2 -2
- package/dist/v2/projections/session-metrics.d.ts +1 -1
- package/dist/v2/projections/session-metrics.js +16 -35
- package/dist/v2/usecases/console-routes.d.ts +2 -2
- package/docs/authoring-v2.md +4 -4
- package/docs/changelog-recent.md +3 -3
- package/docs/configuration.md +1 -1
- package/docs/design/adaptive-coordinator-context-candidates.md +1 -1
- package/docs/design/adaptive-coordinator-context.md +1 -1
- package/docs/design/adaptive-coordinator-routing-candidates.md +18 -18
- package/docs/design/adaptive-coordinator-routing-review.md +1 -1
- package/docs/design/adaptive-coordinator-routing.md +34 -34
- package/docs/design/agent-cascade-protocol.md +2 -2
- package/docs/design/console-daemon-separation-discovery.md +323 -0
- package/docs/design/context-assembly-design-candidates.md +1 -1
- package/docs/design/context-assembly-implementation-plan.md +1 -1
- package/docs/design/context-assembly-layer.md +2 -2
- package/docs/design/context-assembly-review-findings.md +1 -1
- package/docs/design/coordinator-access-audit.md +293 -0
- package/docs/design/coordinator-architecture-audit.md +62 -0
- package/docs/design/coordinator-error-handling-audit.md +240 -0
- package/docs/design/coordinator-testability-audit.md +426 -0
- package/docs/design/daemon-architecture-discovery.md +1 -1
- package/docs/design/daemon-console-separation-discovery.md +242 -0
- package/docs/design/daemon-memory-audit.md +203 -0
- package/docs/design/design-candidates-console-daemon-separation.md +256 -0
- package/docs/design/design-candidates-discovery-loop-fix.md +141 -0
- package/docs/design/design-review-findings-console-daemon-separation.md +106 -0
- package/docs/design/design-review-findings-discovery-loop-fix.md +81 -0
- package/docs/design/discovery-loop-fix-candidates.md +161 -0
- package/docs/design/discovery-loop-fix-design-review.md +106 -0
- package/docs/design/discovery-loop-fix-validation.md +258 -0
- package/docs/design/discovery-loop-investigation-A.md +188 -0
- package/docs/design/discovery-loop-investigation-B.md +287 -0
- package/docs/design/exploration-workflow-candidates.md +205 -0
- package/docs/design/exploration-workflow-design-review.md +166 -0
- package/docs/design/exploration-workflow-discovery.md +443 -0
- package/docs/design/ide-context-files-candidates.md +231 -0
- package/docs/design/ide-context-files-design-review.md +85 -0
- package/docs/design/ide-context-files.md +615 -0
- package/docs/design/implementation-plan-discovery-loop-fix.md +199 -0
- package/docs/design/implementation-plan-queue-poll-rotation.md +102 -0
- package/docs/design/in-process-http-audit.md +190 -0
- package/docs/design/layer3b-ghost-nodes-design-candidates.md +2 -2
- package/docs/design/loadSessionNotes-candidates.md +108 -0
- package/docs/design/loadSessionNotes-test-coverage-discovery.md +297 -0
- package/docs/design/loadSessionNotes-test-coverage-session4.md +209 -0
- package/docs/design/loadSessionNotes-test-coverage-v3.md +321 -0
- package/docs/design/probe-session-design-candidates.md +261 -0
- package/docs/design/probe-session-phase0.md +490 -0
- package/docs/design/routines-guide.md +7 -7
- package/docs/design/session-metrics-attribution-candidates.md +250 -0
- package/docs/design/session-metrics-attribution-design-review.md +115 -0
- package/docs/design/session-metrics-attribution-discovery.md +319 -0
- package/docs/design/session-metrics-candidates.md +227 -0
- package/docs/design/session-metrics-design-review.md +104 -0
- package/docs/design/session-metrics-discovery.md +454 -0
- package/docs/design/spawn-session-debug.md +202 -0
- package/docs/design/trigger-validator-candidates.md +214 -0
- package/docs/design/trigger-validator-review.md +109 -0
- package/docs/design/trigger-validator-shaping-phase0.md +239 -0
- package/docs/design/trigger-validator.md +454 -0
- package/docs/design/v2-core-design-locks.md +2 -2
- package/docs/design/workflow-extension-points.md +15 -15
- package/docs/design/workflow-id-validation-at-startup.md +1 -1
- package/docs/design/workflow-id-validation-implementation-plan.md +2 -2
- package/docs/design/workflow-trigger-lifecycle-audit.md +175 -0
- package/docs/design/worktrain-task-queue-candidates.md +5 -5
- package/docs/design/worktrain-task-queue.md +4 -4
- package/docs/discovery/coordinator-script-design.md +1 -1
- package/docs/discovery/coordinator-ux-discovery.md +3 -3
- package/docs/discovery/simulation-report.md +1 -1
- package/docs/discovery/workflow-modernization-discovery.md +326 -0
- package/docs/discovery/workflow-selection-for-discovery-tasks.md +33 -33
- package/docs/discovery/worktrain-status-briefing.md +1 -1
- package/docs/discovery/wr-discovery-goal-reframing.md +1 -1
- package/docs/docker.md +1 -1
- package/docs/ideas/backlog.md +227 -0
- package/docs/ideas/third-party-workflow-setup-design-thinking.md +1 -1
- package/docs/integrations/claude-code.md +5 -5
- package/docs/integrations/firebender.md +1 -1
- package/docs/plans/agentic-orchestration-roadmap.md +2 -2
- package/docs/plans/mr-review-workflow-redesign.md +9 -9
- package/docs/plans/ui-ux-workflow-design-candidates.md +4 -4
- package/docs/plans/ui-ux-workflow-discovery.md +2 -2
- package/docs/plans/workflow-categories-candidates.md +8 -8
- package/docs/plans/workflow-categories-discovery.md +4 -4
- package/docs/plans/workflow-modernization-design.md +430 -0
- package/docs/plans/workflow-staleness-detection-candidates.md +11 -11
- package/docs/plans/workflow-staleness-detection-review.md +4 -4
- package/docs/plans/workflow-staleness-detection.md +9 -9
- package/docs/plans/workrail-platform-vision.md +3 -3
- package/docs/reference/agent-context-cleaner-snippet.md +1 -1
- package/docs/reference/agent-context-guidance.md +4 -4
- package/docs/reference/context-optimization.md +2 -2
- package/docs/roadmap/now-next-later.md +2 -2
- package/docs/roadmap/open-work-inventory.md +16 -16
- package/docs/workflows.md +31 -31
- package/package.json +1 -1
- package/spec/workflow-tags.json +47 -47
- package/workflows/adaptive-ticket-creation.json +16 -16
- package/workflows/architecture-scalability-audit.json +22 -22
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/classify-task-workflow.json +1 -1
- package/workflows/coding-task-workflow-agentic.json +6 -6
- package/workflows/cross-platform-code-conversion.v2.json +8 -8
- package/workflows/document-creation-workflow.json +8 -8
- package/workflows/documentation-update-workflow.json +8 -8
- package/workflows/intelligent-test-case-generation.json +2 -2
- package/workflows/learner-centered-course-workflow.json +2 -2
- package/workflows/mr-review-workflow.agentic.v2.json +4 -4
- package/workflows/personal-learning-materials-creation-branched.json +8 -8
- package/workflows/presentation-creation.json +5 -5
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +31 -31
- package/workflows/routines/context-gathering.json +1 -1
- package/workflows/routines/design-review.json +1 -1
- package/workflows/routines/execution-simulation.json +1 -1
- package/workflows/routines/feature-implementation.json +3 -3
- package/workflows/routines/final-verification.json +1 -1
- package/workflows/routines/hypothesis-challenge.json +1 -1
- package/workflows/routines/ideation.json +1 -1
- package/workflows/routines/parallel-work-partitioning.json +3 -3
- package/workflows/routines/philosophy-alignment.json +2 -2
- package/workflows/routines/plan-analysis.json +1 -1
- package/workflows/routines/plan-generation.json +1 -1
- package/workflows/routines/tension-driven-design.json +6 -6
- package/workflows/scoped-documentation-workflow.json +26 -26
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-diagnose-environment.json +1 -1
- package/workflows/workflow-for-workflows.json +32 -77
- package/workflows/workflow-for-workflows.v2.json +0 -788
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Coordinator Access Audit: HTTP vs. In-Process Dep Functions
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-19
|
|
4
|
+
**Scope:** All `coordinatorDeps` functions in `src/trigger/trigger-listener.ts` (lines 410-752),
|
|
5
|
+
covering `CoordinatorDeps` and `AdaptiveCoordinatorDeps` interfaces.
|
|
6
|
+
|
|
7
|
+
**TL;DR:** Two dep functions (`awaitSessions`, `getAgentResult`) make HTTP calls to the daemon's
|
|
8
|
+
own console API (port 3456) from inside the daemon process. Both have direct in-process
|
|
9
|
+
replacements via `ConsoleService`. All other deps are correctly implemented.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Dep-by-Dep Audit Table
|
|
14
|
+
|
|
15
|
+
| Dep function | Lines | What it does | Transport | In-process access exists? | Breaks when HTTP fails? | Status |
|
|
16
|
+
|---|---|---|---|---|---|---|
|
|
17
|
+
| `spawnSession` | 411-476 | Allocate a session, dispatch to agent loop | **In-process** (`executeStartWorkflow` + `router.dispatch`) | n/a (already in-process) | No -- no HTTP | FIXED |
|
|
18
|
+
| `contextAssembler` | 478-497 | Assemble git diff + prior session notes | **CLI** (`git`, `gh` subprocesses) | No in-process alternative for `git`/`gh` | Only if `git`/`gh` binary unavailable -- acceptable | CORRECT |
|
|
19
|
+
| `awaitSessions` | 499-540 | Poll for session terminal status | **HTTP** to `GET /api/v2/sessions/:id` on port 3456 | Yes -- `ConsoleService.getSessionDetail()` | **Yes** -- ECONNREFUSED if port unavailable | **BUG** |
|
|
20
|
+
| `getAgentResult` | 542-609 | Fetch recap + artifacts from completed session | **HTTP** to `GET /api/v2/sessions/:id` + `/nodes/:id` on port 3456 | Yes -- `ConsoleService.getSessionDetail()` + `.getNodeDetail()` | **Yes** -- returns empty result silently | **BUG** |
|
|
21
|
+
| `listOpenPRs` | 611-622 | List open PRs via `gh pr list` | **CLI** (`gh` subprocess) | No in-process alternative | Only if `gh` unavailable -- acceptable | CORRECT |
|
|
22
|
+
| `mergePR` | 624-635 | Merge PR via `gh pr merge --squash` | **CLI** (`gh` subprocess) | No in-process alternative | Only if `gh` unavailable -- acceptable | CORRECT |
|
|
23
|
+
| `writeFile` | 637-639 | Write a file to disk | **Filesystem** (`fs.promises.writeFile`) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
24
|
+
| `readFile` | 641 | Read a file from disk | **Filesystem** (`fs.promises.readFile`) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
25
|
+
| `appendFile` | 643-644 | Append content to a file | **Filesystem** (`fs.promises.appendFile`) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
26
|
+
| `mkdir` | 646-647 | Create directory (recursive) | **Filesystem** (`fs.promises.mkdir`) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
27
|
+
| `homedir` | 649 | Return home directory path | **OS** (`os.homedir()`) | n/a | Never -- pure OS call | CORRECT |
|
|
28
|
+
| `joinPath` | 650 | Join path segments | **Pure** (`path.join`) | n/a | Never -- pure function | CORRECT |
|
|
29
|
+
| `nowIso` | 651 | Return ISO timestamp | **Pure** (`new Date().toISOString()`) | n/a | Never -- pure function | CORRECT |
|
|
30
|
+
| `generateId` | 652 | Generate UUID | **Pure** (`randomUUID()`) | n/a | Never -- pure function | CORRECT |
|
|
31
|
+
| `stderr` | 654 | Write to stderr | **Process** (`process.stderr.write`) | n/a | Never -- process I/O | CORRECT |
|
|
32
|
+
| `now` | 655 | Return current ms timestamp | **Pure** (`Date.now()`) | n/a | Never -- pure function | CORRECT |
|
|
33
|
+
| `port` | 656 | Resolved console port number | Constant (3456) | Not a function -- used by CLI context | n/a | REMOVE after fix |
|
|
34
|
+
| `fileExists` | 660 | Check file existence (sync) | **Filesystem** (`fs.existsSync`) | n/a | Only on unusual FS errors | CORRECT |
|
|
35
|
+
| `archiveFile` | 662-663 | Move a file (archive) | **Filesystem** (`fs.promises.rename`) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
36
|
+
| `pollForPR` | 665-692 | Poll `gh pr list` for a matching PR | **CLI** (`gh` subprocess in a loop) | No in-process alternative | Only if `gh` unavailable -- acceptable | CORRECT |
|
|
37
|
+
| `postToOutbox` | 694-705 | Append entry to `~/.workrail/outbox.jsonl` | **Filesystem** (direct JSONL append) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
38
|
+
| `pollOutboxAck` | 707-751 | Poll `~/.workrail/inbox-cursor.json` for ack | **Filesystem** (polling loop) | n/a | Only on disk I/O error -- acceptable | CORRECT |
|
|
39
|
+
|
|
40
|
+
**Summary:** 2 bugs (`awaitSessions`, `getAgentResult`). 20 deps correct. 1 constant to remove.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Priority-Ranked Fix List
|
|
45
|
+
|
|
46
|
+
### Priority 1 -- `awaitSessions` (all pipelines)
|
|
47
|
+
|
|
48
|
+
**File:** `src/trigger/trigger-listener.ts:499-540`
|
|
49
|
+
|
|
50
|
+
**Impact:** Every pipeline phase that spawns child sessions uses `awaitSessions`:
|
|
51
|
+
- `full-pipeline.ts`: discovery, shaping, ux-gate, coding sessions
|
|
52
|
+
- `implement.ts`: ux-gate, coding
|
|
53
|
+
- `implement-shared.ts`: review, fix-agent, audit, re-review
|
|
54
|
+
- `pr-review.ts`: review, fix, re-review
|
|
55
|
+
|
|
56
|
+
**Failure modes:**
|
|
57
|
+
1. Port unavailable (startup race, port conflict, test environment): `ECONNREFUSED` -- all handles returned as `outcome: 'failed'`. The coordinator interprets this as every spawned session having crashed, triggering cascading escalations.
|
|
58
|
+
2. HTTP server not yet bound: sessions created in-process by `spawnSession()` may be polled before the HTTP server starts. First poll returns 404 (session not found).
|
|
59
|
+
|
|
60
|
+
**Why high priority:** Failing `awaitSessions` terminates the entire coordinator pipeline. It is called at every await point across all pipeline modes.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### Priority 2 -- `getAgentResult` (verdict and handoff artifacts)
|
|
65
|
+
|
|
66
|
+
**File:** `src/trigger/trigger-listener.ts:542-609`
|
|
67
|
+
|
|
68
|
+
**Impact:** Called after every `awaitSessions` resolves with `outcome: 'success'` to extract recap markdown and artifacts. Used by:
|
|
69
|
+
- Review phases: reads `wr.review_verdict` artifact for typed verdict extraction
|
|
70
|
+
- Discovery phase (FULL pipeline): reads handoff artifact with shaping context
|
|
71
|
+
|
|
72
|
+
**Failure modes:**
|
|
73
|
+
1. Port unavailable: returns `{ recapMarkdown: null, artifacts: [] }` silently (no error thrown). The coordinator falls back to keyword-scan on `null` notes, producing `severity: 'unknown'` which routes to escalation.
|
|
74
|
+
2. 30-second per-fetch timeout: for sessions with many nodes, the N sequential node fetches can add minutes of latency.
|
|
75
|
+
3. Silent degradation: the `getAgentResult` fallback makes it look like the session succeeded but produced no usable output, causing the coordinator to escalate with opaque reasons.
|
|
76
|
+
|
|
77
|
+
**Why priority 2 (not 1):** Only fires after sessions actually complete. A broken `awaitSessions` prevents reaching `getAgentResult` entirely.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### Priority 3 -- Remove `port: DAEMON_CONSOLE_PORT` from `coordinatorDeps`
|
|
82
|
+
|
|
83
|
+
**File:** `src/trigger/trigger-listener.ts:656`
|
|
84
|
+
|
|
85
|
+
The `port` field is referenced in `pr-review.ts`'s `discoverConsolePort()` helper which is only called from the CLI entry point (`src/cli-worktrain.ts`), not from the in-process coordinator. After the two bugs above are fixed, this constant and the `DAEMON_CONSOLE_PORT = 3456` declaration (line 402) and its associated comment (lines 394-401) can be removed from `trigger-listener.ts`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 3. Recommended In-Process Architecture
|
|
90
|
+
|
|
91
|
+
### Root cause
|
|
92
|
+
|
|
93
|
+
`awaitSessions` and `getAgentResult` were implemented when the coordinator ran as an out-of-process CLI command (`worktrain run pr-review`). HTTP was the correct transport there. When the coordinator was moved into the daemon (TriggerRouter in `trigger-listener.ts`), only `spawnSession` was migrated to in-process. The other two deps were deferred. The comment at line 394 acknowledges this:
|
|
94
|
+
|
|
95
|
+
> `// WHY port=3456 (DAEMON_CONSOLE_PORT): still used by awaitSessions and getAgentResult which poll the console HTTP API`
|
|
96
|
+
|
|
97
|
+
### Current (broken) wiring
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
TriggerListener (daemon process)
|
|
101
|
+
├── coordinatorDeps (wired in startTriggerListener)
|
|
102
|
+
│ ├── spawnSession: executeStartWorkflow + router.dispatch [in-process, CORRECT]
|
|
103
|
+
│ ├── awaitSessions: executeWorktrainAwaitCommand
|
|
104
|
+
│ │ └── fetch http://127.0.0.1:3456/api/v2/sessions/:id [HTTP-to-self, BUG]
|
|
105
|
+
│ └── getAgentResult: globalThis.fetch
|
|
106
|
+
│ └── http://127.0.0.1:3456/api/v2/sessions/:id/nodes/... [HTTP-to-self, BUG]
|
|
107
|
+
└── startDaemonConsole() (separate call, after coordinatorDeps)
|
|
108
|
+
└── consoleService = new ConsoleService({ ctx.v2.sessionStore, ... })
|
|
109
|
+
├── getSessionDetail() [in-process, NOT wired to coordinatorDeps]
|
|
110
|
+
└── getNodeDetail() [in-process, NOT wired to coordinatorDeps]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Target (correct) wiring
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
TriggerListener (daemon process)
|
|
117
|
+
└── startTriggerListener()
|
|
118
|
+
├── consoleService = new ConsoleService({ [constructed BEFORE coordinatorDeps]
|
|
119
|
+
│ directoryListing: ctx.v2.directoryListing,
|
|
120
|
+
│ dataDir: ctx.v2.dataDir,
|
|
121
|
+
│ sessionStore: ctx.v2.sessionStore,
|
|
122
|
+
│ snapshotStore: ctx.v2.snapshotStore,
|
|
123
|
+
│ pinnedWorkflowStore: ctx.v2.pinnedStore,
|
|
124
|
+
│ })
|
|
125
|
+
└── coordinatorDeps
|
|
126
|
+
├── awaitSessions: in-process polling loop
|
|
127
|
+
│ └── consoleService.getSessionDetail(handle) [no HTTP, no port]
|
|
128
|
+
│ └── runs[0].status: ConsoleRunStatus
|
|
129
|
+
└── getAgentResult: in-process node reading
|
|
130
|
+
├── consoleService.getSessionDetail(handle) [no HTTP, no port]
|
|
131
|
+
│ └── runs[0]: { nodes, preferredTipNodeId }
|
|
132
|
+
└── consoleService.getNodeDetail(handle, nodeId) [no HTTP, no port]
|
|
133
|
+
└── { recapMarkdown, artifacts }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `awaitSessions` in-process design
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
awaitSessions: async (handles: readonly string[], timeoutMs: number) => {
|
|
140
|
+
const startMs = Date.now();
|
|
141
|
+
const pending = new Set(handles);
|
|
142
|
+
const results = new Map<string, SessionResult>();
|
|
143
|
+
|
|
144
|
+
while (pending.size > 0) {
|
|
145
|
+
const elapsed = Date.now() - startMs;
|
|
146
|
+
if (elapsed >= timeoutMs) {
|
|
147
|
+
// timeout: remaining handles marked as 'timeout'
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const handle of [...pending]) {
|
|
152
|
+
const detail = await consoleService.getSessionDetail(handle);
|
|
153
|
+
if (detail.isErr()) {
|
|
154
|
+
// 'SESSION_LOAD_FAILED' or 'NODE_NOT_FOUND': session not yet visible or corrupt
|
|
155
|
+
continue; // retry on next poll cycle
|
|
156
|
+
}
|
|
157
|
+
const run = detail.value.runs[0];
|
|
158
|
+
if (!run) continue; // session started but no run yet
|
|
159
|
+
|
|
160
|
+
const status: ConsoleRunStatus = run.status;
|
|
161
|
+
// Terminal: complete, complete_with_gaps (success), blocked (failed)
|
|
162
|
+
// in_progress: still running
|
|
163
|
+
// ConsoleRunStatus does not include 'dormant' -- the poll timeout covers dormant.
|
|
164
|
+
if (status === 'complete' || status === 'complete_with_gaps') {
|
|
165
|
+
results.set(handle, { handle, outcome: 'success', status, durationMs: Date.now() - startMs });
|
|
166
|
+
pending.delete(handle);
|
|
167
|
+
} else if (status === 'blocked') {
|
|
168
|
+
results.set(handle, { handle, outcome: 'failed', status, durationMs: Date.now() - startMs });
|
|
169
|
+
pending.delete(handle);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (pending.size > 0) {
|
|
174
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 3000));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Any remaining pending handles: timeout
|
|
179
|
+
for (const handle of pending) {
|
|
180
|
+
results.set(handle, { handle, outcome: 'timeout', status: null, durationMs: timeoutMs });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const resultsArray = [...results.values()];
|
|
184
|
+
return {
|
|
185
|
+
results: resultsArray,
|
|
186
|
+
allSucceeded: resultsArray.every((r) => r.outcome === 'success'),
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Key design decisions:
|
|
192
|
+
- `SESSION_LOAD_FAILED` on `getSessionDetail` is treated as "not ready yet" (retry), not as failure. This handles the case where the session event log has just been created by `spawnSession` but is not yet complete enough to project.
|
|
193
|
+
- `dormant` is not a `ConsoleRunStatus` -- it is a `ConsoleSessionStatus` computed by `ConsoleService` using mtime + `DORMANCY_THRESHOLD_MS`. The poll timeout (`timeoutMs`) handles sessions that go quiet.
|
|
194
|
+
- Poll interval of 3000ms matches the current `executeWorktrainAwaitCommand` default.
|
|
195
|
+
|
|
196
|
+
### `getAgentResult` in-process design
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
getAgentResult: async (sessionHandle: string) => {
|
|
200
|
+
const emptyResult = { recapMarkdown: null, artifacts: [] as readonly unknown[] };
|
|
201
|
+
|
|
202
|
+
const detailResult = await consoleService.getSessionDetail(sessionHandle);
|
|
203
|
+
if (detailResult.isErr()) return emptyResult;
|
|
204
|
+
|
|
205
|
+
const run = detailResult.value.runs[0];
|
|
206
|
+
if (!run) return emptyResult;
|
|
207
|
+
|
|
208
|
+
const allNodeIds = run.nodes.map((n) => n.nodeId);
|
|
209
|
+
const tipNodeId = run.preferredTipNodeId;
|
|
210
|
+
if (!tipNodeId) return emptyResult;
|
|
211
|
+
|
|
212
|
+
const nodeIdsToFetch = allNodeIds.length > 0 ? allNodeIds : [tipNodeId];
|
|
213
|
+
|
|
214
|
+
let recap: string | null = null;
|
|
215
|
+
const collectedArtifacts: unknown[] = [];
|
|
216
|
+
|
|
217
|
+
for (const nodeId of nodeIdsToFetch) {
|
|
218
|
+
const nodeResult = await consoleService.getNodeDetail(sessionHandle, nodeId);
|
|
219
|
+
if (nodeResult.isErr()) continue;
|
|
220
|
+
|
|
221
|
+
if (nodeId === tipNodeId) {
|
|
222
|
+
recap = nodeResult.value.recapMarkdown;
|
|
223
|
+
}
|
|
224
|
+
if (nodeResult.value.artifacts.length > 0) {
|
|
225
|
+
collectedArtifacts.push(...nodeResult.value.artifacts);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { recapMarkdown: recap, artifacts: collectedArtifacts };
|
|
230
|
+
},
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Key design decisions:
|
|
234
|
+
- Mirrors the existing HTTP logic exactly: all nodes for artifacts, tip node only for recap. The field names on `ConsoleNodeDetail` (`recapMarkdown`, `artifacts`) match what the current HTTP JSON parsing extracts.
|
|
235
|
+
- `ConsoleArtifact` has `{ sha256, contentType, byteLength, content }`. The `content` field carries the artifact payload. `readVerdictArtifact()` in `pr-review.ts` receives this array and searches for `kind: 'wr.review_verdict'` -- the shape must match what `projectArtifactsV2` produces. Verify `ConsoleArtifact.content` matches the artifact schema expected by `ReviewVerdictArtifactV1Schema`.
|
|
236
|
+
|
|
237
|
+
### `ConsoleService` injection
|
|
238
|
+
|
|
239
|
+
`ConsoleService` is already constructed in `daemon-console.ts:123-129` from `ctx.v2` ports. The simplest approach for `trigger-listener.ts` is to construct a second instance locally -- there is no correctness issue since the summary cache is instance-scoped and mtime-invalidated. The instance is cheap to construct.
|
|
240
|
+
|
|
241
|
+
Alternatively, `ConsoleService` could be threaded from `daemon-console.ts` through the call chain to `trigger-listener.ts`. This avoids a duplicate instance but requires a parameter change in `startTriggerListener()`. The local construction approach is recommended as the lower-risk first step.
|
|
242
|
+
|
|
243
|
+
`ConsoleService` does NOT need to be added to `CoordinatorDeps` or `AdaptiveCoordinatorDeps` interfaces. It is an implementation detail of the specific concrete dep functions, not a contract concern.
|
|
244
|
+
|
|
245
|
+
### What must NOT change
|
|
246
|
+
|
|
247
|
+
The CLI `run pr-review` command (`src/cli-worktrain.ts:1265+`) runs in a **separate process** from the daemon. Its `spawnSession`, `awaitSessions`, and `getAgentResult` deps correctly use HTTP to communicate with a running daemon. These are not bugs and must not be changed.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 4. `ConsoleService` Testability Audit
|
|
252
|
+
|
|
253
|
+
**Question:** Is `ConsoleService` properly abstracted for injection, or is it constructed inline making testing hard?
|
|
254
|
+
|
|
255
|
+
**Finding: Properly abstracted.**
|
|
256
|
+
|
|
257
|
+
`ConsoleService` takes `ConsoleServicePorts` in its constructor:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// src/v2/usecases/console-service.ts:314-315
|
|
261
|
+
export class ConsoleService {
|
|
262
|
+
constructor(private readonly ports: ConsoleServicePorts) {}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The `ConsoleServicePorts` interface:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
export interface ConsoleServicePorts {
|
|
269
|
+
readonly directoryListing: DirectoryListingPortV2;
|
|
270
|
+
readonly dataDir: DataDirPortV2;
|
|
271
|
+
readonly sessionStore: SessionEventLogReadonlyStorePortV2;
|
|
272
|
+
readonly snapshotStore: SnapshotStorePortV2;
|
|
273
|
+
readonly pinnedWorkflowStore: PinnedWorkflowStorePortV2;
|
|
274
|
+
readonly daemonRegistry?: DaemonRegistry; // optional
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
All ports are pure interfaces -- entirely fakeable in tests. No construction-inline issues. The same pattern as `WorktrainSpawnCommandDeps` / `WorktrainAwaitCommandDeps`.
|
|
279
|
+
|
|
280
|
+
**For testing the in-process coordinator deps:** A test can construct `ConsoleService` with a fake `sessionStore` that returns pre-seeded session events at specific statuses. This is far simpler than the current situation where tests must either start a real HTTP server or mock `globalThis.fetch`.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 5. Evidence of Known Tech Debt
|
|
285
|
+
|
|
286
|
+
The source code explicitly acknowledges both bugs. `src/trigger/trigger-listener.ts:394-401`:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// WHY port=3456 (DAEMON_CONSOLE_PORT): still used by awaitSessions and getAgentResult
|
|
290
|
+
// which poll the console HTTP API. This constant is kept for those paths.
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`spawnSession` was already migrated (see lines 418-476 and its `WHY in-process (not HTTP)` comment). The comment at line 394 confirms that `awaitSessions` and `getAgentResult` are the remaining two deferred items from that migration.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Coordinator Architecture Audit
|
|
2
|
+
|
|
3
|
+
**Status:** In progress
|
|
4
|
+
**Date:** 2026-04-19
|
|
5
|
+
**Scope:** `src/coordinators/`, `src/trigger/trigger-listener.ts`, `src/daemon/` (NOT `src/mcp/`)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Context / Ask
|
|
10
|
+
|
|
11
|
+
**Stated goal (solution-shaped):** Produce a dep-by-dep analysis of `CoordinatorDeps` and `AdaptiveCoordinatorDeps`, identify indirect access anti-patterns, find missing abstractions, and produce a priority-ranked fix list.
|
|
12
|
+
|
|
13
|
+
**Reframed problem:** WorkTrain coordinators cross the HTTP/shell boundary for data already available in-process via `ctx` (V2ToolContext), and `CoordinatorDeps` lacks the sub-interface structure needed to make these boundaries independently testable. The real risk: if the HTTP console is slow or unavailable, coordinator sessions degrade silently with misleading error messages.
|
|
14
|
+
|
|
15
|
+
**Anti-goals:**
|
|
16
|
+
- Do not audit `src/mcp/` (out of scope)
|
|
17
|
+
- Do not propose rewrites of coordinator logic -- only interface/wiring changes
|
|
18
|
+
- Do not change public CLI commands (`worktrain await` is correct for external callers)
|
|
19
|
+
|
|
20
|
+
**Primary uncertainty:** How many indirect-access sites exist beyond the two known ones (`awaitSessions`, `getAgentResult`)?
|
|
21
|
+
|
|
22
|
+
**Known approaches:**
|
|
23
|
+
- Replace HTTP polling in `awaitSessions` with in-process session store reads + DaemonRegistry
|
|
24
|
+
- Replace HTTP calls in `getAgentResult` with `projectNodeOutputsV2` projection on the session store
|
|
25
|
+
- Introduce a `SessionStatusPort` interface so coordinators can inject a fake for testing
|
|
26
|
+
|
|
27
|
+
**Path recommendation:** `landscape_first` -- the landscape (dep-by-dep analysis of what each function actually does) is the dominant need. The solution direction is already known; what's missing is the complete catalog of all anti-patterns and the exact recommended interface designs.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Artifact Strategy
|
|
32
|
+
|
|
33
|
+
This document is the human-readable output of the audit. It is NOT execution truth for the workflow -- notes and context variables in the WorkRail session are the durable record. If the session is rewound, this file may be stale; regenerate from notes.
|
|
34
|
+
|
|
35
|
+
**Capabilities available:**
|
|
36
|
+
- Delegation: YES (mcp__nested-subagent__Task available)
|
|
37
|
+
- Web browsing: Not needed (codebase-only audit)
|
|
38
|
+
- File reads: YES (main agent reads source files directly)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Landscape Packet
|
|
43
|
+
|
|
44
|
+
*(Populated during research phase)*
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Problem Frame Packet
|
|
49
|
+
|
|
50
|
+
*(Populated during analysis phase)*
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Candidate Directions
|
|
55
|
+
|
|
56
|
+
*(Populated during design phase)*
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Final Summary
|
|
61
|
+
|
|
62
|
+
*(Populated when audit is complete)*
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Coordinator Error Handling Audit
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-19
|
|
4
|
+
**Scope:** `src/coordinators/` coordinator layer
|
|
5
|
+
**Audited files:**
|
|
6
|
+
1. `src/coordinators/adaptive-pipeline.ts` - `PipelineOutcome` type and main entry point
|
|
7
|
+
2. `src/coordinators/modes/full-pipeline.ts` - FULL pipeline phase failure handling
|
|
8
|
+
3. `src/coordinators/modes/implement.ts` - IMPLEMENT mode executor
|
|
9
|
+
4. `src/coordinators/modes/implement-shared.ts` - shared review + verdict cycle
|
|
10
|
+
5. `src/coordinators/pr-review.ts` - reference coordinator
|
|
11
|
+
6. `src/runtime/result.ts` - the `Result<T,E>` type (baseline reference)
|
|
12
|
+
|
|
13
|
+
**Excluded:** `src/mcp/`, `src/v2/durable-core/`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Executive Summary
|
|
18
|
+
|
|
19
|
+
The coordinator layer is broadly sound. The "errors are data" principle is followed: no bare `throw` statements exist in any coordinator file, all session spawn failures return `PipelineOutcome { kind: 'escalated' }`, and `Result<T,string>` is used correctly for the `spawnSession` / `mergePR` boundaries. `WorkflowRunResult` is guarded with `assertNever` in `trigger-router.ts`. The `PipelineOutcome` discriminated union is well-formed.
|
|
20
|
+
|
|
21
|
+
One genuine runtime bug and two design inconsistencies are present. The bug (missing null-guard) is the only finding that can cause incorrect behavior at runtime.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Findings
|
|
26
|
+
|
|
27
|
+
### Finding F1 - Critical
|
|
28
|
+
|
|
29
|
+
**Title:** Missing zombie-detection null-guard on `uxHandle` in `implement.ts`
|
|
30
|
+
|
|
31
|
+
**File:line:** `src/coordinators/modes/implement.ts:144-145`
|
|
32
|
+
|
|
33
|
+
**Description:**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const uxHandle = uxSpawnResult.value;
|
|
37
|
+
const uxAwait = await deps.awaitSessions([uxHandle], REVIEW_TIMEOUT_MS);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`uxHandle` is passed directly to `deps.awaitSessions` without checking for an empty/null value. Every other session handle in the coordinator layer has an explicit zombie-detection guard before being passed to `awaitSessions`:
|
|
41
|
+
|
|
42
|
+
- `implement.ts:189-195` (`codingHandle`)
|
|
43
|
+
- `full-pipeline.ts:222-228` (`discoveryHandle`)
|
|
44
|
+
- `full-pipeline.ts:300-305` (`shapingHandle`)
|
|
45
|
+
- `full-pipeline.ts:348-354` (`uxHandle` - the equivalent check in FULL mode, which this file is missing)
|
|
46
|
+
- `full-pipeline.ts:428-433` (`codingHandle`)
|
|
47
|
+
- `implement-shared.ts:68-74` (`reviewHandle`)
|
|
48
|
+
- `implement-shared.ts:148-153` (`fixHandle`)
|
|
49
|
+
- `implement-shared.ts:233-243` (`auditHandle`)
|
|
50
|
+
- `implement-shared.ts:286-296` (`reReviewHandle`)
|
|
51
|
+
|
|
52
|
+
**Risk:** If `deps.spawnSession` returns `ok('')` (empty string), an empty handle is passed to `awaitSessions`. Downstream behavior depends on the `awaitSessions` implementation, but at minimum it could return a result for a non-existent session, causing the UX gate to be treated as successfully completed when it was not. The coding session would then spawn without UX review having occurred.
|
|
53
|
+
|
|
54
|
+
**Recommended fix:**
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const uxHandle = uxSpawnResult.value;
|
|
58
|
+
if (!uxHandle) {
|
|
59
|
+
return {
|
|
60
|
+
kind: 'escalated',
|
|
61
|
+
escalationReason: { phase: 'ux-gate', reason: 'UX design session returned empty handle' },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const uxAwait = await deps.awaitSessions([uxHandle], REVIEW_TIMEOUT_MS);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This matches the pattern used at `implement.ts:189-195` and `full-pipeline.ts:348-354`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### Finding F2 - Major
|
|
72
|
+
|
|
73
|
+
**Title:** Non-exhaustive `switch` on `ReviewSeverity` in `implement-shared.ts` - missing `assertNever`
|
|
74
|
+
|
|
75
|
+
**File:line:** `src/coordinators/modes/implement-shared.ts:110-175`
|
|
76
|
+
|
|
77
|
+
**Description:**
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
switch (findings.severity) {
|
|
81
|
+
case 'clean':
|
|
82
|
+
return { kind: 'merged', prUrl };
|
|
83
|
+
|
|
84
|
+
case 'minor': {
|
|
85
|
+
// ... fix loop ...
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'blocking':
|
|
89
|
+
case 'unknown': {
|
|
90
|
+
return runAuditChain(...);
|
|
91
|
+
}
|
|
92
|
+
// no default
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`ReviewSeverity = 'clean' | 'minor' | 'blocking' | 'unknown'` is a 4-variant union. The switch covers all four, but there is no `default: assertNever(findings.severity)`. TypeScript does not produce a compile error for switches missing a `default` branch - it only enforces exhaustiveness when the fallthrough type is narrowed to `never`.
|
|
97
|
+
|
|
98
|
+
**Risk:** If `ReviewSeverity` is widened with a new variant (e.g. `'critical'`), the `switch` will compile successfully and the new variant will fall through without being routed. Depending on TypeScript's control-flow analysis, this may return `undefined` (typed as `PipelineOutcome`) and corrupt the coordinator's return value silently.
|
|
99
|
+
|
|
100
|
+
**Note:** The same switch pattern exists in `runFixAgentLoop` in `pr-review.ts` at several points, but `pr-review.ts` uses `PrOutcome` (not `PipelineOutcome`) and the consequences are less severe.
|
|
101
|
+
|
|
102
|
+
**Recommended fix:**
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
switch (findings.severity) {
|
|
106
|
+
case 'clean':
|
|
107
|
+
...
|
|
108
|
+
case 'minor': {
|
|
109
|
+
...
|
|
110
|
+
}
|
|
111
|
+
case 'blocking':
|
|
112
|
+
case 'unknown': {
|
|
113
|
+
return runAuditChain(...);
|
|
114
|
+
}
|
|
115
|
+
default:
|
|
116
|
+
return assertNever(findings.severity);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Import `assertNever` from `../../runtime/assert-never.js`. The same fix applies to the corresponding switch at `implement-shared.ts:334` (`if (reFindings.severity === 'clean' || reFindings.severity === 'minor')`) - that one uses `if/else if` rather than `switch`, and the `else` arm covers the remaining cases. Consider converting to a `switch` with `assertNever` for consistency.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### Finding F3 - Major
|
|
125
|
+
|
|
126
|
+
**Title:** `process.stderr.write` bypasses injected `deps.stderr` in `pr-review.ts`
|
|
127
|
+
|
|
128
|
+
**File:line:** `src/coordinators/pr-review.ts:445-449` inside `readVerdictArtifact()`
|
|
129
|
+
|
|
130
|
+
**Description:**
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`[WARN coord:reason=artifact_parse_failed handle=${handlePrefix}] readVerdictArtifact: wr.review_verdict schema validation failed: ${issues}\n`,
|
|
135
|
+
);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
All other log calls in `pr-review.ts` use the injected `deps.stderr()` function or the local `log()` closure. This single call uses the Node.js global `process.stderr.write` directly, bypassing the injected dependency contract.
|
|
139
|
+
|
|
140
|
+
**Risk:** Test fakes that inject a no-op or recording `stderr` will miss this warning silently. When a schema-invalid verdict artifact is emitted by an agent, the warning will appear in process stderr even in test environments that redirect all deps-based logging.
|
|
141
|
+
|
|
142
|
+
**Recommended fix:**
|
|
143
|
+
|
|
144
|
+
`readVerdictArtifact` currently accepts no `deps` parameter. Two options:
|
|
145
|
+
|
|
146
|
+
**Option A** (preferred - consistent with `full-pipeline.ts:readDiscoveryHandoffArtifact`): Add an optional `stderrFn` parameter:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
export function readVerdictArtifact(
|
|
150
|
+
artifacts: readonly unknown[],
|
|
151
|
+
sessionHandle?: string,
|
|
152
|
+
stderrFn?: (line: string) => void,
|
|
153
|
+
): ReviewFindings | null {
|
|
154
|
+
// ...
|
|
155
|
+
(stderrFn ?? ((s) => process.stderr.write(s + '\n')))(
|
|
156
|
+
`[WARN coord:reason=artifact_parse_failed handle=${handlePrefix}] ...`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Option B** (minimal): Accept the hardcoded `process.stderr.write` as intentional for this utility function and document it explicitly. This is the lower-effort option but does not fix the test isolation gap.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### Finding F4 - Minor
|
|
166
|
+
|
|
167
|
+
**Title:** `checkSpawnCutoff()` returns `PipelineOutcome | null` rather than an `Option` type
|
|
168
|
+
|
|
169
|
+
**File:line:** `src/coordinators/adaptive-pipeline.ts:245-260`
|
|
170
|
+
|
|
171
|
+
**Description:**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
export function checkSpawnCutoff(
|
|
175
|
+
coordinatorStartMs: number,
|
|
176
|
+
now: number,
|
|
177
|
+
phase: string,
|
|
178
|
+
): PipelineOutcome | null {
|
|
179
|
+
if (now - coordinatorStartMs > COORDINATOR_SPAWN_CUTOFF_MS) {
|
|
180
|
+
return { kind: 'escalated', ... };
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`null` is used as a sentinel for "safe to spawn." Callers check `if (cutoffCheck) return cutoffCheck;`. This diverges slightly from the "errors are data" principle (null as absence) but is scoped to one helper with a clear usage pattern.
|
|
187
|
+
|
|
188
|
+
**Risk:** None at runtime. Callers immediately check the return. The naming (`checkSpawnCutoff`) clearly implies a nullable return. This is a stylistic inconsistency, not a functional issue.
|
|
189
|
+
|
|
190
|
+
**Recommended fix (optional):** If the project ever adopts an `Option<T>` type, this is a good candidate for `Option<PipelineOutcome>`. As-is, the current implementation is clear and the null is well-understood at each call site.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Finding F5 - Minor
|
|
195
|
+
|
|
196
|
+
**Title:** `dispatchAdaptivePipeline` returns `{ kind: 'escalated' }` for dedup-skip, which is semantically impure
|
|
197
|
+
|
|
198
|
+
**File:line:** `src/trigger/trigger-router.ts:1003-1030`
|
|
199
|
+
|
|
200
|
+
**Description:**
|
|
201
|
+
|
|
202
|
+
When a duplicate adaptive dispatch is detected within the 30-second dedup window, `dispatchAdaptivePipeline` returns:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
return {
|
|
206
|
+
kind: 'escalated',
|
|
207
|
+
escalationReason: { phase: 'dispatch', reason: 'duplicate ...' },
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The code comment at line 1003-1007 explicitly acknowledges this: *"WHY 'escalated' as the return kind: PipelineOutcome has no 'skipped' variant."*
|
|
212
|
+
|
|
213
|
+
**Risk:** None at runtime. The calling path (GitHub queue poller) is fire-and-forget and does not branch on `outcome.kind`. This is acknowledged technical debt.
|
|
214
|
+
|
|
215
|
+
**Recommended fix (optional):** Add `{ readonly kind: 'skipped'; readonly reason: string }` to `PipelineOutcome` in `adaptive-pipeline.ts`. Update `dispatchAdaptivePipeline` to use it. Update any caller that switches on `outcome.kind` to add a `case 'skipped':` arm (with `assertNever` in the default).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Non-Issues
|
|
220
|
+
|
|
221
|
+
The following patterns were audited and are **not** problems:
|
|
222
|
+
|
|
223
|
+
- **No bare `throw` in coordinator files.** Archive failures in `implement.ts` and `full-pipeline.ts` `finally` blocks catch exceptions and log them without re-throwing - correct.
|
|
224
|
+
- **`PipelineOutcome.escalationReason.reason` is `string`.** String is appropriate at the coordinator boundary (external-facing). Internal domain types use discriminated unions; coordinator escalation reasons are human-readable strings for operator consumption.
|
|
225
|
+
- **`WorkflowRunResult` -> `PipelineOutcome` mapping.** These types operate at different architectural layers and are never mapped to each other. `assertNever` is correctly present in `trigger-router.ts` for `WorkflowRunResult` switches (lines 811-814 and 918-921).
|
|
226
|
+
- **`outcome !== 'success'` string comparisons.** These compare against session result outcomes from the daemon layer - the correct approach at a typed interface boundary.
|
|
227
|
+
- **`readDiscoveryHandoffArtifact` returning `null`.** The null is not leaked outside the function boundary; the function is a local helper returning an optional domain object.
|
|
228
|
+
- **`parseFindingsFromNotes` catching JSON parse errors silently.** The `try/catch` in the JSON block scanner (`pr-review.ts:322-337`) is correct - JSON.parse throws for malformed input and the catch correctly advances to the next block.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Severity Summary
|
|
233
|
+
|
|
234
|
+
| ID | File | Severity | Title |
|
|
235
|
+
|----|------|----------|-------|
|
|
236
|
+
| F1 | `src/coordinators/modes/implement.ts:144` | **Critical** | Missing null-guard on `uxHandle` (zombie detection) |
|
|
237
|
+
| F2 | `src/coordinators/modes/implement-shared.ts:110` | **Major** | Non-exhaustive `ReviewSeverity` switch - no `assertNever` |
|
|
238
|
+
| F3 | `src/coordinators/pr-review.ts:445` | **Major** | `process.stderr.write` bypasses injected `deps.stderr` |
|
|
239
|
+
| F4 | `src/coordinators/adaptive-pipeline.ts:248` | Minor | `checkSpawnCutoff` returns `null` as sentinel |
|
|
240
|
+
| F5 | `src/trigger/trigger-router.ts:1026` | Minor | `escalated` used for dedup-skip (acknowledged in comments) |
|