@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.
- package/dist/cli/commands/init.js +0 -3
- package/dist/cli-worktrain.js +58 -26
- package/dist/cli.js +0 -18
- package/dist/config/app-config.d.ts +0 -16
- package/dist/config/app-config.js +0 -14
- package/dist/config/config-file.js +0 -3
- package/dist/console-ui/assets/index-CQt4UhPB.js +28 -0
- package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
- package/dist/console-ui/index.html +2 -2
- package/dist/coordinators/pr-review.d.ts +23 -1
- package/dist/coordinators/pr-review.js +224 -5
- package/dist/daemon/daemon-events.d.ts +9 -1
- package/dist/daemon/soul-template.d.ts +2 -2
- package/dist/daemon/soul-template.js +11 -1
- package/dist/daemon/workflow-runner.d.ts +17 -3
- package/dist/daemon/workflow-runner.js +401 -28
- package/dist/di/container.js +1 -25
- package/dist/di/tokens.d.ts +0 -3
- package/dist/di/tokens.js +0 -3
- package/dist/engine/engine-factory.js +0 -1
- package/dist/infrastructure/console-defaults.d.ts +1 -0
- package/dist/infrastructure/console-defaults.js +4 -0
- package/dist/infrastructure/session/index.d.ts +0 -1
- package/dist/infrastructure/session/index.js +1 -3
- package/dist/manifest.json +124 -124
- package/dist/mcp/handlers/session.d.ts +1 -0
- package/dist/mcp/handlers/session.js +61 -13
- package/dist/mcp/output-schemas.d.ts +10 -10
- package/dist/mcp/server.js +1 -18
- package/dist/mcp/tools.d.ts +12 -12
- package/dist/mcp/transports/http-entry.js +0 -2
- package/dist/mcp/transports/stdio-entry.js +1 -2
- package/dist/mcp/types.d.ts +0 -2
- package/dist/trigger/daemon-console.d.ts +2 -0
- package/dist/trigger/daemon-console.js +1 -1
- package/dist/trigger/trigger-listener.d.ts +2 -0
- package/dist/trigger/trigger-listener.js +3 -1
- package/dist/trigger/trigger-router.d.ts +4 -3
- package/dist/trigger/trigger-router.js +13 -5
- package/dist/trigger/trigger-store.js +17 -4
- 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.d.ts +2 -1
- package/dist/v2/usecases/console-routes.js +207 -5
- package/dist/v2/usecases/console-service.js +14 -0
- package/dist/v2/usecases/console-types.d.ts +1 -0
- package/docs/authoring.md +16 -16
- package/docs/design/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/design/coordinator-message-queue-drain-plan.md +241 -0
- package/docs/design/coordinator-message-queue-drain-review.md +120 -0
- package/docs/design/coordinator-message-queue-drain.md +289 -0
- package/docs/design/shaping-workflow-external-research.md +119 -0
- package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
- package/docs/discovery/late-bound-goals-review.md +82 -0
- package/docs/discovery/late-bound-goals.md +118 -0
- package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
- package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
- package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
- package/docs/ideas/backlog.md +447 -97
- package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
- package/docs/ideas/design-candidates-session-tree-view.md +196 -0
- package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
- package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
- package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
- package/package.json +2 -1
- package/spec/authoring-spec.json +16 -16
- package/spec/shape.schema.json +178 -0
- package/spec/workflow-tags.json +232 -47
- package/workflows/coding-task-workflow-agentic.json +491 -480
- package/workflows/mr-review-workflow.agentic.v2.json +5 -1
- package/workflows/wr.shaping.json +182 -0
- package/dist/console-ui/assets/index-3oXZ_A9m.js +0 -28
- package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
- package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
- package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
- package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
- package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
- package/dist/infrastructure/session/HttpServer.d.ts +0 -60
- package/dist/infrastructure/session/HttpServer.js +0 -912
- package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
- package/workflows/coding-task-workflow-agentic.v2.json +0 -324
|
@@ -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
|
-
|
|
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 === '
|
|
423
|
-
console.log(`[ConsoleRoutes] Auto dispatch
|
|
604
|
+
else if (result._tag === 'error') {
|
|
605
|
+
console.log(`[ConsoleRoutes] Auto dispatch failed: workflowId=${workflowId} error=${result.message}`);
|
|
424
606
|
}
|
|
425
607
|
else {
|
|
426
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|