@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.7
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 +49 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +2 -2
- package/dist/cli/run-cli.js +105 -7
- package/dist/schemas/code-work-batch.schema.json +3 -3
- 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/eval-snapshots.js +5 -2
- 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 +5 -0
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/runs.js +50 -6
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +26 -11
- package/dist/services/stage-pause.js +31 -16
- package/dist/services/usage.js +74 -42
- package/dist/services/vnext-code-review.js +46 -12
- package/dist/services/vnext-code.js +59 -22
- package/dist/services/vnext-merge.js +102 -42
- package/dist/services/vnext-plan-review.js +31 -13
- package/dist/services/vnext-plan.js +57 -9
- package/dist/services/work-registry.js +130 -27
- package/dist/storage/database.js +125 -2
- package/package.json +12 -12
- 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
|
}
|
|
@@ -136,7 +137,7 @@ export function recalculateRunUsage(context, input) {
|
|
|
136
137
|
// after `work finish` but before the agent's actual terminal event.
|
|
137
138
|
const accountingAt = observedAt;
|
|
138
139
|
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.session_id, ws.created_at, ws.completed_at,
|
|
139
|
-
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
|
|
140
141
|
FROM work_sessions ws
|
|
141
142
|
JOIN works w ON w.work_id = ws.work_id
|
|
142
143
|
LEFT JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
|
|
@@ -154,31 +155,34 @@ export function recalculateRunUsage(context, input) {
|
|
|
154
155
|
const settledSessions = settledRunSessions(context, links, observedAt);
|
|
155
156
|
const unsettledSessions = [...new Set(links.map((link) => link.session_id))].filter((sessionId) => !settledSessions.some((session) => session.session_id === sessionId));
|
|
156
157
|
const usageFinal = final && unsettledSessions.length === 0;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
context.db.
|
|
160
|
-
|
|
161
|
-
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)
|
|
162
165
|
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 'windowed_work_session', 1, ?, ?, ?)`, [
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
175
|
SET status = 'stopped', stop_reason = 'provider_turn_complete', updated_at = ?, stopped_at = ?
|
|
173
176
|
WHERE project_id = ? AND run_id = ? AND session_id = ?
|
|
174
177
|
AND status = 'idle'
|
|
175
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;
|
|
176
185
|
}
|
|
177
|
-
context.db.exec("COMMIT");
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
context.db.exec("ROLLBACK");
|
|
181
|
-
throw error;
|
|
182
186
|
}
|
|
183
187
|
return {
|
|
184
188
|
ok: true,
|
|
@@ -187,16 +191,28 @@ export function recalculateRunUsage(context, input) {
|
|
|
187
191
|
status: usageFinal ? "final" : "provisional",
|
|
188
192
|
scope: input.stage ? { kind: "stage", stage: input.stage.name, started_at: input.stage.startedAt, completed_at: input.stage.completedAt } : { kind: "run" },
|
|
189
193
|
observed_at: observedAt,
|
|
194
|
+
persisted: persistRunProjection,
|
|
190
195
|
totals: outputUsageTotals(totals),
|
|
191
|
-
sessions: rows,
|
|
196
|
+
sessions: publicUsageRows(context, input.projectId, rows),
|
|
192
197
|
session_reconciliation: {
|
|
193
|
-
stopped: settledSessions.map((session) => session.session_id),
|
|
194
|
-
unsettled: unsettledSessions
|
|
198
|
+
stopped: publicSessionIdentities(context, input.projectId, settledSessions.map((session) => session.session_id)),
|
|
199
|
+
unsettled: publicSessionIdentities(context, input.projectId, unsettledSessions)
|
|
195
200
|
},
|
|
196
201
|
tool_calls: toolCallsForLinks(context, links, accountingAt),
|
|
197
202
|
coverage: rows.reduce((coverage, row) => ({ ...coverage, [row.status]: (coverage[row.status] ?? 0) + 1 }), {})
|
|
198
203
|
};
|
|
199
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
|
+
}
|
|
200
216
|
function overlapsStage(link, stage, observedAt) {
|
|
201
217
|
const linkStart = Date.parse(link.created_at);
|
|
202
218
|
const linkEnd = Date.parse(link.completed_at ?? observedAt);
|
|
@@ -212,6 +228,17 @@ function clipToStage(link, stage, observedAt) {
|
|
|
212
228
|
const completedAt = Date.parse(naturalEnd) > Date.parse(stageEnd) ? stageEnd : naturalEnd;
|
|
213
229
|
return { ...link, created_at: startedAt, completed_at: completedAt };
|
|
214
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
|
+
}
|
|
215
242
|
function toolCallsForLinks(context, links, observedAt) {
|
|
216
243
|
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
217
244
|
const sessions = new Map();
|
|
@@ -221,7 +248,7 @@ function toolCallsForLinks(context, links, observedAt) {
|
|
|
221
248
|
const first = windows[0];
|
|
222
249
|
const parsed = isExternalHarness(first.harness)
|
|
223
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)
|
|
224
|
-
: readToolCallsForWindows(first.transcript_path,
|
|
251
|
+
: readToolCallsForWindows(first.transcript_path, nativeSessionId(first), windows, observedAt);
|
|
225
252
|
if (!parsed) {
|
|
226
253
|
result.unavailable_sessions.push(sessionId);
|
|
227
254
|
continue;
|
|
@@ -357,15 +384,15 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
|
357
384
|
sessions.set(link.session_id, [...(sessions.get(link.session_id) ?? []), link]);
|
|
358
385
|
const physicalSessions = [...sessions.values()].filter((sessionLinks) => {
|
|
359
386
|
const link = sessionLinks[0];
|
|
360
|
-
if (!link.
|
|
387
|
+
if (!link.provider_parent_session_id)
|
|
361
388
|
return true;
|
|
362
|
-
const parent = links.find((candidate) => candidate.session_id === link.
|
|
389
|
+
const parent = links.find((candidate) => candidate.session_id === link.provider_parent_session_id);
|
|
363
390
|
return !parent || !hasInclusiveUsageSnapshot(context, parent.project_id, parent.harness, parent.provider_session_id, observedAt);
|
|
364
391
|
});
|
|
365
392
|
return physicalSessions.map((sessionLinks) => {
|
|
366
393
|
const link = sessionLinks[0];
|
|
367
394
|
const startedAt = stage?.startedAt ?? sessionLinks.map((item) => item.created_at).sort()[0];
|
|
368
|
-
const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, link
|
|
395
|
+
const completedTurn = stage ? null : completedProviderTurn(link.transcript_path, nativeSessionId(link));
|
|
369
396
|
const naturalEnd = isExternalHarness(link.harness)
|
|
370
397
|
? observedAt
|
|
371
398
|
: completedTurn?.completed_at ?? sessionLinks.map((item) => item.completed_at ?? observedAt).sort().at(-1);
|
|
@@ -373,7 +400,7 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
|
373
400
|
const harnessUsage = isExternalHarness(link.harness)
|
|
374
401
|
? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt)
|
|
375
402
|
: null;
|
|
376
|
-
const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, link
|
|
403
|
+
const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, nativeSessionId(link), startedAt, endedAt);
|
|
377
404
|
return {
|
|
378
405
|
work_session_id: link.id,
|
|
379
406
|
work_id: link.work_id,
|
|
@@ -437,15 +464,16 @@ function settledRunSessions(context, links, observedAt) {
|
|
|
437
464
|
return [];
|
|
438
465
|
const link = sessionLinks[0];
|
|
439
466
|
const sessionStartedAt = sessionLinks.map((item) => item.created_at).sort()[0];
|
|
467
|
+
const workCompletedAt = sessionLinks.map((item) => item.completed_at).sort().at(-1);
|
|
440
468
|
if (isExternalHarness(link.harness) && link.provider_session_id) {
|
|
441
469
|
const snapshot = context.db.get(`SELECT observed_at FROM harness_usage_snapshots
|
|
442
470
|
WHERE project_id = ? AND harness = ? AND provider_session_id = ?
|
|
443
471
|
AND completeness = 'complete' AND observed_at >= ? AND observed_at <= ?
|
|
444
|
-
ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id,
|
|
472
|
+
ORDER BY observed_at DESC, id DESC LIMIT 1`, [link.project_id, link.harness, link.provider_session_id, workCompletedAt, observedAt]);
|
|
445
473
|
return snapshot ? [{ session_id: sessionId, completed_at: snapshot.observed_at }] : [];
|
|
446
474
|
}
|
|
447
|
-
const completed = completedProviderTurn(sessionLinks[0].transcript_path,
|
|
448
|
-
if (!completed || Date.parse(completed.completed_at) < Date.parse(sessionStartedAt))
|
|
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))
|
|
449
477
|
return [];
|
|
450
478
|
return [{ session_id: sessionId, completed_at: completed.completed_at }];
|
|
451
479
|
});
|
|
@@ -484,10 +512,10 @@ function completedProviderTurn(transcriptPath, expectedSessionId) {
|
|
|
484
512
|
}
|
|
485
513
|
}
|
|
486
514
|
function usageReport(context, input, refresh) {
|
|
487
|
-
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
|
|
488
516
|
FROM sessions WHERE project_id = ? AND run_id = ?
|
|
489
517
|
UNION
|
|
490
|
-
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
|
|
491
519
|
FROM flow_session_segments s JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
492
520
|
WHERE s.project_id = ? AND s.run_id = ?`, [input.runId, input.projectId, input.runId, input.runId, input.projectId, input.runId]);
|
|
493
521
|
const selectedSessions = input.sessionId ? sessions.filter((session) => session.session_id === input.sessionId) : sessions;
|
|
@@ -500,13 +528,15 @@ function usageReport(context, input, refresh) {
|
|
|
500
528
|
LEFT JOIN sessions f ON f.project_id = s.project_id AND f.session_id = s.session_id
|
|
501
529
|
WHERE s.project_id = ? AND s.run_id = ? ORDER BY s.session_id, s.observed_at, s.id`, [input.projectId, input.runId]);
|
|
502
530
|
const selectedRows = input.sessionId ? rows.filter((row) => row.session_id === input.sessionId) : rows;
|
|
503
|
-
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 }));
|
|
504
534
|
const segments = context.db.all(`SELECT id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name
|
|
505
535
|
FROM flow_session_segments WHERE project_id = ? AND run_id = ? ORDER BY started_at, id`, [input.projectId, input.runId]);
|
|
506
536
|
const supported = new Set(["session", "role", "aspect", "plan-item", "stage", "protocol"]);
|
|
507
537
|
const groupBy = supported.has(input.groupBy) ? input.groupBy : "session";
|
|
508
538
|
const groups = new Map();
|
|
509
|
-
for (const delta of
|
|
539
|
+
for (const delta of rawDeltas) {
|
|
510
540
|
const key = groupKey(delta, groupBy);
|
|
511
541
|
const group = groups.get(key) ?? { tokens: emptyUsage(), snapshots: 0, statuses: {}, transition_buckets: 0 };
|
|
512
542
|
group.snapshots += 1;
|
|
@@ -523,7 +553,9 @@ function usageReport(context, input, refresh) {
|
|
|
523
553
|
run_id: input.runId,
|
|
524
554
|
source: { kind: "codex_transcript_v1", parser_version: 1, refreshed_sessions: refreshed.length, checkpoint_source: refresh ? "refreshed" : "stored" },
|
|
525
555
|
sessions: selectedSessions.map((session) => ({
|
|
526
|
-
|
|
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,
|
|
527
559
|
aspect_id: session.aspect_id ?? null, plan_item_id: session.plan_item_id ?? null, session_kind: session.session_kind ?? null
|
|
528
560
|
})),
|
|
529
561
|
groups: [...groups.entries()].map(([key, group]) => ({ key, ...group, tokens: outputUsageTotals(group.tokens) })),
|
|
@@ -531,7 +563,7 @@ function usageReport(context, input, refresh) {
|
|
|
531
563
|
tool_calls: toolCallsForSessions(sessions),
|
|
532
564
|
segments: segments.map((segment) => ({
|
|
533
565
|
segment_id: segment.id,
|
|
534
|
-
|
|
566
|
+
session: publicSessions.get(segment.session_id) ?? null,
|
|
535
567
|
run_id: segment.run_id,
|
|
536
568
|
protocol_id: segment.protocol_id,
|
|
537
569
|
started_at: segment.started_at,
|
|
@@ -544,7 +576,7 @@ function usageReport(context, input, refresh) {
|
|
|
544
576
|
function toolCallsForSessions(sessions) {
|
|
545
577
|
const result = { status: "unavailable", total: 0, failures: 0, by_tool: {}, observed_sessions: 0, unavailable_sessions: [] };
|
|
546
578
|
for (const session of sessions) {
|
|
547
|
-
const parsed = readToolCalls(session.transcript_path, session
|
|
579
|
+
const parsed = readToolCalls(session.transcript_path, nativeSessionId(session));
|
|
548
580
|
if (!parsed) {
|
|
549
581
|
result.unavailable_sessions.push(session.session_id);
|
|
550
582
|
continue;
|
|
@@ -603,7 +635,7 @@ function readToolCalls(transcriptPath, expectedSessionId) {
|
|
|
603
635
|
function segmentUsage(context, session, segment) {
|
|
604
636
|
if (!session)
|
|
605
637
|
return { status: "unavailable", diagnostic: "session_record_missing" };
|
|
606
|
-
const result = readCodexTranscriptWindow(session.transcript_path, session
|
|
638
|
+
const result = readCodexTranscriptWindow(session.transcript_path, nativeSessionId(session), segment.started_at, segment.ended_at ?? context.now());
|
|
607
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 };
|
|
608
640
|
}
|
|
609
641
|
function readCodexTranscript(transcriptPath, expectedSessionId) {
|
|
@@ -10,14 +10,15 @@ 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,8 +47,10 @@ 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(run,
|
|
50
|
+
const mode = effectiveMode(run, acceptedCodeChangedPaths(home));
|
|
50
51
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
52
|
+
const reviewContextPath = path.join(root, "review-context.json");
|
|
53
|
+
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
|
|
51
54
|
if (mode === "off") {
|
|
52
55
|
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
56
|
fs.writeFileSync(promptPath, prompt);
|
|
@@ -96,6 +99,8 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
96
99
|
if (unknown.length)
|
|
97
100
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
|
|
98
101
|
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref)).filter((check) => check.run_at !== "external");
|
|
102
|
+
if (!reviewChecks.length)
|
|
103
|
+
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
104
|
return addVnextCodeRepair(context, {
|
|
100
105
|
projectRoot,
|
|
101
106
|
runId: run.id,
|
|
@@ -113,7 +118,15 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
113
118
|
const run = requireRun(context, project.id, input.runId);
|
|
114
119
|
const home = requireHome(run);
|
|
115
120
|
const root = path.join(home, stageDir);
|
|
116
|
-
const
|
|
121
|
+
const reviewContext = readJson(path.join(root, "review-context.json"));
|
|
122
|
+
const mode = reviewContext.mode;
|
|
123
|
+
if (!mode)
|
|
124
|
+
throw new AppError("runtime_missing", "CODE-REVIEW effective mode was not frozen at stage start", 1);
|
|
125
|
+
if (reviewContext.code_report_sha256 !== sha256File(path.join(home, "05-code", "stage-report.json")))
|
|
126
|
+
throw new AppError("code_review_input_changed", "Accepted CODE report changed after CODE-REVIEW start", 2);
|
|
127
|
+
const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
|
|
128
|
+
if (handoffFailures.length)
|
|
129
|
+
throw new AppError("validation", "CODE-REVIEW handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
|
|
117
130
|
const reportPath = path.join(root, "stage-report.json");
|
|
118
131
|
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
119
132
|
const report = readJson(reportPath);
|
|
@@ -157,10 +170,10 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
157
170
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
158
171
|
const repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
159
172
|
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 =
|
|
173
|
+
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks });
|
|
161
174
|
if (unchangedFailures.length)
|
|
162
175
|
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 =
|
|
176
|
+
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
177
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
165
178
|
if (failed.length)
|
|
166
179
|
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
@@ -174,8 +187,11 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
174
187
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
175
188
|
const now = context.now();
|
|
176
189
|
const startedAt = stageStartedAt(run, now);
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
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" } };
|
|
191
|
+
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
192
|
+
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
193
|
+
const acceptedPaths = changedPaths(run.workspace_root);
|
|
194
|
+
const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
179
195
|
writeReport(root, report);
|
|
180
196
|
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
197
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
@@ -186,7 +202,6 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
186
202
|
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
|
|
187
203
|
}
|
|
188
204
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
189
|
-
const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
|
|
190
205
|
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
206
|
}
|
|
192
207
|
function reviewGroups(run, home, mode) {
|
|
@@ -202,7 +217,7 @@ function reviewGroups(run, home, mode) {
|
|
|
202
217
|
const chunk = mode === "deep" ? 1 : 3;
|
|
203
218
|
return Array.from({ length: Math.ceil(all.length / chunk) }, (_, index) => ({ key: `group-${index + 1}`, aspect_ids: all.slice(index * chunk, (index + 1) * chunk) }));
|
|
204
219
|
}
|
|
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.
|
|
220
|
+
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. 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.`; }
|
|
206
221
|
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_home_path, "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
222
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
208
223
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
@@ -278,8 +293,20 @@ function validateDecision(context, input) {
|
|
|
278
293
|
if (!known.has(item.finding_ref))
|
|
279
294
|
throw new AppError("review_evidence_invalid", "Decision references an unknown reviewer finding", 2, { finding_ref: item.finding_ref });
|
|
280
295
|
}
|
|
281
|
-
function validateReviewerResult(
|
|
282
|
-
|
|
296
|
+
function validateReviewerResult(_context, work, _run) {
|
|
297
|
+
if (!work.result)
|
|
298
|
+
throw new AppError("review_evidence_invalid", "Reviewer Work has no result", 2, { work_id: work.work_id });
|
|
299
|
+
// Schema, group identity and portable evidence references were validated
|
|
300
|
+
// before this Work was accepted. This fan-in guard only checks semantic
|
|
301
|
+
// relationships that depend on the aggregate review decision.
|
|
302
|
+
const result = readJsonString(work.result);
|
|
303
|
+
if (result.verdict === "blocked")
|
|
304
|
+
throw new AppError("review_blocked", "A blocked reviewer result cannot be accepted as completed review evidence", 2, { work_id: work.work_id });
|
|
305
|
+
if (result.verdict === "pass" && result.findings.length)
|
|
306
|
+
throw new AppError("review_evidence_invalid", "A passing reviewer result cannot contain material findings", 2, { work_id: work.work_id });
|
|
307
|
+
if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
|
|
308
|
+
throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
|
|
309
|
+
}
|
|
283
310
|
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
311
|
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
312
|
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 : []; }); }
|
|
@@ -311,6 +338,13 @@ function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json
|
|
|
311
338
|
return []; const refs = repair.review_check_refs; return Array.isArray(refs) ? refs.filter((value) => typeof value === "string") : []; }
|
|
312
339
|
function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
|
|
313
340
|
function effectiveMode(run, paths) { const index = JSON.parse(run.index_json); const requested = index.settings?.code_review?.mode ?? "auto"; return requested === "auto" ? (paths.length ? "standard" : "off") : requested; }
|
|
341
|
+
function acceptedCodeChangedPaths(home) { try {
|
|
342
|
+
const report = readJson(path.join(home, "05-code", "stage-report.json"));
|
|
343
|
+
return Array.isArray(report.semantic?.changed_files) ? report.semantic.changed_files.filter((item) => typeof item === "string") : [];
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return [];
|
|
347
|
+
} }
|
|
314
348
|
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
315
349
|
function changedPaths(workspaceRoot) { try {
|
|
316
350
|
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(/^.* -> /, ""));
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
7
|
+
import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
|
|
8
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
9
|
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
10
|
import { validateSchema } from "./schema-validation.js";
|
|
@@ -135,8 +135,22 @@ export async function finishVnextCode(context, input) {
|
|
|
135
135
|
}
|
|
136
136
|
// Reject a malformed semantic receipt before running the expensive gate.
|
|
137
137
|
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id });
|
|
138
|
-
if (verification.verdict
|
|
139
|
-
|
|
138
|
+
if (verification.verdict === "blocked") {
|
|
139
|
+
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
|
|
140
|
+
}
|
|
141
|
+
if (verification.verdict === "passed" && verification.unresolved.length > 0) {
|
|
142
|
+
throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
|
|
143
|
+
}
|
|
144
|
+
if (verification.verdict === "needs_repair") {
|
|
145
|
+
const repair = addVnextCodeRepair(context, {
|
|
146
|
+
projectRoot,
|
|
147
|
+
runId: run.id,
|
|
148
|
+
originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
|
|
149
|
+
semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
|
|
150
|
+
verificationPath: input.verificationFile,
|
|
151
|
+
objective: verification.summary
|
|
152
|
+
});
|
|
153
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. Run the returned repair Work, then update code-verification.json and invoke this same stage finish command again." };
|
|
140
154
|
}
|
|
141
155
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
142
156
|
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks });
|
|
@@ -174,18 +188,26 @@ export async function finishVnextCode(context, input) {
|
|
|
174
188
|
}
|
|
175
189
|
const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
|
|
176
190
|
const finalReceipts = latestReceiptsByCommand(allReceipts);
|
|
191
|
+
const projectedVerification = verificationProjection(works, finalReceipts);
|
|
192
|
+
const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
|
|
193
|
+
if (unresolvedAcceptance.length)
|
|
194
|
+
throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
|
|
177
195
|
const now = context.now();
|
|
178
196
|
const timing = stageTiming(home, stage, now);
|
|
179
|
-
const
|
|
197
|
+
const reportedPaths = [...new Set(works.flatMap((work) => resultPaths(work.result)))];
|
|
198
|
+
const observedChangedPaths = workspaceChangedPaths(run.workspace_root);
|
|
199
|
+
const changedPaths = observedChangedPaths ?? reportedPaths;
|
|
200
|
+
const missingReportedPaths = observedChangedPaths === null ? [] : reportedPaths.filter((item) => !observedChangedPaths.includes(item));
|
|
201
|
+
if (missingReportedPaths.length)
|
|
202
|
+
throw new AppError("changed_path_not_materialized", "CODE Work reported paths that are not changed in the accepted workspace", 2, { paths: missingReportedPaths });
|
|
180
203
|
const next = nextAction(run, changedPaths);
|
|
181
|
-
writeProtocolFlowStatus(run.workspace_root, home, run.id, next === "start_code_review" ? "CODE complete; CODE-REVIEW is next." : next === "start_merge" ? "CODE complete; MERGE is queued." : "CODE complete; this RUN reached its configured terminal boundary.");
|
|
182
204
|
const report = {
|
|
183
205
|
schema_id: "dd-flow/stage-report@1",
|
|
184
206
|
run_id: run.id,
|
|
185
207
|
stage,
|
|
186
208
|
generated_at: now,
|
|
187
209
|
verdict: "done",
|
|
188
|
-
verification:
|
|
210
|
+
verification: projectedVerification,
|
|
189
211
|
semantic: {
|
|
190
212
|
result: `Completed ${works.length} CODE Work item${works.length === 1 ? "" : "s"}; ${finalReceipts.filter((receipt) => receipt.status === "passed").length} current final check receipt${finalReceipts.length === 1 ? "" : "s"} passed.`,
|
|
191
213
|
acceptance: [...expected],
|
|
@@ -213,6 +235,10 @@ export async function finishVnextCode(context, input) {
|
|
|
213
235
|
artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
|
|
214
236
|
validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
|
|
215
237
|
};
|
|
238
|
+
// Creating the MERGE request is part of accepting this terminal handoff.
|
|
239
|
+
// Do it before materialising a terminal stage report or changing RUN state,
|
|
240
|
+
// so an invalid merge policy leaves CODE safely running and retryable.
|
|
241
|
+
const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
216
242
|
writeReport(root, report);
|
|
217
243
|
completeFlowRunStage(context, {
|
|
218
244
|
projectRoot,
|
|
@@ -237,7 +263,6 @@ export async function finishVnextCode(context, input) {
|
|
|
237
263
|
else {
|
|
238
264
|
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
239
265
|
}
|
|
240
|
-
const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
|
|
241
266
|
return {
|
|
242
267
|
ok: true,
|
|
243
268
|
run_id: run.id,
|
|
@@ -255,8 +280,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
255
280
|
const home = requireHome(run);
|
|
256
281
|
if (!input.objective.trim())
|
|
257
282
|
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
258
|
-
if (!input.checkReceiptId && !input.reviewFindingIds?.length) {
|
|
259
|
-
throw new AppError("validation", "Repair needs
|
|
283
|
+
if (!input.checkReceiptId && !input.reviewFindingIds?.length && !input.semanticUnresolved?.length) {
|
|
284
|
+
throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
|
|
260
285
|
}
|
|
261
286
|
const receipt = input.checkReceiptId ? context.db.get("SELECT id, declaration_id, run_id, command, status, receipt_path, stdout_path, stderr_path FROM check_receipts WHERE project_id = ? AND id = ?", [
|
|
262
287
|
project.id,
|
|
@@ -269,11 +294,19 @@ export function addVnextCodeRepair(context, input) {
|
|
|
269
294
|
}
|
|
270
295
|
const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
|
|
271
296
|
const packets = origins.map((work) => packet(work));
|
|
272
|
-
const invariantPackets = receipt
|
|
297
|
+
const invariantPackets = receipt || input.semanticUnresolved?.length
|
|
273
298
|
? codeWorks(context, project.id, run.id).map((work) => packet(work)).filter((value) => !value.repair)
|
|
274
299
|
: packets;
|
|
275
300
|
const first = packets[0];
|
|
276
|
-
|
|
301
|
+
// A later repair may be caused by any accepted aggregate declaration, not
|
|
302
|
+
// only the checks copied into its immediate repair parent. Otherwise a
|
|
303
|
+
// repair that fixes one failed aggregate check can make a second, already
|
|
304
|
+
// declared aggregate failure impossible to repair. The immutable source
|
|
305
|
+
// of repair eligibility is the original CODE graph.
|
|
306
|
+
const declaredChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
|
|
307
|
+
.map((work) => packet(work))
|
|
308
|
+
.filter((value) => !value.repair)
|
|
309
|
+
.flatMap((value) => value.checks));
|
|
277
310
|
const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
|
|
278
311
|
if (receipt && !failedCheck) {
|
|
279
312
|
throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
|
|
@@ -281,7 +314,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
281
314
|
declaration_id: receipt.declaration_id
|
|
282
315
|
});
|
|
283
316
|
}
|
|
284
|
-
const
|
|
317
|
+
const semanticRepair = Boolean(input.semanticUnresolved?.length);
|
|
318
|
+
const key = input.reviewFindingIds?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
|
|
285
319
|
const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
|
|
286
320
|
const repair = {
|
|
287
321
|
schema_id: "dd-flow/code-work-packet@5",
|
|
@@ -291,22 +325,23 @@ export function addVnextCodeRepair(context, input) {
|
|
|
291
325
|
repair: {
|
|
292
326
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
293
327
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
294
|
-
...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []), review_check_refs: unique(input.reviewCheckRefs ?? (input.reviewChecks ?? []).map((check) => check.id)) } : {})
|
|
328
|
+
...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []), review_check_refs: unique(input.reviewCheckRefs ?? (input.reviewChecks ?? []).map((check) => check.id)) } : {}),
|
|
329
|
+
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
|
|
295
330
|
},
|
|
296
331
|
task: input.objective,
|
|
297
332
|
semantic_spine: {
|
|
298
|
-
user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
|
|
333
|
+
user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : semanticRepair ? `Close the unresolved CODE verification: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
|
|
299
334
|
component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
|
|
300
335
|
must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
|
|
301
336
|
non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
|
|
302
|
-
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindingIds.join(", ")}`
|
|
337
|
+
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindingIds.join(", ")}`
|
|
303
338
|
},
|
|
304
339
|
requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
|
|
305
340
|
acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
|
|
306
341
|
// A CODE-REVIEW repair changes delivered code/evidence, never the
|
|
307
342
|
// already accepted PLAN or its ownership declaration.
|
|
308
343
|
document_updates: [],
|
|
309
|
-
required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...packets.flatMap((value) => value.required_read)]),
|
|
344
|
+
required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...(semanticRepair && input.verificationPath ? [input.verificationPath] : []), ...packets.flatMap((value) => value.required_read)]),
|
|
310
345
|
discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
|
|
311
346
|
// These are collision-avoidance hints. Receipt paths enrich the coordinator
|
|
312
347
|
// picture but never restrict the repair's project-local edits.
|
|
@@ -314,7 +349,7 @@ export function addVnextCodeRepair(context, input) {
|
|
|
314
349
|
// Receipts record the resolved shell command. A repair must retain the
|
|
315
350
|
// accepted declaration (including its immutable @check alias) and only
|
|
316
351
|
// change when it runs, so work finish can validate it again.
|
|
317
|
-
checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}) }),
|
|
352
|
+
checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) }),
|
|
318
353
|
provides_checks: [],
|
|
319
354
|
stop_conditions: unique([
|
|
320
355
|
...invariantPackets.flatMap((value) => value.stop_conditions),
|
|
@@ -339,16 +374,18 @@ export function addVnextCodeRepair(context, input) {
|
|
|
339
374
|
type: "code_repair_created",
|
|
340
375
|
work_id: id,
|
|
341
376
|
origin_work_ids: input.originWorkIds,
|
|
342
|
-
...(receipt ? { check_receipt_id: receipt.id } : { review_finding_ids: input.reviewFindingIds })
|
|
377
|
+
...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindingIds?.length ? { review_finding_ids: input.reviewFindingIds } : { semantic_unresolved: input.semanticUnresolved })
|
|
343
378
|
});
|
|
344
379
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
345
380
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
346
381
|
}
|
|
347
|
-
/**
|
|
382
|
+
/** Retain the causal declaration for worker context; run_at keeps aggregate gates at stage scope. */
|
|
348
383
|
export function selectRepairChecks(input) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
384
|
+
// Gate placement belongs to the accepted PLAN. `runCodeChecks` executes only
|
|
385
|
+
// work-scoped entries, while retaining the aggregate declaration tells the
|
|
386
|
+
// worker exactly what will be rerun by the coordinator.
|
|
387
|
+
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
388
|
+
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
352
389
|
return uniqueBy(checks, (check) => check.id);
|
|
353
390
|
}
|
|
354
391
|
function receiptRepairPaths(workspaceRoot, receipt) {
|