@deksden-com/dd-flow-cli 0.9.0-beta.0 → 0.9.0-beta.10
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/CHANGELOG.md +77 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +3 -3
- package/dist/cli/run-cli.js +127 -8
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +1 -1
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/cleanup.js +18 -8
- package/dist/services/code-checks.js +194 -44
- package/dist/services/engines.js +4 -4
- package/dist/services/eval-snapshots.js +10 -5
- package/dist/services/harness-config.js +66 -0
- package/dist/services/hooks.js +25 -22
- package/dist/services/lanes.js +1 -0
- package/dist/services/managed-processes.js +169 -0
- package/dist/services/merge-server.js +8 -2
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/prompts.js +4 -2
- package/dist/services/run-engine-bindings.js +19 -61
- package/dist/services/run-projection.js +10 -8
- package/dist/services/runs.js +71 -9
- package/dist/services/schema-validation.js +11 -11
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +26 -11
- package/dist/services/stage-lifecycle.js +15 -8
- package/dist/services/stage-pause.js +35 -20
- package/dist/services/usage.js +81 -57
- package/dist/services/vnext-code-review.js +82 -41
- package/dist/services/vnext-code.js +98 -34
- package/dist/services/vnext-fanout.js +5 -12
- package/dist/services/vnext-merge.js +144 -65
- package/dist/services/vnext-plan-review.js +50 -35
- package/dist/services/vnext-plan.js +69 -21
- package/dist/services/vnext-protocolize.js +6 -6
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +150 -40
- package/dist/storage/database.js +128 -2
- package/package.json +1 -1
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
package/dist/services/usage.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import { parseJsonObject } from "../shared/json.js";
|
|
4
4
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
5
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
6
|
+
import { publicSessionIdentity } from "./session-identity.js";
|
|
6
7
|
export function ingestZcodeUsage(context, input) {
|
|
7
8
|
return ingestHarnessUsage(context, input, { harness: "zcode-acp", label: "ZCode", idPrefix: "ZUSG", sourceKind: "zcode_session_usage_v1" });
|
|
8
9
|
}
|
|
@@ -55,11 +56,11 @@ function ingestHarnessUsage(context, input, harness) {
|
|
|
55
56
|
values.input, values.cacheRead, values.cacheWrite, values.output, values.reasoning, values.requests, values.errors,
|
|
56
57
|
values.toolCalls, values.toolFailures, values.toolByName ? JSON.stringify(values.toolByName) : null, harness.sourceKind,
|
|
57
58
|
stringValue(payload.usage_scope) ?? "physical_session", stringValue(payload.completeness) ?? "complete", context.now()]);
|
|
58
|
-
return { ok: true, observed: inserted.changes === 1, duplicate: inserted.changes === 0, snapshot_id: id,
|
|
59
|
+
return { ok: true, observed: inserted.changes === 1, duplicate: inserted.changes === 0, snapshot_id: id, session: publicSessionIdentity({ harness: harness.harness, provider_session_id: providerSessionId, session_id: providerSessionId }) };
|
|
59
60
|
}
|
|
60
61
|
export function checkpointSessionUsage(context, session, input) {
|
|
61
62
|
const observedAt = context.now();
|
|
62
|
-
const result = readCodexTranscript(session.transcript_path, session
|
|
63
|
+
const result = readCodexTranscript(session.transcript_path, nativeSessionId(session));
|
|
63
64
|
const source = sourceFacts(session.transcript_path);
|
|
64
65
|
const snapshot = {
|
|
65
66
|
id: `USG-${crypto.randomUUID()}`,
|
|
@@ -114,7 +115,7 @@ export function checkpointSessionUsage(context, session, input) {
|
|
|
114
115
|
return snapshotForOutput(snapshot);
|
|
115
116
|
}
|
|
116
117
|
export function checkpointRunUsage(context, input) {
|
|
117
|
-
const sessions = context.db.all(`SELECT session_id, project_id, run_id, transcript_path, parent_session_id, role, aspect_id, plan_item_id, session_kind
|
|
118
|
+
const sessions = context.db.all(`SELECT session_id, project_id, run_id, transcript_path, provider_session_id, parent_session_id, role, aspect_id, plan_item_id, session_kind
|
|
118
119
|
FROM sessions WHERE project_id = ? AND run_id = ?`, [input.projectId, input.runId]);
|
|
119
120
|
return sessions.map((session) => checkpointSessionUsage(context, session, input));
|
|
120
121
|
}
|
|
@@ -131,11 +132,12 @@ export function storedUsageForRun(context, input) {
|
|
|
131
132
|
*/
|
|
132
133
|
export function recalculateRunUsage(context, input) {
|
|
133
134
|
const observedAt = context.now();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// Provider completion, not the controller's later lifecycle write, is the
|
|
136
|
+
// accounting boundary. A final refresh must therefore see tokens emitted
|
|
137
|
+
// after `work finish` but before the agent's actual terminal event.
|
|
138
|
+
const accountingAt = observedAt;
|
|
137
139
|
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.session_id, ws.created_at, ws.completed_at,
|
|
138
|
-
w.project_id, s.harness, s.transcript_path, s.parent_session_id, s.agent_id, s.provider_session_id
|
|
140
|
+
w.project_id, s.harness, s.transcript_path, s.parent_session_id, s.provider_parent_session_id, s.agent_id, s.provider_session_id
|
|
139
141
|
FROM work_sessions ws
|
|
140
142
|
JOIN works w ON w.work_id = ws.work_id
|
|
141
143
|
LEFT JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
|
|
@@ -153,31 +155,34 @@ export function recalculateRunUsage(context, input) {
|
|
|
153
155
|
const settledSessions = settledRunSessions(context, links, observedAt);
|
|
154
156
|
const unsettledSessions = [...new Set(links.map((link) => link.session_id))].filter((sessionId) => !settledSessions.some((session) => session.session_id === sessionId));
|
|
155
157
|
const usageFinal = final && unsettledSessions.length === 0;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
context.db.
|
|
159
|
-
|
|
160
|
-
context.db.run(
|
|
158
|
+
const persistRunProjection = !input.sessionId && !input.stage;
|
|
159
|
+
if (persistRunProjection) {
|
|
160
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
161
|
+
try {
|
|
162
|
+
context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
context.db.run(`INSERT INTO usage (id, project_id, run_id, session_id, checkpoint, stage, stage_attempt, observed_at, total_tokens, input_tokens, cached_input_tokens, cache_read_input_tokens, cache_write_input_tokens, uncached_input_tokens, output_tokens, reasoning_output_tokens, source_kind, source_locator, source_sha256, source_size_bytes, source_mtime_ms, token_event_at, turn_id, turn_attribution, parser_version, extraction_status, diagnostic_code, created_at)
|
|
161
165
|
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 'windowed_work_session', 1, ?, ?, ?)`, [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
`USG-${crypto.randomUUID()}`, input.projectId, input.runId, row.session_id, usageFinal ? "final" : "provisional", input.stage?.name ?? null, row.observed_at,
|
|
167
|
+
row.tokens?.total_tokens ?? null, row.tokens?.input_tokens ?? null, row.tokens?.cache_read_input_tokens ?? null,
|
|
168
|
+
row.tokens?.cache_read_input_tokens ?? null, row.tokens?.cache_write_input_tokens ?? null, row.tokens?.uncached_input_tokens ?? null,
|
|
169
|
+
row.tokens?.output_tokens ?? null, row.tokens?.reasoning_output_tokens ?? null, row.source_kind, row.source.locator, row.source.sha256, row.source.size, row.source.mtime,
|
|
170
|
+
row.status, row.diagnostic, observedAt
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
for (const session of settledSessions) {
|
|
174
|
+
context.db.run(`UPDATE sessions
|
|
171
175
|
SET status = 'stopped', stop_reason = 'provider_turn_complete', updated_at = ?, stopped_at = ?
|
|
172
176
|
WHERE project_id = ? AND run_id = ? AND session_id = ?
|
|
173
177
|
AND status = 'idle'
|
|
174
178
|
AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')`, [observedAt, session.completed_at, input.projectId, input.runId, session.session_id, session.session_id]);
|
|
179
|
+
}
|
|
180
|
+
context.db.exec("COMMIT");
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
context.db.exec("ROLLBACK");
|
|
184
|
+
throw error;
|
|
175
185
|
}
|
|
176
|
-
context.db.exec("COMMIT");
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
context.db.exec("ROLLBACK");
|
|
180
|
-
throw error;
|
|
181
186
|
}
|
|
182
187
|
return {
|
|
183
188
|
ok: true,
|
|
@@ -186,16 +191,28 @@ export function recalculateRunUsage(context, input) {
|
|
|
186
191
|
status: usageFinal ? "final" : "provisional",
|
|
187
192
|
scope: input.stage ? { kind: "stage", stage: input.stage.name, started_at: input.stage.startedAt, completed_at: input.stage.completedAt } : { kind: "run" },
|
|
188
193
|
observed_at: observedAt,
|
|
194
|
+
persisted: persistRunProjection,
|
|
189
195
|
totals: outputUsageTotals(totals),
|
|
190
|
-
sessions: rows,
|
|
196
|
+
sessions: publicUsageRows(context, input.projectId, rows),
|
|
191
197
|
session_reconciliation: {
|
|
192
|
-
stopped: settledSessions.map((session) => session.session_id),
|
|
193
|
-
unsettled: unsettledSessions
|
|
198
|
+
stopped: publicSessionIdentities(context, input.projectId, settledSessions.map((session) => session.session_id)),
|
|
199
|
+
unsettled: publicSessionIdentities(context, input.projectId, unsettledSessions)
|
|
194
200
|
},
|
|
195
201
|
tool_calls: toolCallsForLinks(context, links, accountingAt),
|
|
196
202
|
coverage: rows.reduce((coverage, row) => ({ ...coverage, [row.status]: (coverage[row.status] ?? 0) + 1 }), {})
|
|
197
203
|
};
|
|
198
204
|
}
|
|
205
|
+
function publicUsageRows(context, projectId, rows) {
|
|
206
|
+
const identityFor = (storageId) => {
|
|
207
|
+
const row = context.db.get("SELECT harness, provider_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [projectId, storageId]);
|
|
208
|
+
return row ? publicSessionIdentity({ harness: row.harness, provider_session_id: row.provider_session_id, session_id: storageId }) : null;
|
|
209
|
+
};
|
|
210
|
+
return rows.map(({ session_id, provider_session_id: _providerSessionId, parent_session_id, ...row }) => ({
|
|
211
|
+
...row,
|
|
212
|
+
session: identityFor(session_id),
|
|
213
|
+
...(parent_session_id ? { parent_session: identityFor(parent_session_id) } : {})
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
199
216
|
function overlapsStage(link, stage, observedAt) {
|
|
200
217
|
const linkStart = Date.parse(link.created_at);
|
|
201
218
|
const linkEnd = Date.parse(link.completed_at ?? observedAt);
|
|
@@ -211,6 +228,17 @@ function clipToStage(link, stage, observedAt) {
|
|
|
211
228
|
const completedAt = Date.parse(naturalEnd) > Date.parse(stageEnd) ? stageEnd : naturalEnd;
|
|
212
229
|
return { ...link, created_at: startedAt, completed_at: completedAt };
|
|
213
230
|
}
|
|
231
|
+
function nativeSessionId(session) {
|
|
232
|
+
return session.provider_session_id ?? session.session_id;
|
|
233
|
+
}
|
|
234
|
+
function publicSessionIdentities(context, projectId, storageIds) {
|
|
235
|
+
if (storageIds.length === 0)
|
|
236
|
+
return [];
|
|
237
|
+
const marks = storageIds.map(() => "?").join(", ");
|
|
238
|
+
const rows = context.db.all(`SELECT session_id, harness, provider_session_id FROM sessions WHERE project_id = ? AND session_id IN (${marks})`, [projectId, ...storageIds]);
|
|
239
|
+
const byStorage = new Map(rows.map((row) => [row.session_id, publicSessionIdentity(row)]));
|
|
240
|
+
return storageIds.flatMap((id) => byStorage.get(id) ?? []);
|
|
241
|
+
}
|
|
214
242
|
function toolCallsForLinks(context, links, observedAt) {
|
|
215
243
|
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
216
244
|
const sessions = new Map();
|
|
@@ -220,7 +248,7 @@ function toolCallsForLinks(context, links, observedAt) {
|
|
|
220
248
|
const first = windows[0];
|
|
221
249
|
const parsed = isExternalHarness(first.harness)
|
|
222
250
|
? readHarnessToolCallsWindow(context, first.harness, first.project_id, first.provider_session_id, windows.map((item) => item.created_at).sort()[0], windows.map((item) => item.completed_at ?? observedAt).sort().at(-1), observedAt)
|
|
223
|
-
: readToolCallsForWindows(first.transcript_path,
|
|
251
|
+
: readToolCallsForWindows(first.transcript_path, nativeSessionId(first), windows, observedAt);
|
|
224
252
|
if (!parsed) {
|
|
225
253
|
result.unavailable_sessions.push(sessionId);
|
|
226
254
|
continue;
|
|
@@ -356,15 +384,15 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
|
356
384
|
sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
|
|
357
385
|
const physicalSessions = [...sessions.values()].filter((sessionLinks) => {
|
|
358
386
|
const link = sessionLinks[0];
|
|
359
|
-
if (!link.
|
|
387
|
+
if (!link.provider_parent_session_id)
|
|
360
388
|
return true;
|
|
361
|
-
const parent = links.find((candidate) => candidate.session_id === link.
|
|
389
|
+
const parent = links.find((candidate) => candidate.session_id === link.provider_parent_session_id);
|
|
362
390
|
return !parent || !hasInclusiveUsageSnapshot(context, parent.project_id, parent.harness, parent.provider_session_id, observedAt);
|
|
363
391
|
});
|
|
364
392
|
return physicalSessions.map((sessionLinks) => {
|
|
365
393
|
const link = sessionLinks[0];
|
|
366
394
|
const startedAt = stage?.startedAt ?? sessionLinks.map((item) => item.created_at).sort()[0];
|
|
367
|
-
const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, link
|
|
395
|
+
const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, nativeSessionId(link));
|
|
368
396
|
const naturalEnd = isExternalHarness(link.harness)
|
|
369
397
|
? observedAt
|
|
370
398
|
: completedTurn?.completed_at ?? sessionLinks.map((item) => item.completed_at ?? observedAt).sort().at(-1);
|
|
@@ -372,7 +400,7 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
|
372
400
|
const harnessUsage = isExternalHarness(link.harness)
|
|
373
401
|
? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt)
|
|
374
402
|
: null;
|
|
375
|
-
const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, link
|
|
403
|
+
const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, nativeSessionId(link), startedAt, endedAt);
|
|
376
404
|
return {
|
|
377
405
|
work_session_id: link.id,
|
|
378
406
|
work_id: link.work_id,
|
|
@@ -427,15 +455,6 @@ function readHarnessUsageWindow(context, harness, projectId, providerSessionId,
|
|
|
427
455
|
}
|
|
428
456
|
};
|
|
429
457
|
}
|
|
430
|
-
function terminalTimestamp(indexJson) {
|
|
431
|
-
try {
|
|
432
|
-
const index = JSON.parse(indexJson);
|
|
433
|
-
return ["done", "blocked", "cancelled", "failed"].includes(index.status ?? "") && typeof index.completed_at === "string" ? index.completed_at : null;
|
|
434
|
-
}
|
|
435
|
-
catch {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
458
|
function settledRunSessions(context, links, observedAt) {
|
|
440
459
|
const sessions = new Map();
|
|
441
460
|
for (const link of links)
|
|
@@ -444,16 +463,17 @@ function settledRunSessions(context, links, observedAt) {
|
|
|
444
463
|
if (sessionLinks.some((link) => !link.completed_at))
|
|
445
464
|
return [];
|
|
446
465
|
const link = sessionLinks[0];
|
|
447
|
-
const
|
|
448
|
-
|
|
466
|
+
const sessionStartedAt = sessionLinks.map((item) => item.created_at).sort()[0];
|
|
467
|
+
const workCompletedAt = sessionLinks.map((item) => item.completed_at).sort().at(-1);
|
|
468
|
+
if (isExternalHarness(link.harness) && link.provider_session_id) {
|
|
449
469
|
const snapshot = context.db.get(`SELECT observed_at FROM harness_usage_snapshots
|
|
450
470
|
WHERE project_id = ? AND harness = ? AND provider_session_id = ?
|
|
451
|
-
AND observed_at >= ? AND observed_at <= ?
|
|
452
|
-
ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id,
|
|
471
|
+
AND completeness = 'complete' AND observed_at >= ? AND observed_at <= ?
|
|
472
|
+
ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id, workCompletedAt, observedAt]);
|
|
453
473
|
return snapshot ? [{ session_id: sessionId, completed_at: snapshot.observed_at }] : [];
|
|
454
474
|
}
|
|
455
|
-
const completed = completedProviderTurn(sessionLinks[0].transcript_path,
|
|
456
|
-
if (!completed || (
|
|
475
|
+
const completed = completedProviderTurn(sessionLinks[0].transcript_path, nativeSessionId(sessionLinks[0]));
|
|
476
|
+
if (!completed || Date.parse(completed.completed_at) < Date.parse(workCompletedAt) || Date.parse(completed.completed_at) < Date.parse(sessionStartedAt))
|
|
457
477
|
return [];
|
|
458
478
|
return [{ session_id: sessionId, completed_at: completed.completed_at }];
|
|
459
479
|
});
|
|
@@ -492,10 +512,10 @@ function completedProviderTurn(transcriptPath, expectedSessionId) {
|
|
|
492
512
|
}
|
|
493
513
|
}
|
|
494
514
|
function usageReport(context, input, refresh) {
|
|
495
|
-
const sessions = context.db.all(`SELECT DISTINCT session_id, project_id, ? AS run_id, transcript_path, parent_session_id, role, aspect_id, plan_item_id, session_kind
|
|
515
|
+
const sessions = context.db.all(`SELECT DISTINCT session_id, project_id, ? AS run_id, transcript_path, harness, provider_session_id, parent_session_id, role, aspect_id, plan_item_id, session_kind
|
|
496
516
|
FROM sessions WHERE project_id = ? AND run_id = ?
|
|
497
517
|
UNION
|
|
498
|
-
SELECT DISTINCT f.session_id, f.project_id, ? AS run_id, f.transcript_path, f.parent_session_id, f.role, f.aspect_id, f.plan_item_id, f.session_kind
|
|
518
|
+
SELECT DISTINCT f.session_id, f.project_id, ? AS run_id, f.transcript_path, f.harness, f.provider_session_id, f.parent_session_id, f.role, f.aspect_id, f.plan_item_id, f.session_kind
|
|
499
519
|
FROM flow_session_segments s JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
500
520
|
WHERE s.project_id = ? AND s.run_id = ?`, [input.runId, input.projectId, input.runId, input.runId, input.projectId, input.runId]);
|
|
501
521
|
const selectedSessions = input.sessionId ? sessions.filter((session) => session.session_id === input.sessionId) : sessions;
|
|
@@ -508,13 +528,15 @@ function usageReport(context, input, refresh) {
|
|
|
508
528
|
LEFT JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
509
529
|
WHERE s.project_id = ? AND s.run_id = ? ORDER BY s.session_id, s.observed_at, s.id`, [input.projectId, input.runId]);
|
|
510
530
|
const selectedRows = input.sessionId ? rows.filter((row) => row.session_id === input.sessionId) : rows;
|
|
511
|
-
const
|
|
531
|
+
const publicSessions = new Map(sessions.map((session) => [session.session_id, publicSessionIdentity({ harness: session.harness ?? "codex-desktop", provider_session_id: session.provider_session_id ?? null, session_id: session.session_id })]));
|
|
532
|
+
const rawDeltas = usageDeltas(selectedRows);
|
|
533
|
+
const deltas = rawDeltas.map(({ session_id, ...delta }) => ({ ...delta, session: publicSessions.get(session_id) ?? null }));
|
|
512
534
|
const segments = context.db.all(`SELECT id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name
|
|
513
535
|
FROM flow_session_segments WHERE project_id = ? AND run_id = ? ORDER BY started_at, id`, [input.projectId, input.runId]);
|
|
514
536
|
const supported = new Set(["session", "role", "aspect", "plan-item", "stage", "protocol"]);
|
|
515
537
|
const groupBy = supported.has(input.groupBy) ? input.groupBy : "session";
|
|
516
538
|
const groups = new Map();
|
|
517
|
-
for (const delta of
|
|
539
|
+
for (const delta of rawDeltas) {
|
|
518
540
|
const key = groupKey(delta, groupBy);
|
|
519
541
|
const group = groups.get(key) ?? { tokens: emptyUsage(), snapshots: 0, statuses: {}, transition_buckets: 0 };
|
|
520
542
|
group.snapshots += 1;
|
|
@@ -531,7 +553,9 @@ function usageReport(context, input, refresh) {
|
|
|
531
553
|
run_id: input.runId,
|
|
532
554
|
source: { kind: "codex_transcript_v1", parser_version: 1, refreshed_sessions: refreshed.length, checkpoint_source: refresh ? "refreshed" : "stored" },
|
|
533
555
|
sessions: selectedSessions.map((session) => ({
|
|
534
|
-
|
|
556
|
+
session: publicSessionIdentity({ harness: session.harness ?? "codex-desktop", provider_session_id: session.provider_session_id ?? null, session_id: session.session_id }),
|
|
557
|
+
...(session.parent_session_id ? { parent_session: publicSessions.get(session.parent_session_id) ?? null } : {}),
|
|
558
|
+
role: session.role ?? null,
|
|
535
559
|
aspect_id: session.aspect_id ?? null, plan_item_id: session.plan_item_id ?? null, session_kind: session.session_kind ?? null
|
|
536
560
|
})),
|
|
537
561
|
groups: [...groups.entries()].map(([key, group]) => ({ key, ...group, tokens: outputUsageTotals(group.tokens) })),
|
|
@@ -539,7 +563,7 @@ function usageReport(context, input, refresh) {
|
|
|
539
563
|
tool_calls: toolCallsForSessions(sessions),
|
|
540
564
|
segments: segments.map((segment) => ({
|
|
541
565
|
segment_id: segment.id,
|
|
542
|
-
|
|
566
|
+
session: publicSessions.get(segment.session_id) ?? null,
|
|
543
567
|
run_id: segment.run_id,
|
|
544
568
|
protocol_id: segment.protocol_id,
|
|
545
569
|
started_at: segment.started_at,
|
|
@@ -552,7 +576,7 @@ function usageReport(context, input, refresh) {
|
|
|
552
576
|
function toolCallsForSessions(sessions) {
|
|
553
577
|
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
554
578
|
for (const session of sessions) {
|
|
555
|
-
const parsed = readToolCalls(session.transcript_path, session
|
|
579
|
+
const parsed = readToolCalls(session.transcript_path, nativeSessionId(session));
|
|
556
580
|
if (!parsed) {
|
|
557
581
|
result.unavailable_sessions.push(session.session_id);
|
|
558
582
|
continue;
|
|
@@ -611,7 +635,7 @@ function readToolCalls(transcriptPath, expectedSessionId) {
|
|
|
611
635
|
function segmentUsage(context, session, segment) {
|
|
612
636
|
if (!session)
|
|
613
637
|
return { status: "unavailable", diagnostic: "session_record_missing" };
|
|
614
|
-
const result = readCodexTranscriptWindow(session.transcript_path, session
|
|
638
|
+
const result = readCodexTranscriptWindow(session.transcript_path, nativeSessionId(session), segment.started_at, segment.ended_at ?? context.now());
|
|
615
639
|
return result.counter ? { status: result.status, tokens: result.counter.usage, token_event_at: result.counter.token_event_at } : { status: result.status, diagnostic: result.diagnostic ?? null };
|
|
616
640
|
}
|
|
617
641
|
function readCodexTranscript(transcriptPath, expectedSessionId) {
|
|
@@ -5,19 +5,20 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
|
|
8
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
|
|
9
9
|
import { validateSchema } from "./schema-validation.js";
|
|
10
10
|
import { flowCommand } from "./stage-pause.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair
|
|
14
|
-
import { finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures } from "./code-checks.js";
|
|
13
|
+
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
14
|
+
import { finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
15
15
|
import { addWorkBatch, bindStageCoordinatorWork, finishWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
16
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
18
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
19
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
20
20
|
import { ensureVnextMergeRequest } from "./vnext-merge.js";
|
|
21
|
+
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
21
22
|
const stage = "code-review";
|
|
22
23
|
const stageDir = vnextStageDirectory(stage);
|
|
23
24
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -46,29 +47,32 @@ export function startVnextCodeReview(context, input) {
|
|
|
46
47
|
const orchestration = readFanoutDescriptor(root);
|
|
47
48
|
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
48
49
|
}
|
|
49
|
-
const mode = effectiveMode(
|
|
50
|
+
const mode = effectiveMode(home, run);
|
|
51
|
+
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
50
52
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
51
|
-
|
|
53
|
+
const reviewContextPath = path.join(root, "review-context.json");
|
|
54
|
+
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
|
|
55
|
+
if (mode.mode === "off") {
|
|
52
56
|
const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
|
|
53
57
|
fs.writeFileSync(promptPath, prompt);
|
|
54
58
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
55
59
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
56
60
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
57
|
-
return { ok: true, run_id: run.id, stage, mode, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
61
|
+
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
58
62
|
}
|
|
59
|
-
const groups = reviewGroups(
|
|
63
|
+
const groups = reviewGroups(home);
|
|
60
64
|
const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", group, read_only: true } })) };
|
|
61
65
|
const batchFile = path.join(root, "review-work-batch.json");
|
|
62
66
|
fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
|
|
63
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode, groups });
|
|
67
|
+
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
|
|
64
68
|
fs.writeFileSync(promptPath, prompt);
|
|
65
69
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
66
70
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
67
71
|
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, file: batchFile });
|
|
68
72
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
69
73
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
70
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode, groups, registered });
|
|
71
|
-
return { ok: true, run_id: run.id, stage, mode, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
74
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
75
|
+
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
72
76
|
}
|
|
73
77
|
/** Create a narrow CODE repair from accepted independent-review evidence. */
|
|
74
78
|
export function addVnextCodeReviewRepair(context, input) {
|
|
@@ -96,13 +100,13 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
96
100
|
if (unknown.length)
|
|
97
101
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
|
|
98
102
|
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref)).filter((check) => check.run_at !== "external");
|
|
103
|
+
if (!reviewChecks.length)
|
|
104
|
+
throw new AppError("review_repair_executable_check_required", "CODE-REVIEW repair requires at least one executable causal check; external-only evidence cannot validate a repair Work", 2, { finding_ids: input.findingIds, check_refs: requestedCheckRefs });
|
|
99
105
|
return addVnextCodeRepair(context, {
|
|
100
106
|
projectRoot,
|
|
101
107
|
runId: run.id,
|
|
102
|
-
|
|
103
|
-
reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
|
|
108
|
+
reviewFindings: selected.map(({ finding_ref, finding }) => ({ finding_ref, priority: finding.priority, problem: finding.problem, impact: finding.impact, required_outcome: finding.required_outcome, evidence_refs: finding.evidence_refs, obligation_refs: finding.obligation_refs, decision_reason: input.decisionReasonsByFinding?.[finding_ref] ?? input.objective, check_refs: input.checkRefsByFinding[finding_ref] ?? [] })),
|
|
104
109
|
reviewChecks,
|
|
105
|
-
reviewCheckRefs: requestedCheckRefs,
|
|
106
110
|
originWorkIds: origins,
|
|
107
111
|
objective: input.objective
|
|
108
112
|
});
|
|
@@ -113,7 +117,15 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
113
117
|
const run = requireRun(context, project.id, input.runId);
|
|
114
118
|
const home = requireHome(run);
|
|
115
119
|
const root = path.join(home, stageDir);
|
|
116
|
-
const
|
|
120
|
+
const reviewContext = readJson(path.join(root, "review-context.json"));
|
|
121
|
+
const mode = reviewContext.mode;
|
|
122
|
+
if (!mode)
|
|
123
|
+
throw new AppError("runtime_missing", "CODE-REVIEW effective mode was not frozen at stage start", 1);
|
|
124
|
+
if (reviewContext.code_report_sha256 !== sha256File(path.join(home, "05-code", "stage-report.json")))
|
|
125
|
+
throw new AppError("code_review_input_changed", "Accepted CODE report changed after CODE-REVIEW start", 2);
|
|
126
|
+
const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
|
|
127
|
+
if (handoffFailures.length)
|
|
128
|
+
throw new AppError("validation", "CODE-REVIEW handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
|
|
117
129
|
const reportPath = path.join(root, "stage-report.json");
|
|
118
130
|
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
119
131
|
const report = readJson(reportPath);
|
|
@@ -126,7 +138,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
126
138
|
const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
|
|
127
139
|
if (!fs.existsSync(decisionFile) && mode === "off")
|
|
128
140
|
fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
|
|
129
|
-
validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
141
|
+
validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
130
142
|
const decision = readJson(decisionFile);
|
|
131
143
|
const reviewers = reviewerWorks(context, project.id, run.id);
|
|
132
144
|
if (mode !== "off") {
|
|
@@ -144,7 +156,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
144
156
|
throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
|
|
145
157
|
const fixIds = canonical.fix_ids;
|
|
146
158
|
if (fixIds.length && repairs.length === 0) {
|
|
147
|
-
const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
|
|
159
|
+
const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
|
|
148
160
|
fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
|
|
149
161
|
return { ok: true, run_id: run.id, stage, outcome: "repair_required", decision_sha256: decisionSha, repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke the same stage finish command again with the unchanged decision file.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
|
|
150
162
|
}
|
|
@@ -157,10 +169,10 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
157
169
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
158
170
|
const repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
159
171
|
const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
|
|
160
|
-
const unchangedFailures =
|
|
172
|
+
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks });
|
|
161
173
|
if (unchangedFailures.length)
|
|
162
174
|
throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; repair the evidenced failure before retrying", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures });
|
|
163
|
-
const receipts =
|
|
175
|
+
const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.progress ? { progress: input.progress } : {}) });
|
|
164
176
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
165
177
|
if (failed.length)
|
|
166
178
|
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
@@ -174,8 +186,11 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
174
186
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
175
187
|
const now = context.now();
|
|
176
188
|
const startedAt = stageStartedAt(run, now);
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json` }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
|
|
190
|
+
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
191
|
+
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
192
|
+
const acceptedPaths = changedPaths(run.workspace_root);
|
|
193
|
+
const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
179
194
|
writeReport(root, report);
|
|
180
195
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
181
196
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
@@ -186,24 +201,34 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
186
201
|
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
|
|
187
202
|
}
|
|
188
203
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
189
|
-
const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
|
|
190
204
|
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {}) };
|
|
191
205
|
}
|
|
192
|
-
function reviewGroups(
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
for (const file of
|
|
206
|
+
function reviewGroups(home) {
|
|
207
|
+
const expected = new Set(baselineAspects);
|
|
208
|
+
const declared = [];
|
|
209
|
+
for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
|
|
196
210
|
const map = readJson(file);
|
|
197
211
|
for (const aspect of map.aspects ?? [])
|
|
198
212
|
if (aspect.applicability === "applicable" && aspect.aspect_id)
|
|
199
|
-
|
|
213
|
+
expected.add(aspect.aspect_id);
|
|
214
|
+
for (const group of map.review_groups ?? [])
|
|
215
|
+
if (group.id && group.aspect_ids?.length)
|
|
216
|
+
declared.push({ key: `${map.protocol_id ?? path.basename(path.dirname(file))}/${group.id}`, aspect_ids: [...new Set(group.aspect_ids)] });
|
|
200
217
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
if (!declared.length)
|
|
219
|
+
return [{ key: "baseline", aspect_ids: [...expected].sort() }];
|
|
220
|
+
const assigned = new Set();
|
|
221
|
+
for (const group of declared)
|
|
222
|
+
for (const aspectId of group.aspect_ids) {
|
|
223
|
+
if (assigned.has(aspectId))
|
|
224
|
+
throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one accepted group", 2, { aspect_id: aspectId });
|
|
225
|
+
assigned.add(aspectId);
|
|
226
|
+
}
|
|
227
|
+
const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
|
|
228
|
+
return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
|
|
204
229
|
}
|
|
205
|
-
function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring.
|
|
206
|
-
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.
|
|
230
|
+
export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
|
|
231
|
+
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the same Finish command again. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
|
|
207
232
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
208
233
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
209
234
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -252,8 +277,11 @@ function validateDecision(context, input) {
|
|
|
252
277
|
const repairedBy = new Map();
|
|
253
278
|
for (const work of input.repairs) {
|
|
254
279
|
const result = readJsonString(work.result);
|
|
255
|
-
for (const
|
|
256
|
-
|
|
280
|
+
for (const resolution of Array.isArray(result.resolutions) ? result.resolutions : []) {
|
|
281
|
+
const ref = typeof resolution === "object" && resolution !== null && typeof resolution.finding_ref === "string" ? resolution.finding_ref : null;
|
|
282
|
+
if (ref)
|
|
283
|
+
repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
|
|
284
|
+
}
|
|
257
285
|
}
|
|
258
286
|
for (const { finding_ref: findingRef, finding } of findings) {
|
|
259
287
|
const item = decisions.get(findingRef);
|
|
@@ -278,8 +306,20 @@ function validateDecision(context, input) {
|
|
|
278
306
|
if (!known.has(item.finding_ref))
|
|
279
307
|
throw new AppError("review_evidence_invalid", "Decision references an unknown reviewer finding", 2, { finding_ref: item.finding_ref });
|
|
280
308
|
}
|
|
281
|
-
function validateReviewerResult(
|
|
282
|
-
|
|
309
|
+
function validateReviewerResult(_context, work, _run) {
|
|
310
|
+
if (!work.result)
|
|
311
|
+
throw new AppError("review_evidence_invalid", "Reviewer Work has no result", 2, { work_id: work.work_id });
|
|
312
|
+
// Schema, group identity and portable evidence references were validated
|
|
313
|
+
// before this Work was accepted. This fan-in guard only checks semantic
|
|
314
|
+
// relationships that depend on the aggregate review decision.
|
|
315
|
+
const result = readJsonString(work.result);
|
|
316
|
+
if (result.verdict === "blocked")
|
|
317
|
+
throw new AppError("review_blocked", "A blocked reviewer result cannot be accepted as completed review evidence", 2, { work_id: work.work_id });
|
|
318
|
+
if (result.verdict === "pass" && result.findings.length)
|
|
319
|
+
throw new AppError("review_evidence_invalid", "A passing reviewer result cannot contain material findings", 2, { work_id: work.work_id });
|
|
320
|
+
if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
|
|
321
|
+
throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
|
|
322
|
+
}
|
|
283
323
|
function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const p = readJsonString(work.payload_json); return Boolean(p && p.kind === "code-review"); }); }
|
|
284
324
|
function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
|
|
285
325
|
function acceptedCodeChecks(context, projectId, runId) { return codeWorks(context, projectId, runId).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }); }
|
|
@@ -306,11 +346,12 @@ export function isCodeReviewStageRepair(work, root) {
|
|
|
306
346
|
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
307
347
|
}
|
|
308
348
|
function reviewFindingIds(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
309
|
-
return []; const
|
|
349
|
+
return []; const findings = repair.review_findings; return Array.isArray(findings) ? findings.flatMap((value) => typeof value === "object" && value !== null && typeof value.finding_ref === "string" ? [value.finding_ref] : []) : []; }
|
|
310
350
|
function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
311
|
-
return []; const
|
|
351
|
+
return []; const findings = repair.review_findings; return Array.isArray(findings) ? [...new Set(findings.flatMap((value) => typeof value === "object" && value !== null && Array.isArray(value.check_refs) ? value.check_refs.filter((ref) => typeof ref === "string") : []))] : []; }
|
|
312
352
|
function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
|
|
313
|
-
function effectiveMode(
|
|
353
|
+
function effectiveMode(home, run) { const index = JSON.parse(run.index_json); const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
|
|
354
|
+
return { mode: requested, source: configured?.source === "user_instruction" ? "user_instruction" : "project_policy", reason: configured?.reason ?? "Frozen RUN configuration." }; const assessments = findFiles(path.join(home, "03-plan"), "plan.json").map((file) => readJson(file)); const deep = assessments.some((plan) => plan.assessment?.failure_impact?.level === "high" || plan.assessment?.solution_uncertainty?.level === "high"); return { mode: deep ? "deep" : "standard", source: "plan_assessment", reason: deep ? "Accepted PLAN assessment records high impact or uncertainty." : "Accepted PLAN assessment requires the standard independent review." }; }
|
|
314
355
|
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
315
356
|
function changedPaths(workspaceRoot) { try {
|
|
316
357
|
return execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replace(/^.* -> /, ""));
|
|
@@ -334,10 +375,10 @@ catch {
|
|
|
334
375
|
function runRef(runId, home, file) { const relative = path.relative(home, file).split(path.sep).join("/"); return relative && !relative.startsWith("../") ? `run://${runId}/${relative}` : file; }
|
|
335
376
|
function requireRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work || work.status !== "running")
|
|
336
377
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
337
|
-
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root,
|
|
378
|
+
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
|
|
338
379
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
339
|
-
function requireHome(run) { if (!run.
|
|
340
|
-
throw new AppError("runtime_missing", "RUN
|
|
380
|
+
function requireHome(run) { if (!run.run_root)
|
|
381
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
341
382
|
function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage code-review --decision-file ${JSON.stringify(decision)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
|
|
342
383
|
function findFiles(root, name) { if (!fs.existsSync(root))
|
|
343
384
|
return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|