@exaudeus/workrail 3.39.0 → 3.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/cli/commands/init.js +0 -3
  2. package/dist/cli-worktrain.js +58 -26
  3. package/dist/cli.js +0 -18
  4. package/dist/config/app-config.d.ts +0 -16
  5. package/dist/config/app-config.js +0 -14
  6. package/dist/config/config-file.js +0 -3
  7. package/dist/console-ui/assets/index-CQt4UhPB.js +28 -0
  8. package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
  9. package/dist/console-ui/index.html +2 -2
  10. package/dist/coordinators/pr-review.d.ts +23 -1
  11. package/dist/coordinators/pr-review.js +224 -5
  12. package/dist/daemon/daemon-events.d.ts +9 -1
  13. package/dist/daemon/soul-template.d.ts +2 -2
  14. package/dist/daemon/soul-template.js +11 -1
  15. package/dist/daemon/workflow-runner.d.ts +17 -3
  16. package/dist/daemon/workflow-runner.js +401 -28
  17. package/dist/di/container.js +1 -25
  18. package/dist/di/tokens.d.ts +0 -3
  19. package/dist/di/tokens.js +0 -3
  20. package/dist/engine/engine-factory.js +0 -1
  21. package/dist/infrastructure/console-defaults.d.ts +1 -0
  22. package/dist/infrastructure/console-defaults.js +4 -0
  23. package/dist/infrastructure/session/index.d.ts +0 -1
  24. package/dist/infrastructure/session/index.js +1 -3
  25. package/dist/manifest.json +124 -124
  26. package/dist/mcp/handlers/session.d.ts +1 -0
  27. package/dist/mcp/handlers/session.js +61 -13
  28. package/dist/mcp/output-schemas.d.ts +10 -10
  29. package/dist/mcp/server.js +1 -18
  30. package/dist/mcp/tools.d.ts +12 -12
  31. package/dist/mcp/transports/http-entry.js +0 -2
  32. package/dist/mcp/transports/stdio-entry.js +1 -2
  33. package/dist/mcp/types.d.ts +0 -2
  34. package/dist/trigger/daemon-console.d.ts +2 -0
  35. package/dist/trigger/daemon-console.js +1 -1
  36. package/dist/trigger/trigger-listener.d.ts +2 -0
  37. package/dist/trigger/trigger-listener.js +3 -1
  38. package/dist/trigger/trigger-router.d.ts +4 -3
  39. package/dist/trigger/trigger-router.js +13 -5
  40. package/dist/trigger/trigger-store.js +17 -4
  41. package/dist/types/workflow-source.d.ts +0 -1
  42. package/dist/types/workflow-source.js +3 -6
  43. package/dist/types/workflow.d.ts +1 -1
  44. package/dist/types/workflow.js +1 -2
  45. package/dist/v2/durable-core/domain/artifact-contract-validator.js +66 -0
  46. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +25 -0
  47. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.js +31 -0
  48. package/dist/v2/durable-core/schemas/artifacts/index.d.ts +3 -1
  49. package/dist/v2/durable-core/schemas/artifacts/index.js +14 -1
  50. package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +41 -0
  51. package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +30 -0
  52. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +236 -236
  53. package/dist/v2/durable-core/schemas/session/events.d.ts +50 -50
  54. package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
  55. package/dist/v2/durable-core/schemas/session/manifest.d.ts +4 -4
  56. package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
  57. package/dist/v2/usecases/console-routes.d.ts +2 -1
  58. package/dist/v2/usecases/console-routes.js +207 -5
  59. package/dist/v2/usecases/console-service.js +14 -0
  60. package/dist/v2/usecases/console-types.d.ts +1 -0
  61. package/docs/authoring.md +16 -16
  62. package/docs/design/coordinator-artifact-protocol-design-candidates.md +155 -0
  63. package/docs/design/coordinator-artifact-protocol-design-review.md +103 -0
  64. package/docs/design/coordinator-artifact-protocol-implementation-plan.md +259 -0
  65. package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
  66. package/docs/design/coordinator-message-queue-drain-review.md +120 -0
  67. package/docs/design/coordinator-message-queue-drain.md +289 -0
  68. package/docs/design/shaping-workflow-external-research.md +119 -0
  69. package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
  70. package/docs/discovery/late-bound-goals-review.md +82 -0
  71. package/docs/discovery/late-bound-goals.md +118 -0
  72. package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
  73. package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
  74. package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
  75. package/docs/ideas/backlog.md +447 -97
  76. package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
  77. package/docs/ideas/design-candidates-session-tree-view.md +196 -0
  78. package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
  79. package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
  80. package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
  81. package/package.json +2 -1
  82. package/spec/authoring-spec.json +16 -16
  83. package/spec/shape.schema.json +178 -0
  84. package/spec/workflow-tags.json +232 -47
  85. package/workflows/coding-task-workflow-agentic.json +491 -480
  86. package/workflows/mr-review-workflow.agentic.v2.json +5 -1
  87. package/workflows/wr.shaping.json +182 -0
  88. package/dist/console-ui/assets/index-3oXZ_A9m.js +0 -28
  89. package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
  90. package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
  91. package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
  92. package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
  93. package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
  94. package/dist/infrastructure/session/HttpServer.d.ts +0 -60
  95. package/dist/infrastructure/session/HttpServer.js +0 -912
  96. package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
  97. package/workflows/coding-task-workflow-agentic.v2.json +0 -324
@@ -90,8 +90,6 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
90
90
  content?: unknown;
91
91
  }>]>;
92
92
  }, "strip", z.ZodTypeAny, {
93
- outputId: string;
94
- outputChannel: "recap" | "artifact";
95
93
  payload: {
96
94
  payloadKind: "notes";
97
95
  notesMarkdown: string;
@@ -102,10 +100,10 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
102
100
  byteLength: number;
103
101
  content?: unknown;
104
102
  };
105
- supersedesOutputId?: string | undefined;
106
- }, {
107
103
  outputId: string;
108
104
  outputChannel: "recap" | "artifact";
105
+ supersedesOutputId?: string | undefined;
106
+ }, {
109
107
  payload: {
110
108
  payloadKind: "notes";
111
109
  notesMarkdown: string;
@@ -116,10 +114,10 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
116
114
  byteLength: number;
117
115
  content?: unknown;
118
116
  };
119
- supersedesOutputId?: string | undefined;
120
- }>, {
121
117
  outputId: string;
122
118
  outputChannel: "recap" | "artifact";
119
+ supersedesOutputId?: string | undefined;
120
+ }>, {
123
121
  payload: {
124
122
  payloadKind: "notes";
125
123
  notesMarkdown: string;
@@ -130,10 +128,10 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
130
128
  byteLength: number;
131
129
  content?: unknown;
132
130
  };
133
- supersedesOutputId?: string | undefined;
134
- }, {
135
131
  outputId: string;
136
132
  outputChannel: "recap" | "artifact";
133
+ supersedesOutputId?: string | undefined;
134
+ }, {
137
135
  payload: {
138
136
  payloadKind: "notes";
139
137
  notesMarkdown: string;
@@ -144,5 +142,7 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
144
142
  byteLength: number;
145
143
  content?: unknown;
146
144
  };
145
+ outputId: string;
146
+ outputChannel: "recap" | "artifact";
147
147
  supersedesOutputId?: string | undefined;
148
148
  }>;
@@ -4,4 +4,5 @@ import type { WorkflowService } from '../../application/services/workflow-servic
4
4
  import type { ToolCallTimingRingBuffer } from '../../mcp/tool-call-timing.js';
5
5
  import type { TriggerRouter } from '../../trigger/trigger-router.js';
6
6
  import type { V2ToolContext } from '../../mcp/types.js';
7
- export declare function mountConsoleRoutes(app: Application, consoleService: ConsoleService, workflowService?: WorkflowService, timingRingBuffer?: ToolCallTimingRingBuffer, toolCallsPerfFile?: string, serverVersion?: string, v2ToolContext?: V2ToolContext, triggerRouter?: TriggerRouter): () => void;
7
+ import type { SteerRegistry } from '../../daemon/workflow-runner.js';
8
+ export declare function mountConsoleRoutes(app: Application, consoleService: ConsoleService, workflowService?: WorkflowService, timingRingBuffer?: ToolCallTimingRingBuffer, toolCallsPerfFile?: string, serverVersion?: string, v2ToolContext?: V2ToolContext, triggerRouter?: TriggerRouter, steerRegistry?: SteerRegistry): () => void;
@@ -40,10 +40,12 @@ exports.mountConsoleRoutes = mountConsoleRoutes;
40
40
  const express_1 = __importDefault(require("express"));
41
41
  const path_1 = __importDefault(require("path"));
42
42
  const fs_1 = __importDefault(require("fs"));
43
+ const os_1 = __importDefault(require("os"));
43
44
  const worktree_service_js_1 = require("./worktree-service.js");
44
45
  const workflow_js_1 = require("../../types/workflow.js");
45
46
  const dev_mode_js_1 = require("../../mcp/dev-mode.js");
46
47
  const workflow_runner_js_1 = require("../../daemon/workflow-runner.js");
48
+ const assert_never_js_1 = require("../../runtime/assert-never.js");
47
49
  const start_js_1 = require("../../mcp/handlers/v2-execution/start.js");
48
50
  const v2_token_ops_js_1 = require("../../mcp/handlers/v2-token-ops.js");
49
51
  function watchSessionsDir(sessionsDir, onChanged) {
@@ -89,7 +91,7 @@ function loadWorkflowTags() {
89
91
  return { version: 0, tags: [], workflows: {} };
90
92
  }
91
93
  }
92
- function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer, toolCallsPerfFile, serverVersion, v2ToolContext, triggerRouter) {
94
+ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer, toolCallsPerfFile, serverVersion, v2ToolContext, triggerRouter, steerRegistry) {
93
95
  const sseClients = new Set();
94
96
  let sseDebounceTimer = null;
95
97
  function broadcastChange() {
@@ -135,6 +137,183 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
135
137
  req.on('close', () => { sseClients.delete(res); });
136
138
  res.on('close', () => { sseClients.delete(res); });
137
139
  });
140
+ const daemonEventsDir = path_1.default.join(process.env['HOME'] ?? os_1.default.homedir(), '.workrail', 'events', 'daemon');
141
+ async function tailDaemonEvents(filePath, prevSize) {
142
+ try {
143
+ const stat = await fs_1.default.promises.stat(filePath);
144
+ if (stat.size <= prevSize)
145
+ return [];
146
+ const fd = await fs_1.default.promises.open(filePath, 'r');
147
+ const length = stat.size - prevSize;
148
+ const buf = Buffer.alloc(length);
149
+ try {
150
+ await fd.read(buf, 0, length, prevSize);
151
+ }
152
+ finally {
153
+ await fd.close();
154
+ }
155
+ const chunk = buf.toString('utf8');
156
+ return chunk
157
+ .split('\n')
158
+ .filter(Boolean)
159
+ .flatMap((line) => {
160
+ try {
161
+ return [JSON.parse(line)];
162
+ }
163
+ catch {
164
+ return [];
165
+ }
166
+ });
167
+ }
168
+ catch {
169
+ return [];
170
+ }
171
+ }
172
+ const SESSION_SSE_EVENT_KINDS = new Set([
173
+ 'tool_called',
174
+ 'tool_call_started',
175
+ 'tool_call_completed',
176
+ 'tool_call_failed',
177
+ 'tool_error',
178
+ 'step_advanced',
179
+ 'session_completed',
180
+ 'issue_reported',
181
+ 'agent_stuck',
182
+ 'llm_turn_started',
183
+ 'llm_turn_completed',
184
+ 'signal_emitted',
185
+ ]);
186
+ app.get('/api/v2/sessions/:sessionId/events', async (req, res) => {
187
+ const { sessionId } = req.params;
188
+ const sessionResult = await consoleService.getSessionDetail(sessionId);
189
+ if (sessionResult.isErr()) {
190
+ const status = sessionResult.error.code === 'SESSION_LOAD_FAILED' ? 404 : 500;
191
+ res.status(status).json({ success: false, error: sessionResult.error.message });
192
+ return;
193
+ }
194
+ const sessionDetail = sessionResult.value;
195
+ if (!sessionDetail || !sessionDetail.runs || sessionDetail.runs.length === 0) {
196
+ res.status(404).json({ success: false, error: `Session not found: ${sessionId}` });
197
+ return;
198
+ }
199
+ res.setHeader('Content-Type', 'text/event-stream');
200
+ res.setHeader('Cache-Control', 'no-cache');
201
+ res.setHeader('Connection', 'keep-alive');
202
+ res.setHeader('X-Accel-Buffering', 'no');
203
+ res.flushHeaders();
204
+ res.write(`data: ${JSON.stringify({ kind: 'connected', sessionId })}\n\n`);
205
+ let currentLogDate = new Date().toISOString().slice(0, 10);
206
+ let currentLogPath = path_1.default.join(daemonEventsDir, `${currentLogDate}.jsonl`);
207
+ let fileOffset = 0;
208
+ try {
209
+ const stat = await fs_1.default.promises.stat(currentLogPath);
210
+ fileOffset = stat.size;
211
+ }
212
+ catch {
213
+ }
214
+ let isClosed = false;
215
+ let isProcessing = false;
216
+ let watcher = null;
217
+ const cleanup = () => {
218
+ if (isClosed)
219
+ return;
220
+ isClosed = true;
221
+ try {
222
+ watcher?.close();
223
+ }
224
+ catch { }
225
+ try {
226
+ if (!res.writableEnded)
227
+ res.end();
228
+ }
229
+ catch { }
230
+ };
231
+ const processNewEvents = async () => {
232
+ if (isClosed || isProcessing)
233
+ return;
234
+ isProcessing = true;
235
+ const todayDate = new Date().toISOString().slice(0, 10);
236
+ if (todayDate !== currentLogDate) {
237
+ currentLogDate = todayDate;
238
+ currentLogPath = path_1.default.join(daemonEventsDir, `${currentLogDate}.jsonl`);
239
+ fileOffset = 0;
240
+ }
241
+ const newEvents = await tailDaemonEvents(currentLogPath, fileOffset);
242
+ for (const event of newEvents) {
243
+ if (isClosed)
244
+ break;
245
+ const kind = typeof event['kind'] === 'string' ? event['kind'] : null;
246
+ const evtSessionId = typeof event['workrailSessionId'] === 'string'
247
+ ? event['workrailSessionId']
248
+ : null;
249
+ if (!kind || !SESSION_SSE_EVENT_KINDS.has(kind))
250
+ continue;
251
+ if (evtSessionId !== sessionId)
252
+ continue;
253
+ try {
254
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
255
+ }
256
+ catch {
257
+ cleanup();
258
+ return;
259
+ }
260
+ if (kind === 'session_completed') {
261
+ cleanup();
262
+ return;
263
+ }
264
+ }
265
+ try {
266
+ const stat = await fs_1.default.promises.stat(currentLogPath);
267
+ fileOffset = stat.size;
268
+ }
269
+ catch {
270
+ fileOffset = 0;
271
+ }
272
+ isProcessing = false;
273
+ };
274
+ try {
275
+ fs_1.default.mkdirSync(daemonEventsDir, { recursive: true });
276
+ }
277
+ catch { }
278
+ try {
279
+ watcher = fs_1.default.watch(daemonEventsDir, { recursive: false }, (_eventType, filename) => {
280
+ if (filename !== null && filename.endsWith('.jsonl')) {
281
+ void processNewEvents();
282
+ }
283
+ });
284
+ watcher.on('error', cleanup);
285
+ }
286
+ catch {
287
+ }
288
+ const keepaliveInterval = setInterval(() => {
289
+ if (isClosed) {
290
+ clearInterval(keepaliveInterval);
291
+ return;
292
+ }
293
+ try {
294
+ res.write(': keepalive\n\n');
295
+ }
296
+ catch {
297
+ clearInterval(keepaliveInterval);
298
+ cleanup();
299
+ }
300
+ }, 30000);
301
+ const maxConnectionTimeout = setTimeout(() => {
302
+ clearInterval(keepaliveInterval);
303
+ cleanup();
304
+ }, 4 * 60 * 60 * 1000);
305
+ req.on('close', () => {
306
+ clearInterval(keepaliveInterval);
307
+ clearTimeout(maxConnectionTimeout);
308
+ cleanup();
309
+ });
310
+ res.on('close', () => {
311
+ clearInterval(keepaliveInterval);
312
+ clearTimeout(maxConnectionTimeout);
313
+ cleanup();
314
+ });
315
+ void processNewEvents();
316
+ });
138
317
  const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
139
318
  const PERF_FILE_READ_LIMIT_BYTES = 5 * 1024 * 1024;
140
319
  async function readDiskEntries(perfFile) {
@@ -412,18 +591,21 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
412
591
  triggerRouter.dispatch(trigger);
413
592
  }
414
593
  else {
415
- void (0, workflow_runner_js_1.runWorkflow)(trigger, v2ToolContext, apiKey ?? '').then((result) => {
594
+ void (0, workflow_runner_js_1.runWorkflow)(trigger, v2ToolContext, apiKey ?? '', undefined, undefined, steerRegistry).then((result) => {
416
595
  if (result._tag === 'success') {
417
596
  console.log(`[ConsoleRoutes] Auto dispatch completed: workflowId=${workflowId} stopReason=${result.stopReason}`);
418
597
  }
598
+ else if (result._tag === 'delivery_failed') {
599
+ console.log(`[ConsoleRoutes] Auto dispatch delivery failed: workflowId=${workflowId}`);
600
+ }
419
601
  else if (result._tag === 'timeout') {
420
602
  console.log(`[ConsoleRoutes] Auto dispatch timed out: workflowId=${workflowId}`);
421
603
  }
422
- else if (result._tag === 'delivery_failed') {
423
- console.log(`[ConsoleRoutes] Auto dispatch delivery failed: workflowId=${workflowId}`);
604
+ else if (result._tag === 'error') {
605
+ console.log(`[ConsoleRoutes] Auto dispatch failed: workflowId=${workflowId} error=${result.message}`);
424
606
  }
425
607
  else {
426
- console.log(`[ConsoleRoutes] Auto dispatch failed: workflowId=${workflowId} error=${result.message}`);
608
+ (0, assert_never_js_1.assertNever)(result);
427
609
  }
428
610
  });
429
611
  }
@@ -443,6 +625,26 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
443
625
  }));
444
626
  res.json({ success: true, data: { triggers } });
445
627
  });
628
+ app.post('/api/v2/sessions/:sessionId/steer', express_1.default.json(), (req, res) => {
629
+ if (!steerRegistry) {
630
+ res.status(503).json({ success: false, error: 'Steer not available (not a daemon context).' });
631
+ return;
632
+ }
633
+ const { sessionId } = req.params;
634
+ const body = req.body;
635
+ const text = typeof body.text === 'string' ? body.text.trim() : '';
636
+ if (!text) {
637
+ res.status(400).json({ success: false, error: 'text is required and must be a non-empty string.' });
638
+ return;
639
+ }
640
+ const callback = steerRegistry.get(sessionId);
641
+ if (!callback) {
642
+ res.status(404).json({ success: false, error: 'Session not found or not a daemon session.' });
643
+ return;
644
+ }
645
+ callback(text);
646
+ res.json({ success: true });
647
+ });
446
648
  const consoleDist = resolveConsoleDist();
447
649
  if (consoleDist) {
448
650
  app.use('/console', express_1.default.static(consoleDist, {
@@ -585,6 +585,17 @@ function extractRepoRoot(events) {
585
585
  }
586
586
  return workspacePathFallback;
587
587
  }
588
+ function extractParentSessionId(events) {
589
+ for (const e of events) {
590
+ if (e.kind === constants_js_1.EVENT_KIND.SESSION_CREATED) {
591
+ const parentId = e.data.parentSessionId;
592
+ if (typeof parentId === 'string' && parentId.length > 0)
593
+ return parentId;
594
+ return null;
595
+ }
596
+ }
597
+ return null;
598
+ }
588
599
  function truncateTitle(text, maxLen = 120) {
589
600
  if (text.length <= maxLen)
590
601
  return text;
@@ -612,6 +623,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
612
623
  const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
613
624
  const gitBranch = extractGitBranch(events);
614
625
  const repoRoot = extractRepoRoot(events);
626
+ const parentSessionId = extractParentSessionId(events);
615
627
  const isAutonomous = (() => {
616
628
  if (!sortedEventsRes.isOk())
617
629
  return false;
@@ -643,6 +655,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
643
655
  lastModifiedMs,
644
656
  isAutonomous,
645
657
  isLive,
658
+ parentSessionId,
646
659
  };
647
660
  }
648
661
  const workflow = run.workflow;
@@ -688,6 +701,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
688
701
  lastModifiedMs,
689
702
  isAutonomous,
690
703
  isLive,
704
+ parentSessionId,
691
705
  };
692
706
  }
693
707
  function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, workflowNames, skippedStepsMap = {}) {
@@ -20,6 +20,7 @@ export interface ConsoleSessionSummary {
20
20
  readonly lastModifiedMs: number;
21
21
  readonly isAutonomous: boolean;
22
22
  readonly isLive: boolean;
23
+ readonly parentSessionId: string | null;
23
24
  }
24
25
  export interface ConsoleSessionListResponse {
25
26
  readonly sessions: readonly ConsoleSessionSummary[];
package/docs/authoring.md CHANGED
@@ -42,7 +42,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
42
42
  **Source refs**
43
43
  - `spec/workflow.schema.json` (schema) — Legal structure and supported fields.
44
44
  - `src/application/services/validation-engine.ts` (runtime) — Validator-enforced authoring rules.
45
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Current modern example.
45
+ - `workflows/coding-task-workflow-agentic.json` (example) — Current modern example.
46
46
 
47
47
  ### validate-early-and-often
48
48
  - **Level**: required
@@ -141,7 +141,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
141
141
  - Part A / Part B / Rules: ... when the structure adds ceremony rather than clarity
142
142
 
143
143
  **Example refs**
144
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — See the sharpened user-voiced prompts in the current lean coding workflow.
144
+ - `workflows/coding-task-workflow-agentic.json` — See the sharpened user-voiced prompts in the current lean coding workflow.
145
145
 
146
146
  ### protocol-footers-stay-explicit
147
147
  - **Level**: required
@@ -160,10 +160,10 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
160
160
  - Replacing exact capture requirements with vague summary prose
161
161
 
162
162
  **Example refs**
163
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Uses compact Capture footers and explicit loop-control wording.
163
+ - `workflows/coding-task-workflow-agentic.json` — Uses compact Capture footers and explicit loop-control wording.
164
164
 
165
165
  **Source refs**
166
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Uses explicit capture footers and shape-preserving loop outputs.
166
+ - `workflows/coding-task-workflow-agentic.json` (example) — Uses explicit capture footers and shape-preserving loop outputs.
167
167
 
168
168
 
169
169
  ## Prompt composition
@@ -185,11 +185,11 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
185
185
  - Encoding runtime logic in prose when promptFragments or templates are the right mechanism
186
186
 
187
187
  **Example refs**
188
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Uses prompt fragments and context templates to keep prompts slimmer at render time.
188
+ - `workflows/coding-task-workflow-agentic.json` — Uses prompt fragments and context templates to keep prompts slimmer at render time.
189
189
 
190
190
  **Source refs**
191
191
  - `docs/authoring.md` (documentation) — Documents context templates and prompt fragments.
192
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Uses prompt fragments to slim mode-specific prompt branches.
192
+ - `workflows/coding-task-workflow-agentic.json` (example) — Uses prompt fragments to slim mode-specific prompt branches.
193
193
 
194
194
  ### templates-are-for-simple-substitution
195
195
  - **Level**: recommended
@@ -405,11 +405,11 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
405
405
  - Prompt text says to stop, but the example output only permits continue
406
406
 
407
407
  **Example refs**
408
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Current loop decision steps show shape-only output examples.
408
+ - `workflows/coding-task-workflow-agentic.json` — Current loop decision steps show shape-only output examples.
409
409
 
410
410
  **Source refs**
411
411
  - `scripts/validate-workflows-registry.ts` (validator) — Registry validation should preserve semantically correct discoverable workflows.
412
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Current loop decision prompts show shape-only output examples.
412
+ - `workflows/coding-task-workflow-agentic.json` (example) — Current loop decision prompts show shape-only output examples.
413
413
 
414
414
  ### loops-need-real-exit-rules
415
415
  - **Level**: required
@@ -445,7 +445,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
445
445
  - `contextAuditNeeded = true|false` without an explicit rubric
446
446
 
447
447
  **Example refs**
448
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Phase 0 uses a context-clarity rubric instead of a vibes-only confidence flag.
448
+ - `workflows/coding-task-workflow-agentic.json` — Phase 0 uses a context-clarity rubric instead of a vibes-only confidence flag.
449
449
 
450
450
 
451
451
  ## Confirmation discipline
@@ -466,7 +466,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
466
466
  - Using requireConfirmation as a substitute for clear loop or rigor policy
467
467
 
468
468
  **Source refs**
469
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Uses confirmation for real review barriers like MultiPR checkpoints.
469
+ - `workflows/coding-task-workflow-agentic.json` (example) — Uses confirmation for real review barriers like MultiPR checkpoints.
470
470
 
471
471
 
472
472
  ## Assessment gates
@@ -544,7 +544,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
544
544
  - Treating named builder or researcher roles as alternate owners
545
545
 
546
546
  **Source refs**
547
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Delegation checkpoints keep the main agent as the synthesizer and decision-maker.
547
+ - `workflows/coding-task-workflow-agentic.json` (example) — Delegation checkpoints keep the main agent as the synthesizer and decision-maker.
548
548
 
549
549
  ### batched-checkpoints-over-ad-hoc-optionality
550
550
  - **Level**: recommended
@@ -563,7 +563,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
563
563
  - Optional challenge wording at high-value decision points
564
564
 
565
565
  **Example refs**
566
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Uses explicit challenge, audit, and verification barriers.
566
+ - `workflows/coding-task-workflow-agentic.json` — Uses explicit challenge, audit, and verification barriers.
567
567
 
568
568
 
569
569
  ## Subagent synthesis and claim adoption
@@ -601,10 +601,10 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
601
601
  - Using delegated findings as blockers or green lights without verification
602
602
 
603
603
  **Example refs**
604
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Major synthesis checkpoints use Confirmed / Plausible / Rejected for decision-driving findings.
604
+ - `workflows/coding-task-workflow-agentic.json` — Major synthesis checkpoints use Confirmed / Plausible / Rejected for decision-driving findings.
605
605
 
606
606
  **Source refs**
607
- - `workflows/coding-task-workflow-agentic.lean.v2.json` (example) — Major synthesis checkpoints use Confirmed / Plausible / Rejected for adopted claims.
607
+ - `workflows/coding-task-workflow-agentic.json` (example) — Major synthesis checkpoints use Confirmed / Plausible / Rejected for adopted claims.
608
608
 
609
609
 
610
610
  ## Discouraged legacy patterns
@@ -670,7 +670,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
670
670
  - Keeping a canonical example ref after the workflow has drifted into a legacy style
671
671
 
672
672
  **Example refs**
673
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Current example of modern prompt composition, delegation barriers, and loop semantics.
673
+ - `workflows/coding-task-workflow-agentic.json` — Current example of modern prompt composition, delegation barriers, and loop semantics.
674
674
 
675
675
 
676
676
  ## Validation
@@ -728,7 +728,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
728
728
  - Verification steps check one artifact while planning updates a different one
729
729
 
730
730
  **Example refs**
731
- - `workflows/coding-task-workflow-agentic.lean.v2.json` — Uses explicit spec vs implementation-plan ownership.
731
+ - `workflows/coding-task-workflow-agentic.json` — Uses explicit spec vs implementation-plan ownership.
732
732
 
733
733
 
734
734
  ## Planned guidance
@@ -0,0 +1,155 @@
1
+ # Design Candidates: Coordinator Artifact Protocol
2
+
3
+ **Status:** Candidate analysis complete
4
+ **Date:** 2026-04-18
5
+ **Task:** Implement wr.review_verdict schema, fix onComplete callback, update mr-review workflow to emit it, update coordinator to read artifacts before keyword-scanning
6
+
7
+ ---
8
+
9
+ ## Problem Understanding
10
+
11
+ ### Core Tensions
12
+
13
+ **T1: Breaking interface vs. backward compatibility**
14
+ `CoordinatorDeps.getAgentResult` returns `Promise<string | null>` today. Changing it to `Promise<{ recapMarkdown: string | null; artifacts: readonly unknown[] }>` is a compile-time breaking change. All call sites (2 in coordinator, 2 in test fakes, 1 real implementation) must change simultaneously. TypeScript catches this at build, so risk is low -- but the change must be complete.
15
+
16
+ **T2: N+1 HTTP calls vs. tip-node-only simplicity**
17
+ ALL-node aggregation requires walking `runs[0].nodes` and fetching each node's detail individually. For a 6-phase workflow, that's 6 HTTP calls to localhost per session. The simple approach (tip node only) would miss a verdict artifact from any non-final step.
18
+
19
+ **T3: `required: false` vs. engine enforcement**
20
+ `outputContract` with `required: false` means the engine won't block if the artifact is absent. This is the correct transition strategy but means the coordinator must maintain two code paths (artifact + keyword-scan fallback) until the graduation criterion (10+ consecutive sessions with 0 fallback warnings) is met.
21
+
22
+ **T4: Schema strictness vs. forward compatibility**
23
+ `.strict()` rejects unknown fields (forward-incompatible). `.strip()` strips them silently (forward-compatible). The task spec says `.strict()`, which matches the `loop-control.ts` precedent. The design doc recommends `.strip()` for forward-compat. **Task spec wins** -- use `.strict()` to be consistent with existing schema patterns.
24
+
25
+ ### Likely Seam
26
+
27
+ `CoordinatorDeps.getAgentResult` is the real boundary. It is already the I/O abstraction layer where the coordinator interacts with sessions. Changing the return type here forces all consumers to acknowledge the new shape without touching coordinator routing logic.
28
+
29
+ ### What Makes This Hard
30
+
31
+ 1. **Three separate `onComplete` sites:** `makeCompleteStepTool` (line 1249), `makeContinueWorkflowTool` (line 1046), and the closure definition (line 2096). TypeScript will catch signature mismatches on the closure but not at the two call sites if the closure's new parameter is optional.
32
+ 2. **Exhaustiveness in the switch:** `artifact-contract-validator.ts` switch currently handles only `LOOP_CONTROL_CONTRACT_REF`. Adding `'wr.contracts.review_verdict'` to `ARTIFACT_CONTRACT_REFS` without adding a switch case causes `validateArtifactContract()` to hit the default `UNKNOWN_CONTRACT_REF` error for any step declaring this contract.
33
+ 3. **`source?` field on ReviewFindings:** Adding `source` as required breaks 4 existing test literals. Making it optional (`source?`) is a minor type weakness but preserves backward compat.
34
+
35
+ ---
36
+
37
+ ## Philosophy Constraints
38
+
39
+ From CLAUDE.md:
40
+ - **Make illegal states unrepresentable:** `verdict: 'clean'|'minor'|'blocking'` not `string`. `source: 'artifact'|'keyword_scan'` not `string`.
41
+ - **Validate at boundaries:** Zod parse at coordinator read time + engine validation at advance time.
42
+ - **Errors are data:** `readVerdictArtifact()` returns `ReviewFindings | null`, not throws.
43
+ - **Functional/declarative:** `readVerdictArtifact()` is a pure function, composable with `parseFindingsFromNotes()`.
44
+ - **Prefer fakes over mocks:** The `makeFakeDeps()` pattern in tests is the established style.
45
+
46
+ **Conflict:** `required: false` during transition temporarily violates 'make illegal states unrepresentable' at the coordinator level. Accepted per design doc -- the fallback is explicit and time-boxed.
47
+
48
+ ---
49
+
50
+ ## Impact Surface
51
+
52
+ Files that must change:
53
+ - `src/v2/durable-core/schemas/artifacts/review-verdict.ts` (new)
54
+ - `src/v2/durable-core/schemas/artifacts/index.ts` (ARTIFACT_CONTRACT_REFS)
55
+ - `src/v2/durable-core/domain/artifact-contract-validator.ts` (switch case)
56
+ - `src/daemon/workflow-runner.ts` (onComplete signature, WorkflowRunSuccess, final return)
57
+ - `src/cli-worktrain.ts` (getAgentResult implementation + return type)
58
+ - `src/coordinators/pr-review.ts` (CoordinatorDeps, ReviewFindings, readVerdictArtifact, call sites)
59
+ - `workflows/mr-review-workflow.agentic.v2.json` (phase-6 outputContract + prompt)
60
+ - `tests/unit/coordinator-pr-review.test.ts` (new tests + updated fakes)
61
+
62
+ Must remain consistent:
63
+ - `ConsoleNodeDetail.artifacts` -- no change needed, already returns artifacts
64
+ - `projectArtifactsV2()` -- no change needed, already projects artifacts
65
+ - `delivery-action.ts` -- reads `lastStepNotes`, not artifacts; no change needed
66
+ - `makeSpawnAgentTool()` -- returns `{ notes: string }` only; `lastStepArtifacts` gap acknowledged, post-MVP
67
+
68
+ ---
69
+
70
+ ## Candidates
71
+
72
+ ### Candidate A: Exact task spec implementation (RECOMMENDED)
73
+
74
+ **Summary:** Implement all three changes exactly as specified: fix `onComplete` to forward `params.artifacts`, add `wr.review_verdict` schema with `.strict()`, update `getAgentResult` to aggregate ALL-node artifacts, add `readVerdictArtifact()` pure function with keyword-scan fallback.
75
+
76
+ **Tensions resolved:**
77
+ - T1: TypeScript compile-time catch ensures completeness
78
+ - T3: `required: false` + keyword-scan fallback avoids session blocking
79
+
80
+ **Tensions accepted:**
81
+ - T2: N+1 calls (accepted -- localhost, negligible latency)
82
+ - T4: `.strict()` over `.strip()` (follows existing precedent)
83
+
84
+ **Boundary:** `CoordinatorDeps.getAgentResult` return type change. Best-fit because it is already the established abstraction boundary for coordinator-to-session I/O. All consumers must acknowledge the change at this single point.
85
+
86
+ **Failure mode:** Missing the `makeContinueWorkflowTool` `onComplete` call site (line 1046) when updating `makeCompleteStepTool` (line 1249). Both tools call `onComplete` but are in separate functions. TypeScript will not catch this if `artifacts?` is optional in the signature -- the closure will be called with `undefined` for `artifacts` from `continue_workflow`, and `lastStepArtifacts` will be silently empty.
87
+
88
+ **Repo-pattern relationship:** Follows `loop-control.ts` schema pattern exactly. Follows `WorkflowRunSuccess.lastStepNotes` conditional spread pattern. Follows `makeFakeDeps()` fake deps testing pattern. No new patterns introduced.
89
+
90
+ **Gains:**
91
+ - Coordinator reads typed data for sessions that emit the artifact
92
+ - Additive: all existing sessions continue to work via fallback
93
+ - Zero new infrastructure: 7 file changes + 1 new file
94
+ - Artifact visible in console (`hasArtifacts: true` on phase-6 node)
95
+ - Observability: `source: 'artifact'|'keyword_scan'` + logging enables emission rate tracking
96
+
97
+ **Losses:**
98
+ - N+1 HTTP calls per session for artifact aggregation
99
+ - Two coordinator code paths until graduation
100
+
101
+ **Scope:** Best-fit. Minimal delta, highest backward compatibility, clear graduation path.
102
+
103
+ **Philosophy:** Honors validate-at-boundaries, functional/declarative, prefer-fakes, exhaustiveness (closed enum `source`). Minor tension: `source?` optional field vs. type-safety-first. Temporary conflict with 'make illegal states unrepresentable' (accepted).
104
+
105
+ ---
106
+
107
+ ### Candidate B: Tip-node only (simpler, misses design intent)
108
+
109
+ **Summary:** Only read tip node's artifacts -- matching the existing `preferredTipNodeId` pattern in `getAgentResult` today. Avoids N+1 calls.
110
+
111
+ **Tensions resolved:**
112
+ - T2: 1 HTTP call vs. N+1
113
+
114
+ **Tensions accepted:**
115
+ - Violates task spec 'CRITICAL: must aggregate artifacts across ALL session nodes'
116
+ - If a verdict artifact is on step N-1 and the workflow gains a post-synthesis confirmation step N, coordinator silently gets zero artifacts
117
+
118
+ **Failure mode:** Silent data loss when artifact is on a non-final node. This is the ORANGE-1 constraint from the design doc.
119
+
120
+ **Scope:** Too narrow -- explicitly contradicts task requirement.
121
+
122
+ **Why rejected:** The task spec uses 'CRITICAL' emphasis for ALL-node aggregation. Disqualified.
123
+
124
+ ---
125
+
126
+ ## Comparison and Recommendation
127
+
128
+ **Recommendation: Candidate A.** No contest -- Candidate B is disqualified by the task spec.
129
+
130
+ | Criterion | A | B |
131
+ |-----------|---|---|
132
+ | ALL-node aggregation (task spec) | Correct | WRONG |
133
+ | N+1 calls | Accepted | Avoided |
134
+ | Backward compat | Full | Same |
135
+ | Schema precedent | Follows exactly | N/A |
136
+ | Philosophy fit | Best | N/A |
137
+
138
+ ---
139
+
140
+ ## Self-Critique
141
+
142
+ **Strongest counter-argument:** N+1 calls add latency. For a 6-step session, that's 6 additional HTTP calls. Acceptable on localhost (~50-100ms) but could be optimized with a `/api/v2/sessions/:id/artifacts` aggregation endpoint (Candidate C from the design doc). Evidence required: a second coordinator that needs this, or performance data showing N+1 calls are a problem.
143
+
144
+ **Narrower option that almost works:** Tip-node only. Loses for the explicit task-spec reason.
145
+
146
+ **Broader option:** Add `/api/v2/sessions/:id/artifacts` server-side endpoint. Right long-term direction, premature now.
147
+
148
+ **Assumption that would invalidate:** If `runs[0].nodes` in the session detail response returns objects without `nodeId` fields. Confirmed from `ConsoleDagNode` type that `nodeId: string` is always present.
149
+
150
+ ---
151
+
152
+ ## Open Questions for the Main Agent
153
+
154
+ 1. Should `source?` be optional or required on `ReviewFindings`? Optional breaks fewer existing tests but weakens the type. The 4 existing `ReviewFindings` literals in tests would need `source` added if required.
155
+ 2. Should `readVerdictArtifact()` log a divergence warning when both artifact severity and keyword-scan severity are available but disagree? The design doc recommends this (ORANGE finding). Adds ~10 LOC but improves observability.