@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.123
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 +8 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +52 -6
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +21 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +5 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +15 -4
- package/dist/harness-runtime/lib/managed-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/services/codex-capacity.js +131 -12
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +27 -9
- package/dist/services/merge-server.js +26 -9
- package/dist/services/review-copy.js +21 -0
- package/dist/services/run-controller-adapter.js +25 -3
- package/dist/services/run-controller-recovery.js +9 -2
- package/dist/services/run-controller.js +59 -20
- package/dist/services/run-observations.js +7 -2
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +27 -7
- package/dist/services/vnext-code-review.js +6 -4
- package/dist/services/vnext-plan-review.js +7 -2
- package/dist/services/work-registry.js +2 -0
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@ import { requireRun, requireController, controllerReceipt, appendEvent, saveStat
|
|
|
30
30
|
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
31
31
|
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady } from "./run-controller-process.js";
|
|
32
32
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
33
|
-
import { continueCodexCapacity } from "./codex-capacity.js";
|
|
33
|
+
import { capacityDelay, continueCodexCapacity } from "./codex-capacity.js";
|
|
34
34
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
35
35
|
import { captureControllerBoundary } from "./run-controller-capture.js";
|
|
36
36
|
import { requestRunControl } from "./run-control-worker.js";
|
|
@@ -534,8 +534,15 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
534
534
|
await reconcilePromptPredecessor(session);
|
|
535
535
|
const firstOperationId = operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
536
536
|
const send = (value, id) => {
|
|
537
|
+
const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
|
|
538
|
+
if (retained) {
|
|
539
|
+
const dispatch = JSON.parse(retained.input_json);
|
|
540
|
+
if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
|
|
541
|
+
throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
|
|
542
|
+
return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal);
|
|
543
|
+
}
|
|
537
544
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
538
|
-
fs.writeFileSync(file,
|
|
545
|
+
fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
|
|
539
546
|
state.turns += 1;
|
|
540
547
|
saveState(context, row, state);
|
|
541
548
|
return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal);
|
|
@@ -553,24 +560,45 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
553
560
|
throw error;
|
|
554
561
|
const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
|
|
555
562
|
args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
|
|
556
|
-
const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId,
|
|
563
|
+
const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true,
|
|
564
|
+
retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ? AND operation_id IN (?, ?)", [row.controller_id, row.generation, firstOperationId, `${firstOperationId}:capacity:1:session.prompt`])
|
|
565
|
+
.flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
|
|
557
566
|
inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
|
|
558
|
-
inspectSession:
|
|
567
|
+
inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
|
|
559
568
|
assertCurrent: () => {
|
|
560
569
|
assertOwner();
|
|
570
|
+
},
|
|
571
|
+
decision: async () => {
|
|
572
|
+
if (kind === "child") {
|
|
573
|
+
const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
|
|
574
|
+
if (!target)
|
|
575
|
+
return "reconciliation_blocked";
|
|
576
|
+
const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
|
|
577
|
+
const child = controllerNativeChildren(seen, session.id).find(item => item.session_id === target[0]);
|
|
578
|
+
if (child?.parent_session_id === session.id && (child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id))
|
|
579
|
+
return "completed";
|
|
580
|
+
}
|
|
561
581
|
const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
562
582
|
const next = view.continuation;
|
|
583
|
+
if (next.kind === "blocked")
|
|
584
|
+
return "reconciliation_blocked";
|
|
585
|
+
if (next.kind === "paused" && next.stage !== state.stage)
|
|
586
|
+
return "paused";
|
|
587
|
+
if (next.kind === "terminal" || next.stage !== state.stage)
|
|
588
|
+
return "completed";
|
|
563
589
|
if (hitl) {
|
|
564
590
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
565
591
|
const answer = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status = 'accepted'", [answerId, row.controller_id]);
|
|
566
592
|
const accepted = answer && JSON.parse(answer.input_json);
|
|
567
593
|
const pause = view.index.stage_runs.find(item => item.stage === accepted?.stage)?.pause;
|
|
568
|
-
if (next.kind !== "paused"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
throw new AppError("capacity_continuation_blocked", "RUN changed while native capacity was unavailable", 1, { continuation: next });
|
|
594
|
+
if (next.kind !== "paused")
|
|
595
|
+
return "continue";
|
|
596
|
+
if (!accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
|
|
597
|
+
return "paused";
|
|
573
598
|
}
|
|
599
|
+
else if (next.kind === "paused")
|
|
600
|
+
return "paused";
|
|
601
|
+
return next.kind === "wait" ? "reconciliation_blocked" : "continue";
|
|
574
602
|
},
|
|
575
603
|
retainIntent: intent => context.db.writeTransaction(() => {
|
|
576
604
|
const changed = context.db.run("UPDATE run_controller_operations SET status = 'failed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
|
|
@@ -579,7 +607,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
579
607
|
appendEvent(context, row.controller_id, "capacity_continuation", intent);
|
|
580
608
|
}),
|
|
581
609
|
prompt: (id, value) => send(value, id),
|
|
582
|
-
...(resumeCommand ? { continuationText: `
|
|
610
|
+
...(resumeCommand ? { continuationText: `Continue the same accepted HITL task in this Session. First inspect current durable RUN and pause state. Only if the exact pause still awaits the accepted answer, run this previously issued command once:\n\n${resumeCommand}\n\nNever replay an already committed answer. Otherwise use only the current issued successor.` } : {}) });
|
|
583
611
|
if (!resumed)
|
|
584
612
|
throw error;
|
|
585
613
|
receipt = resumed;
|
|
@@ -773,18 +801,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
773
801
|
const prior = child && state.child_capacity?.[child.session_id];
|
|
774
802
|
const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]) : [];
|
|
775
803
|
if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
|
|
776
|
-
&& child.native_turn_items?.observed === true && child.native_turn_items.possible_effects === false
|
|
804
|
+
&& child.native_turn_items?.observed === true && (child.native_turn_items.possible_effects === false || child.native_turn_items.pending === false)
|
|
777
805
|
&& inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
|
|
778
|
-
&& (!links.length || links[0].status === "running") && (prior?.attempts ?? 0) < 2) {
|
|
779
|
-
|
|
806
|
+
&& (!links.length || links[0].status === "running") && ((prior?.attempts ?? 0) < 2 || prior?.turn_id === turnId && prior.dispatched === false)) {
|
|
807
|
+
const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
|
|
808
|
+
if (prior?.turn_id === turnId && !prepared)
|
|
780
809
|
throw new AppError("capacity_child_followup_unconfirmed", "Parent continuation did not start a new native child Turn", 1, { child_session_id: child.session_id, turn_id: turnId });
|
|
781
|
-
const attempt = (prior?.attempts ?? 0) + 1;
|
|
810
|
+
const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
|
|
811
|
+
const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
|
|
812
|
+
if (!Number.isFinite(wait) || wait > 30_000)
|
|
813
|
+
throw new AppError("capacity_child_retry_delay_exceeded", "Native child retry delay exceeds the bounded continuation window", 1, { child_session_id: child.session_id, delay_ms: wait });
|
|
814
|
+
const operationId = prepared?.operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
|
|
782
815
|
state.child_capacity ??= {};
|
|
783
|
-
state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt };
|
|
816
|
+
state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
|
|
784
817
|
saveState(context, row, state);
|
|
785
|
-
const operationId = `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
|
|
786
818
|
appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId });
|
|
787
|
-
await delay(
|
|
819
|
+
await delay(wait, undefined, { signal: cancellation.signal });
|
|
788
820
|
assertOwner();
|
|
789
821
|
const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
790
822
|
if (current.continuation.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(current.continuation.kind))
|
|
@@ -792,13 +824,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
792
824
|
const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
793
825
|
const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
|
|
794
826
|
const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
|
|
827
|
+
if (still?.status === "completed" && currentLinks.length <= 1 && currentLinks.every(link => link.status === "completed")) {
|
|
828
|
+
state.last_receipt = fresh;
|
|
829
|
+
saveState(context, row, state);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
795
832
|
if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
|
|
796
|
-
|| still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false || currentLinks.length > 1
|
|
833
|
+
|| still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false && still.native_turn_items.pending !== false || currentLinks.length > 1
|
|
797
834
|
|| currentLinks.some(link => link.status !== "running"))
|
|
798
835
|
throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
|
|
799
836
|
assertOwner();
|
|
800
|
-
const text = `The existing native child ${child.session_id} has a confirmed
|
|
837
|
+
const text = `The existing native child ${child.session_id} has a confirmed terminal overloaded Turn. Use the native followup_task tool exactly once for that same child ID with this short message: "Continue your assigned Work from its current durable state. Do not repeat an observed lifecycle command. Finish only the original Work." Do not spawn another child, claim another Work, or perform the child's Work in this parent Turn. End this Turn after the followup_task response.`;
|
|
801
838
|
await prompt(session, text, operationId, "child");
|
|
839
|
+
state.child_capacity[child.session_id].dispatched = true;
|
|
840
|
+
saveState(context, row, state);
|
|
802
841
|
const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
803
842
|
state.last_receipt = after;
|
|
804
843
|
saveState(context, row, state);
|
|
@@ -829,7 +868,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
829
868
|
const results = await Promise.allSettled(fanout.works.map(async (work) => {
|
|
830
869
|
try {
|
|
831
870
|
assertOwner();
|
|
832
|
-
const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage });
|
|
871
|
+
const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
|
|
833
872
|
if (fatal)
|
|
834
873
|
throw fatal;
|
|
835
874
|
appendEvent(context, row.controller_id, "external_work_observed", { work_id: work.work_id, receipt });
|
|
@@ -90,7 +90,7 @@ export function runObservations(context, projectId, runId) {
|
|
|
90
90
|
const actual = fs.realpathSync(file);
|
|
91
91
|
if (!inside(home, actual))
|
|
92
92
|
throw new Error("journal escaped owned home");
|
|
93
|
-
fd = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
93
|
+
fd = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
94
94
|
if (!fs.fstatSync(fd).isFile())
|
|
95
95
|
throw new Error("journal is not a regular file");
|
|
96
96
|
replayToolJournal(file, { harness: source.session.harness_id, rootSessionId: sessionId, text: fs.readFileSync(fd, "utf8"), ...(source.session.harness_id === "codex-desktop" ? { expectedSessionIds: expectedCodexIds } : {}) }, reducer);
|
|
@@ -114,7 +114,9 @@ export function runObservations(context, projectId, runId) {
|
|
|
114
114
|
// Select transcript fallback only after replay reveals an actual gap.
|
|
115
115
|
for (const session of sessions) {
|
|
116
116
|
const nativeId = session.provider_session_id ?? session.session_id;
|
|
117
|
-
|
|
117
|
+
const sessionSummary = reducer.summary(nativeId);
|
|
118
|
+
if (!session.transcript_path || covered().has(JSON.stringify({ harness_id: session.harness, session_id: nativeId }))
|
|
119
|
+
&& sessionSummary.completeness === "complete" && sessionSummary.outcome_completeness === "complete")
|
|
118
120
|
continue;
|
|
119
121
|
const id = `transcript:${session.harness}:${nativeId}`;
|
|
120
122
|
add(id, nativeId, session.transcript_path, home, "current", session.harness);
|
|
@@ -136,6 +138,9 @@ export function runObservations(context, projectId, runId) {
|
|
|
136
138
|
if (!expectedIds.has(JSON.stringify(event.session))) {
|
|
137
139
|
reducer.turns.delete(key);
|
|
138
140
|
}
|
|
141
|
+
for (const [key, event] of reducer.sources)
|
|
142
|
+
if (!expectedIds.has(JSON.stringify(event.session)))
|
|
143
|
+
reducer.sources.delete(key);
|
|
139
144
|
const summary = reducer.summary();
|
|
140
145
|
const observed = new Set(summary.sessions.map(s => JSON.stringify(s.session)));
|
|
141
146
|
const missing = expected.filter(s => !observed.has(JSON.stringify(s)));
|
|
@@ -39,6 +39,7 @@ export function acceptStagePacket(context, input) {
|
|
|
39
39
|
context.db.afterCommit(() => publishStagePacket(context, input.projectId, input.runId, input.stage, input.prepared.file));
|
|
40
40
|
}
|
|
41
41
|
export function publishStagePacket(context, projectId, runId, stage, file) {
|
|
42
|
+
file = resolveStagePublicationPath(context, projectId, runId, file);
|
|
42
43
|
assertPublicationPath(context, projectId, runId, file);
|
|
43
44
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
44
45
|
withProjectionLock(`${file}.publish-lock`, () => {
|
|
@@ -47,7 +48,7 @@ export function publishStagePacket(context, projectId, runId, stage, file) {
|
|
|
47
48
|
const index = run ? JSON.parse(run.index_json) : null;
|
|
48
49
|
const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
|
|
49
50
|
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
|
|
50
|
-
const packet = rows.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && item.file === file);
|
|
51
|
+
const packet = rows.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && resolveStagePublicationPath(context, projectId, runId, item.file) === file);
|
|
51
52
|
if (!packet)
|
|
52
53
|
return;
|
|
53
54
|
const bytes = Buffer.from(packet.base64, "base64");
|
|
@@ -78,6 +79,7 @@ export function acceptStageReport(context, input) {
|
|
|
78
79
|
}
|
|
79
80
|
/** Replays the latest committed receipt, never an obsolete attempt. */
|
|
80
81
|
export function publishStageReport(context, projectId, runId, stage, root) {
|
|
82
|
+
root = resolveStagePublicationPath(context, projectId, runId, root);
|
|
81
83
|
assertPublicationPath(context, projectId, runId, root);
|
|
82
84
|
withProjectionLock(`${root}.report-publish-lock`, () => {
|
|
83
85
|
assertPublicationPath(context, projectId, runId, root);
|
|
@@ -85,7 +87,7 @@ export function publishStageReport(context, projectId, runId, stage, root) {
|
|
|
85
87
|
const index = run ? JSON.parse(run.index_json) : null;
|
|
86
88
|
const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
|
|
87
89
|
const events = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [projectId]);
|
|
88
|
-
const receipt = events.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.
|
|
90
|
+
const receipt = events.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && resolveStagePublicationPath(context, projectId, runId, item.root) === root);
|
|
89
91
|
if (!receipt)
|
|
90
92
|
return;
|
|
91
93
|
fs.mkdirSync(root, { recursive: true });
|
|
@@ -104,6 +106,27 @@ export function publishStageReport(context, projectId, runId, stage, root) {
|
|
|
104
106
|
}
|
|
105
107
|
});
|
|
106
108
|
}
|
|
109
|
+
/** Operational destinations follow validated imports, while audit bytes retain
|
|
110
|
+
* their original locators. SQL-only so readers may also use it in settlement. */
|
|
111
|
+
export function resolveStagePublicationPath(context, projectId, runId, locator) {
|
|
112
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
113
|
+
if (!run?.run_root || !path.isAbsolute(locator))
|
|
114
|
+
throw new AppError("stage_packet_invalid", "Stage publication has no absolute bound RUN locator", 1, { run_id: runId, locator });
|
|
115
|
+
let destination = path.resolve(locator);
|
|
116
|
+
const mappings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.publication_relocation' ORDER BY id", [projectId]);
|
|
117
|
+
for (const row of mappings) {
|
|
118
|
+
const mapping = JSON.parse(row.payload_json);
|
|
119
|
+
if (mapping.run_id !== runId || !path.isAbsolute(mapping.from) || !path.isAbsolute(mapping.to))
|
|
120
|
+
continue;
|
|
121
|
+
const relative = path.relative(mapping.from, destination);
|
|
122
|
+
if (relative && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
123
|
+
destination = path.resolve(mapping.to, relative);
|
|
124
|
+
}
|
|
125
|
+
const relative = path.relative(run.run_root, destination);
|
|
126
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
127
|
+
throw new AppError("publication_pending", "Stage publication escaped its bound RUN lineage", 1, { effect: "committed", run_id: runId, locator });
|
|
128
|
+
return destination;
|
|
129
|
+
}
|
|
107
130
|
/** Recheck the actual destination after preparation: a missing leaf may have
|
|
108
131
|
* an existing symlink ancestor, and retained packet bytes never authorize it. */
|
|
109
132
|
function assertPublicationPath(context, projectId, runId, file) {
|
|
@@ -4,13 +4,18 @@ import path from "node:path";
|
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
6
6
|
import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations } from "./code-checks.js";
|
|
7
|
-
import { acceptStagePacket, prepareStagePacket, publishStagePacket } from "./stage-report-renderer.js";
|
|
7
|
+
import { acceptStagePacket, prepareStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
8
8
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
9
9
|
export const verificationMatrixContract = "dd-flow/verification-matrix@1";
|
|
10
10
|
/** A view of already validated facts: never a gate or a receipt selector. */
|
|
11
11
|
export function buildVerificationMatrix(input) {
|
|
12
12
|
const sourceCatalog = new Map();
|
|
13
|
+
const pathChecksums = new Map();
|
|
13
14
|
for (const source of input.sources) {
|
|
15
|
+
const location = JSON.stringify([source.root, source.path]);
|
|
16
|
+
if (pathChecksums.has(location) && pathChecksums.get(location) !== source.sha256)
|
|
17
|
+
throw new AppError("verification_matrix_source_changed", "Matrix source aliases have conflicting checksums", 1, { root: source.root, path: source.path });
|
|
18
|
+
pathChecksums.set(location, source.sha256);
|
|
14
19
|
const identity = JSON.stringify([source.role, source.root, source.path]);
|
|
15
20
|
const prior = sourceCatalog.get(identity);
|
|
16
21
|
if (prior && prior.sha256 !== source.sha256)
|
|
@@ -105,9 +110,24 @@ export function prepareRunVerificationMatrix(context, input) {
|
|
|
105
110
|
const ownership = Object.fromEntries(protocols.map((protocol, i) => [protocol, protocolize.obligation_ownership.filter(item => item.member_keys.includes(protocolize.delivery.members[i].key)).map(item => item.obligation_id)]));
|
|
106
111
|
const skeleton = input.stage === "plan" && input.role === "input";
|
|
107
112
|
const plans = [];
|
|
108
|
-
const
|
|
109
|
-
const
|
|
113
|
+
const reviewSkipped = index.settings?.code_review?.mode === "off" && Boolean(index.settings.code_review.reason?.trim()) && !index.stage_runs?.some(item => item.stage === "code-review");
|
|
114
|
+
const predecessor = input.stage === "plan-review" ? "plan" : input.stage === "code" ? "plan-review" : input.stage === "code-review" ? "code" : input.stage === "merge" ? reviewSkipped ? "code" : "code-review" : null;
|
|
115
|
+
const priorMatrix = input.role === "input" && predecessor ? latestRunVerificationMatrix(context, input.projectId, input.runId, predecessor, "output") : null;
|
|
116
|
+
if (input.role === "input" && predecessor && !priorMatrix)
|
|
117
|
+
throw new AppError("verification_matrix_packet_missing", "Marked Stage requires its accepted predecessor output matrix", 1, { stage: input.stage, predecessor });
|
|
110
118
|
if (!skeleton) {
|
|
119
|
+
const batchPath = "03-plan/code-work-batch.json", batchFile = safePath(home, batchPath);
|
|
120
|
+
if (!fs.existsSync(batchFile))
|
|
121
|
+
throw new AppError("verification_matrix_source_missing", "Marked matrix requires its generated CODE batch", 1, { path: batchPath });
|
|
122
|
+
const batchBytes = fs.readFileSync(batchFile), batchSha = digest(batchBytes);
|
|
123
|
+
const acceptedBatch = priorMatrix?.matrix.sources.find(source => source.role === "code_work_batch");
|
|
124
|
+
if (acceptedBatch && acceptedBatch.sha256 !== batchSha)
|
|
125
|
+
throw new AppError("verification_matrix_source_changed", "CODE batch differs from accepted predecessor matrix", 1, { path: batchPath, expected: acceptedBatch.sha256, current: batchSha });
|
|
126
|
+
const batchSnapshot = `${vnextStageDirectory(input.stage)}/verification/sources/${batchSha}/code-work-batch.json`;
|
|
127
|
+
packets.push(prepareStagePacket(safePath(home, batchSnapshot), batchBytes));
|
|
128
|
+
sources.push({ role: "code_work_batch", root: "run", path: batchSnapshot, sha256: batchSha });
|
|
129
|
+
if (input.stage === "merge" && input.role === "output")
|
|
130
|
+
sources.push({ role: "code_work_batch_current", root: "run", path: batchPath, sha256: batchSha });
|
|
111
131
|
for (const protocol of protocols) {
|
|
112
132
|
const relative = `.memory-bank/protocol/${protocol}/plan.json`, file = safePath(workspace, relative);
|
|
113
133
|
// PLAN bytes are retained before MERGE cleanup; later recovery does not
|
|
@@ -266,9 +286,9 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
|
|
|
266
286
|
continue;
|
|
267
287
|
if (matrix.schema_id !== verificationMatrixContract || matrix.header.run_id !== runId || matrix.header.stage !== stage || matrix.header.stage_attempt !== attempt)
|
|
268
288
|
throw new AppError("stage_packet_invalid", "Retained matrix owner differs from its packet", 1);
|
|
269
|
-
|
|
289
|
+
packet.file = resolveStagePublicationPath(context, projectId, runId, packet.file);
|
|
270
290
|
const mdFile = packet.file.replace(/\.json$/, ".md");
|
|
271
|
-
const paired = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && item.file === mdFile);
|
|
291
|
+
const paired = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && resolveStagePublicationPath(context, projectId, runId, item.file) === mdFile);
|
|
272
292
|
if (!paired)
|
|
273
293
|
throw new AppError("stage_packet_invalid", "Retained matrix lacks its exact Markdown packet", 1);
|
|
274
294
|
const mdBytes = Buffer.from(paired.base64, "base64");
|
|
@@ -277,9 +297,9 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
|
|
|
277
297
|
const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
|
|
278
298
|
const json = { file: packet.file, bytes, sha256: packet.sha256 };
|
|
279
299
|
const metadata = { contract: verificationMatrixContract, role: matrix.header.role, completeness: matrix.header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(run.run_root, packet.file)), sha256: packet.sha256 }, markdown: { path: slash(path.relative(run.run_root, mdFile)), sha256: md.sha256 } };
|
|
280
|
-
const sourcePackets = matrix.sources.filter(source => source.role.startsWith("plan:") && source.root === "run").map(source => {
|
|
300
|
+
const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch") && source.root === "run").map(source => {
|
|
281
301
|
const file = safePath(run.run_root, source.path, false);
|
|
282
|
-
const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && item.file === file && item.sha256 === source.sha256);
|
|
302
|
+
const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && resolveStagePublicationPath(context, projectId, runId, item.file) === file && item.sha256 === source.sha256);
|
|
283
303
|
if (!retained)
|
|
284
304
|
throw new AppError("stage_packet_invalid", "Matrix PLAN source lacks retained bytes", 1, { path: source.path });
|
|
285
305
|
const bytes = Buffer.from(retained.base64, "base64");
|
|
@@ -25,6 +25,7 @@ import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
|
25
25
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
26
26
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
27
27
|
import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
|
|
28
|
+
import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
|
|
28
29
|
const stage = "code-review";
|
|
29
30
|
const stageDir = vnextStageDirectory(stage);
|
|
30
31
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -56,14 +57,15 @@ export function prepareVnextCodeReviewStart(context, input) {
|
|
|
56
57
|
const cycle = reviewCycle(run);
|
|
57
58
|
const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
|
|
58
59
|
const groups = mode.mode === "off" ? [] : reviewGroups(home);
|
|
59
|
-
const
|
|
60
|
+
const reviewerInputs = mode.mode === "off" ? [] : reviewerSourceInputs(run.workspace_root, groups.flatMap(group => group.aspect_ids));
|
|
61
|
+
const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: `${reviewerTask(home, group)}\n${reviewerSourceInstruction(run.workspace_root, group.aspect_ids)}`, launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
|
|
60
62
|
const items = mode.mode === "off" ? [] : prepareWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch });
|
|
61
63
|
const template = mode.mode === "off" ? "" : read(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md"));
|
|
62
64
|
const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
|
|
63
65
|
for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
|
|
64
66
|
prepareOutputFile(path.join(root, name));
|
|
65
67
|
const matrix = mode.mode === "off" ? null : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", stageAttempt: attachment.attempt });
|
|
66
|
-
return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks, matrix };
|
|
68
|
+
return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks, matrix, reviewerInputs };
|
|
67
69
|
}
|
|
68
70
|
export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
|
|
69
71
|
const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
|
|
@@ -88,7 +90,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
88
90
|
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
89
91
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
90
92
|
const reviewContextPath = path.join(root, "review-context.json");
|
|
91
|
-
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
|
|
93
|
+
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", reviewer_inputs: prepared.reviewerInputs, cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
|
|
92
94
|
if (mode.mode === "off") {
|
|
93
95
|
fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
|
|
94
96
|
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${stageLifecycleInstruction}\n`;
|
|
@@ -352,7 +354,7 @@ export function assertCodeReviewCoverage(groups, reviewers) {
|
|
|
352
354
|
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
353
355
|
}
|
|
354
356
|
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.`; }
|
|
355
|
-
function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); 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
|
|
357
|
+
function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); 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 passed its declared checks; each check proves only its named contour and proof limits, not complete semantic acceptance. Conduct independent quality review and do not manually rerun every declared check.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${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. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or 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"); }
|
|
356
358
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
357
359
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
358
360
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -23,6 +23,7 @@ import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writ
|
|
|
23
23
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
24
|
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
25
25
|
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
26
|
+
import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
|
|
26
27
|
const stage = "plan-review";
|
|
27
28
|
const stageDir = vnextStageDirectory(stage);
|
|
28
29
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -55,6 +56,10 @@ export function prepareVnextPlanReviewStart(context, input) {
|
|
|
55
56
|
assertReviewWorkspace(run.workspace_root, protocolIds);
|
|
56
57
|
const groups = reviewGroups(home, run.workspace_root);
|
|
57
58
|
const effective = resolveMode(requested, groups, home, run.workspace_root);
|
|
59
|
+
const reviewerInputs = effective === "off" ? [] : reviewerSourceInputs(run.workspace_root, groups.flatMap(group => group.aspect_ids));
|
|
60
|
+
if (effective !== "off")
|
|
61
|
+
for (const group of groups)
|
|
62
|
+
group.task += `\n${reviewerSourceInstruction(run.workspace_root, group.aspect_ids)}`;
|
|
58
63
|
const existing = context.db.get("SELECT work_id, status FROM works WHERE project_id = ? AND run_id = ? AND task = ? ORDER BY created_at DESC LIMIT 1", [project.id, run.id, reviewTask]);
|
|
59
64
|
const batchChecksum = checksum(batch), planChecksum = planSetChecksum(home, run.workspace_root);
|
|
60
65
|
if (input.externalContext)
|
|
@@ -62,7 +67,7 @@ export function prepareVnextPlanReviewStart(context, input) {
|
|
|
62
67
|
prepareOutputFile(path.join(root, "stage-prompt.md"));
|
|
63
68
|
const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
64
69
|
const matrix = existing?.status === "running" ? latestRunVerificationMatrix(context, project.id, run.id, stage, "input") : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
|
|
65
|
-
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix };
|
|
70
|
+
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix, reviewerInputs };
|
|
66
71
|
if (existing?.status === "running") {
|
|
67
72
|
const orchestration = readFanoutDescriptor(root);
|
|
68
73
|
if (!orchestration)
|
|
@@ -163,7 +168,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
163
168
|
const workSessionId = String(binding.work_session_id);
|
|
164
169
|
const sessionId = String(binding.session_id);
|
|
165
170
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
166
|
-
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
171
|
+
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
167
172
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
168
173
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
169
174
|
return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
@@ -995,6 +995,8 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
|
|
|
995
995
|
add("run", matrix.metadata.json.path, matrix.metadata.json.sha256);
|
|
996
996
|
add("run", matrix.metadata.markdown.path, matrix.metadata.markdown.sha256);
|
|
997
997
|
}
|
|
998
|
+
for (const item of value.reviewer_inputs ?? [])
|
|
999
|
+
add("workspace", item.path, item.sha256);
|
|
998
1000
|
const runInputs = [...expected].map(([key, sha]) => { const [root, ...parts] = key.split(":"); return { root: root, path: parts.join(":"), sha256: sha }; }).sort((a, b) => `${a.root}:${a.path}`.localeCompare(`${b.root}:${b.path}`));
|
|
999
1001
|
for (const item of runInputs) {
|
|
1000
1002
|
const target = reviewInputPath(item.root === "run" ? home : run.workspace_root, item.path);
|