@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/help.js +3 -3
  4. package/dist/cli/run-cli.js +127 -8
  5. package/dist/runtime/context.js +3 -1
  6. package/dist/schemas/code-review-result.schema.json +1 -1
  7. package/dist/schemas/code-work-batch.schema.json +4 -3
  8. package/dist/schemas/code-work-result.schema.json +1 -1
  9. package/dist/schemas/harness-config.schema.json +23 -0
  10. package/dist/schemas/plan-review-decision.schema.json +1 -1
  11. package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
  12. package/dist/services/cleanup.js +18 -8
  13. package/dist/services/code-checks.js +194 -44
  14. package/dist/services/engines.js +4 -4
  15. package/dist/services/eval-snapshots.js +10 -5
  16. package/dist/services/harness-config.js +66 -0
  17. package/dist/services/hooks.js +62 -28
  18. package/dist/services/lanes.js +1 -0
  19. package/dist/services/managed-processes.js +169 -0
  20. package/dist/services/merge-server.js +8 -2
  21. package/dist/services/portable-refs.js +57 -0
  22. package/dist/services/prompts.js +4 -2
  23. package/dist/services/run-engine-bindings.js +19 -61
  24. package/dist/services/run-projection.js +10 -8
  25. package/dist/services/runs.js +71 -9
  26. package/dist/services/schema-validation.js +11 -11
  27. package/dist/services/session-identity.js +19 -0
  28. package/dist/services/sessions.js +26 -11
  29. package/dist/services/stage-lifecycle.js +15 -8
  30. package/dist/services/stage-pause.js +35 -20
  31. package/dist/services/usage.js +74 -42
  32. package/dist/services/vnext-code-review.js +82 -41
  33. package/dist/services/vnext-code.js +98 -34
  34. package/dist/services/vnext-fanout.js +5 -12
  35. package/dist/services/vnext-merge.js +144 -65
  36. package/dist/services/vnext-plan-review.js +50 -35
  37. package/dist/services/vnext-plan.js +69 -21
  38. package/dist/services/vnext-protocolize.js +6 -6
  39. package/dist/services/vnext-specify.js +6 -6
  40. package/dist/services/work-registry.js +150 -40
  41. package/dist/storage/database.js +128 -2
  42. package/package.json +1 -1
  43. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
@@ -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, provider_session_id: providerSessionId, harness: harness.harness };
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.session_id);
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
- context.db.exec("BEGIN IMMEDIATE");
158
- try {
159
- context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
160
- for (const row of rows) {
161
- 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)
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
- `USG-${crypto.randomUUID()}`, input.projectId, input.runId, row.session_id, usageFinal ? "final" : "provisional", input.stage?.name ?? null, row.observed_at,
164
- row.tokens?.total_tokens ?? null, row.tokens?.input_tokens ?? null, row.tokens?.cache_read_input_tokens ?? null,
165
- row.tokens?.cache_read_input_tokens ?? null, row.tokens?.cache_write_input_tokens ?? null, row.tokens?.uncached_input_tokens ?? null,
166
- 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,
167
- row.status, row.diagnostic, observedAt
168
- ]);
169
- }
170
- for (const session of settledSessions) {
171
- context.db.run(`UPDATE sessions
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, sessionId, windows, observedAt);
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.parent_session_id)
387
+ if (!link.provider_parent_session_id)
361
388
  return true;
362
- const parent = links.find((candidate) => candidate.session_id === link.parent_session_id);
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.session_id);
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.session_id, startedAt, endedAt);
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, sessionStartedAt, observedAt]);
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, sessionId);
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 deltas = usageDeltas(selectedRows);
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 deltas) {
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
- session_id: session.session_id, parent_session_id: session.parent_session_id ?? null, role: session.role ?? null,
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
- session_id: segment.session_id,
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.session_id);
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.session_id, segment.started_at, segment.ended_at ?? context.now());
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) {
@@ -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, writeProtocolFlowStatus } from "./vnext-code.js";
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(run, changedPaths(run.workspace_root));
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
- if (mode === "off") {
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(run, home, mode);
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
- reviewFindingIds: selected.map((finding) => finding.finding_ref),
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 mode = effectiveMode(run, changedPaths(run.workspace_root));
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 = repairs.length ? unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks }) : [];
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 = repairs.length ? 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 } : {}) }) : [];
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
- writeProtocolFlowStatus(run.workspace_root, home, run.id, nextAction === "start_merge" ? "CODE-REVIEW complete; MERGE is queued." : "CODE-REVIEW complete; this RUN reached its configured terminal boundary.");
178
- 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", "post_repair_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" } };
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(run, home, mode) {
193
- const aspectMapFiles = findFiles(path.join(home, "03-plan"), "aspect-map.json");
194
- const ids = new Set(baselineAspects);
195
- for (const file of aspectMapFiles) {
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
- ids.add(aspect.aspect_id);
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
- const all = [...ids];
202
- const chunk = mode === "deep" ? 1 : 3;
203
- return Array.from({ length: Math.ceil(all.length / chunk) }, (_, index) => ({ key: `group-${index + 1}`, aspect_ids: all.slice(index * chunk, (index + 1) * chunk) }));
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. Finding ids must use the unique prefix FIND-${group.key}-. Return dd-flow/code-review-result@1.`; }
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.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"); }
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 ref of Array.isArray(result.resolved_finding_refs) ? result.resolved_finding_refs.filter((value) => typeof value === "string") : [])
256
- repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
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(context, work, run) { if (!work.result)
282
- throw new AppError("review_evidence_invalid", "Reviewer Work has no result", 2, { work_id: work.work_id }); const file = path.join(requireHome(run), stageDir, `${work.work_id}.result.json`); fs.writeFileSync(file, work.result); validateSchema({ schemaName: "code-review-result", file, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id }); }
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 ids = repair.review_finding_ids; return Array.isArray(ids) ? ids.filter((value) => typeof value === "string") : []; }
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 refs = repair.review_check_refs; return Array.isArray(refs) ? refs.filter((value) => typeof value === "string") : []; }
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(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; }
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, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
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.run_home_path)
340
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
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 })) {