@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -14,6 +14,7 @@ import { appendFlowRunTimelineEvent, pauseFlowRunStage, resumeFlowRunStage } fro
|
|
|
14
14
|
import { bindSessionForResume, refreshRunWorkProjection } from "./work-registry.js";
|
|
15
15
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
16
16
|
import { runtimeCommand } from "./runtime-command.js";
|
|
17
|
+
import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
17
18
|
export { flowCommand } from "./runtime-command.js";
|
|
18
19
|
export function prepareStagePause(context, input) {
|
|
19
20
|
if (typeof input.question !== "string" || !input.question.trim())
|
|
@@ -38,6 +39,7 @@ export function pauseStageForUser(context, input) {
|
|
|
38
39
|
const questionPath = path.join(pauseRoot, "question.md");
|
|
39
40
|
fs.mkdirSync(pauseRoot, { recursive: true });
|
|
40
41
|
publishImmutableInput(questionPath, Buffer.from(question), "hitl_question_conflict");
|
|
42
|
+
const answerInputPath = prepareAgentOutputFile(path.join(pauseRoot, "answer-input.md"));
|
|
41
43
|
const now = context.now();
|
|
42
44
|
context.db.writeTransaction(() => {
|
|
43
45
|
const paused = context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
|
|
@@ -46,7 +48,6 @@ export function pauseStageForUser(context, input) {
|
|
|
46
48
|
pauseFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, pauseId, questionPath });
|
|
47
49
|
context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
|
|
48
50
|
});
|
|
49
|
-
const answerInputPath = path.join(pauseRoot, "answer-input.md");
|
|
50
51
|
const resume = stageResumeCommand(context, { runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, answerInputPath });
|
|
51
52
|
const resumeTemplate = `Write the complete user answer exactly to ${quote([answerInputPath])}, then run:\n${resume}`;
|
|
52
53
|
return {
|
|
@@ -146,7 +147,7 @@ export function resumeStageAfterUser(context, input) {
|
|
|
146
147
|
export function stagePauseCommand(context, input) {
|
|
147
148
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
148
149
|
const run = requireRun(context, project.id, input.runId);
|
|
149
|
-
const questionInputPath = path.join(requireRunHome(run), "works", input.workId, "question-input.md");
|
|
150
|
+
const questionInputPath = prepareAgentOutputFile(path.join(requireRunHome(run), "works", input.workId, "question-input.md"));
|
|
150
151
|
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
151
152
|
}
|
|
152
153
|
/** Render separate file-write and lifecycle-call instructions for HITL. */
|
package/dist/services/usage.js
CHANGED
|
@@ -369,7 +369,7 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
|
|
|
369
369
|
: completedTurn?.completed_at ?? sessionLinks.map((item) => item.completed_at ?? observedAt).sort().at(-1);
|
|
370
370
|
const endedAt = stage?.completedAt ?? (Date.parse(naturalEnd) > Date.parse(observedAt) ? observedAt : naturalEnd);
|
|
371
371
|
const harnessUsage = isExternalHarness(link.harness)
|
|
372
|
-
? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt)
|
|
372
|
+
? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt, Boolean(stage))
|
|
373
373
|
: null;
|
|
374
374
|
const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, nativeSessionId(link), startedAt, endedAt);
|
|
375
375
|
return {
|
|
@@ -393,18 +393,29 @@ function hasInclusiveUsageSnapshot(context, projectId, harness, providerSessionI
|
|
|
393
393
|
return false;
|
|
394
394
|
return Boolean(context.db.get("SELECT 1 FROM harness_usage_snapshots WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND usage_scope = 'execution_tree_inclusive' AND observed_at <= ? LIMIT 1", [projectId, harness, providerSessionId, observedAt]));
|
|
395
395
|
}
|
|
396
|
-
function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
|
|
396
|
+
function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt, stageWindow) {
|
|
397
397
|
if (!providerSessionId)
|
|
398
398
|
return { status: "not_observable", diagnostic: "provider_session_id_missing" };
|
|
399
399
|
const snapshots = context.db.all(`SELECT completeness, observed_at, total_tokens, input_tokens, cache_read_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens
|
|
400
400
|
FROM harness_usage_snapshots
|
|
401
401
|
WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND observed_at <= ?
|
|
402
402
|
ORDER BY observed_at, id`, [projectId, harness, providerSessionId, observedAt]);
|
|
403
|
-
const last = snapshots.find((snapshot) => Date.parse(snapshot.observed_at) > Date.parse(endedAt))
|
|
403
|
+
const last = (!stageWindow ? snapshots.find((snapshot) => Date.parse(snapshot.observed_at) > Date.parse(endedAt)) : undefined)
|
|
404
404
|
?? snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(endedAt)).at(-1);
|
|
405
405
|
const before = snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(startedAt)).at(-1);
|
|
406
406
|
if (!last || !before || last === before)
|
|
407
407
|
return { status: "not_yet_emitted", diagnostic: `${harness}_usage_window_incomplete` };
|
|
408
|
+
// Cumulative counters outside an attempt's boundaries cannot establish its
|
|
409
|
+
// spend: adjacent attempts would otherwise charge overlapping intervals.
|
|
410
|
+
if (stageWindow && (Date.parse(before.observed_at) !== Date.parse(startedAt)
|
|
411
|
+
|| Date.parse(last.observed_at) !== Date.parse(endedAt)
|
|
412
|
+
|| before.completeness !== "complete" || last.completeness !== "complete")) {
|
|
413
|
+
return { status: "not_observable", diagnostic: `${harness}_stage_window_unconfirmed` };
|
|
414
|
+
}
|
|
415
|
+
if (stageWindow && ["total_tokens", "input_tokens", "cache_read_input_tokens", "cache_write_input_tokens", "output_tokens", "reasoning_output_tokens"]
|
|
416
|
+
.some(key => last[key] !== null && before[key] !== null && last[key] < before[key])) {
|
|
417
|
+
return { status: "not_observable", diagnostic: `${harness}_stage_counter_reset` };
|
|
418
|
+
}
|
|
408
419
|
if (harness === "droid-cli" && (last.completeness !== "complete" || before.completeness !== "complete"))
|
|
409
420
|
return { status: "not_observable", diagnostic: "droid-cli_usage_window_partial" };
|
|
410
421
|
const delta = (value, baseline) => value === null || baseline === null ? null : Math.max(0, value - baseline);
|
|
@@ -7,6 +7,7 @@ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations }
|
|
|
7
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
|
+
export const finalCheckCoverageContract = "dd-flow/final-check-coverage@1";
|
|
10
11
|
/** A view of already validated facts: never a gate or a receipt selector. */
|
|
11
12
|
export function buildVerificationMatrix(input) {
|
|
12
13
|
const sourceCatalog = new Map();
|
|
@@ -178,6 +179,10 @@ export function prepareRunVerificationMatrix(context, input) {
|
|
|
178
179
|
let finalBindings;
|
|
179
180
|
if (input.stage === "merge") {
|
|
180
181
|
const gate = readSource("merge_gate", "run", "07-merge/merge-gate.json");
|
|
182
|
+
if (index.final_check_coverage_contract === finalCheckCoverageContract) {
|
|
183
|
+
for (const role of final ? ["baseline", "source", "target"] : ["baseline", "source"])
|
|
184
|
+
readSource(`check_profile_${role}`, "run", `07-merge/check-profile-${role}.json`);
|
|
185
|
+
}
|
|
181
186
|
const semanticRefs = new Set(plans.flatMap(plan => plan.acceptance.flatMap(entry => entry.check_refs.map(id => `${plan.protocol_id}/${id}`))));
|
|
182
187
|
policyChecks = gate.checks.filter(check => !semanticRefs.has(check.canonical_ref ?? check.id));
|
|
183
188
|
if (final) {
|
|
@@ -224,6 +229,20 @@ export function prepareRunVerificationMatrix(context, input) {
|
|
|
224
229
|
return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: "unavailable", reason: "receipt_file_missing" };
|
|
225
230
|
const bytes = fs.readFileSync(receiptFile);
|
|
226
231
|
sources.push({ role: `receipt:${ref}`, root: "run", path: portable.receipt_path, sha256: digest(bytes) });
|
|
232
|
+
const retainedReceipt = JSON.parse(bytes.toString("utf8"));
|
|
233
|
+
for (const artifact of retainedReceipt.artifacts ?? []) {
|
|
234
|
+
if (path.isAbsolute(artifact.path) || artifact.path.includes("\0") || artifact.path.split(/[\\/]/).includes(".."))
|
|
235
|
+
throw new AppError("verification_matrix_path_invalid", "Native artifact must remain inside its evidence directory", 1, { path: artifact.path });
|
|
236
|
+
const artifactPath = slash(path.join(path.dirname(portable.receipt_path), "artifacts", artifact.path));
|
|
237
|
+
const artifactFile = safePath(home, artifactPath);
|
|
238
|
+
if (fs.existsSync(artifactFile))
|
|
239
|
+
sources.push({ role: `artifact:${ref}:${artifact.path}`, root: "run", path: artifactPath, sha256: digest(fs.readFileSync(artifactFile)) });
|
|
240
|
+
}
|
|
241
|
+
if (receipt.status !== "aborted" && fs.existsSync(receipt.completion_path)) {
|
|
242
|
+
const completionPath = portable.completion_path;
|
|
243
|
+
const completionFile = safePath(home, completionPath);
|
|
244
|
+
sources.push({ role: `completion:${ref}`, root: "run", path: completionPath, sha256: digest(fs.readFileSync(completionFile)) });
|
|
245
|
+
}
|
|
227
246
|
return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: valid ? "retained" : "unavailable", ...(!valid ? { reason: "receipt_evidence_invalid" } : {}) };
|
|
228
247
|
});
|
|
229
248
|
const works = context.db.all("SELECT work_id, payload_json FROM works WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]).flatMap(work => {
|
|
@@ -238,6 +257,8 @@ export function prepareRunVerificationMatrix(context, input) {
|
|
|
238
257
|
const markdown = prepareStagePacket(safePath(home, `${root}/verification-matrix.md`), Buffer.from(renderVerificationMatrix(matrix)));
|
|
239
258
|
packets.push(json, markdown);
|
|
240
259
|
const metadata = { contract: verificationMatrixContract, role: header.role, completeness: header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(home, json.file)), sha256: json.sha256 }, markdown: { path: slash(path.relative(home, markdown.file)), sha256: markdown.sha256 } };
|
|
260
|
+
if (index.final_check_coverage_contract === finalCheckCoverageContract)
|
|
261
|
+
metadata.coverage_contract = finalCheckCoverageContract;
|
|
241
262
|
const predecessors = (index.stage_runs ?? []).filter(item => item.stage !== input.stage && item.status === "done").map(item => ({ stage: item.stage, ...(item.attempt ? { attempt: item.attempt } : {}), ...(item.data_sha256 ? { data_sha256: item.data_sha256 } : {}) }));
|
|
242
263
|
return { projectId: input.projectId, runId: input.runId, stage: input.stage, home, attempt, generation, packets, matrix, metadata, predecessors };
|
|
243
264
|
}
|
|
@@ -297,6 +318,8 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
|
|
|
297
318
|
const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
|
|
298
319
|
const json = { file: packet.file, bytes, sha256: packet.sha256 };
|
|
299
320
|
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 } };
|
|
321
|
+
if (index.final_check_coverage_contract === finalCheckCoverageContract)
|
|
322
|
+
metadata.coverage_contract = finalCheckCoverageContract;
|
|
300
323
|
const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch") && source.root === "run").map(source => {
|
|
301
324
|
const file = safePath(run.run_root, source.path, false);
|
|
302
325
|
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);
|
|
@@ -314,7 +337,13 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
|
|
|
314
337
|
}
|
|
315
338
|
function portableReceipt(receipt, home) {
|
|
316
339
|
const relative = (file) => slash(path.relative(home, safePath(home, path.relative(home, file))));
|
|
317
|
-
return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item =>
|
|
340
|
+
return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item => {
|
|
341
|
+
const artifactPath = path.isAbsolute(item.path) ? path.relative(path.join(path.dirname(receipt.receipt_path), "artifacts"), item.path) : item.path;
|
|
342
|
+
if (path.isAbsolute(artifactPath) || artifactPath.includes("\0") || artifactPath.split(/[\\/]/).includes(".."))
|
|
343
|
+
throw new AppError("verification_matrix_path_invalid", "Native artifact must remain inside its evidence directory", 1, { path: item.path });
|
|
344
|
+
safePath(home, path.join(path.dirname(relative(receipt.receipt_path)), "artifacts", artifactPath));
|
|
345
|
+
return { ...item, path: slash(artifactPath) };
|
|
346
|
+
}) };
|
|
318
347
|
}
|
|
319
348
|
function safePath(root, relative, inspectFilesystem = true) {
|
|
320
349
|
if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
|
|
@@ -18,7 +18,7 @@ import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
|
18
18
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
19
19
|
import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
|
|
20
20
|
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix, publishVerificationMatrix } from "./verification-matrix.js";
|
|
21
|
-
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
|
+
import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
22
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
23
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
24
|
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
@@ -26,6 +26,8 @@ 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
28
|
import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
|
|
29
|
+
import { reviewDefEvidence } from "./review-deferral.js";
|
|
30
|
+
import { stageSuccessor } from "./vnext-contracts.js";
|
|
29
31
|
const stage = "code-review";
|
|
30
32
|
const stageDir = vnextStageDirectory(stage);
|
|
31
33
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -75,11 +77,11 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
75
77
|
if (prepared.resumed) {
|
|
76
78
|
if (effectiveMode(prepared.home, run).mode !== "off")
|
|
77
79
|
resumeVerificationMatrix(context, project.id, run.id, stage);
|
|
78
|
-
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
80
|
+
const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
79
81
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
80
|
-
const
|
|
82
|
+
const delivered = currentStagePrompt(context, existingPrompt);
|
|
81
83
|
const orchestration = prepared.orchestration;
|
|
82
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id,
|
|
84
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, ...delivered, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
83
85
|
}
|
|
84
86
|
const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
|
|
85
87
|
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
|
|
@@ -95,17 +97,17 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
95
97
|
fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
|
|
96
98
|
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`;
|
|
97
99
|
fs.writeFileSync(promptPath, prompt);
|
|
98
|
-
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
100
|
+
const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
99
101
|
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 } : {}) });
|
|
100
102
|
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" };
|
|
101
103
|
attachFlowRunStage(context, attachmentInput, prepareFlowRunStageAttachment(context, attachmentInput));
|
|
102
|
-
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id,
|
|
104
|
+
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
103
105
|
}
|
|
104
106
|
const batchFile = path.join(root, "review-work-batch.json");
|
|
105
107
|
fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
|
|
106
108
|
const prompt = `${orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, prepared.home, "reviewing whether current evidence proves the accepted obligations")}\n`;
|
|
107
109
|
fs.writeFileSync(promptPath, prompt);
|
|
108
|
-
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
110
|
+
const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
109
111
|
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 } : {}) });
|
|
110
112
|
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
|
|
111
113
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
@@ -113,7 +115,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
113
115
|
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
114
116
|
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
115
117
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
116
|
-
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id,
|
|
118
|
+
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
117
119
|
}
|
|
118
120
|
export function prepareVnextCodeReviewRepair(context, input) {
|
|
119
121
|
if (!Array.isArray(input.findingIds) || !input.findingIds.length || input.findingIds.some(id => typeof id !== "string" || !id.trim()))
|
|
@@ -196,6 +198,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
196
198
|
validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
197
199
|
const cycle = reviewContext.cycle ?? "initial";
|
|
198
200
|
const reviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
201
|
+
const defEvidence = decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id));
|
|
199
202
|
if (mode !== "off") {
|
|
200
203
|
assertCodeReviewCoverage(reviewGroups(home), reviewers);
|
|
201
204
|
const incomplete = reviewers.filter((work) => work.status !== "completed");
|
|
@@ -204,6 +207,8 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
204
207
|
for (const work of reviewers)
|
|
205
208
|
validateReviewerResult(context, work, run);
|
|
206
209
|
const canonical = reviewDecisionForRepair(decision, reviewers);
|
|
210
|
+
if (reviewContext.accepted_decision && JSON.stringify(reviewContext.def_evidence ?? []) !== JSON.stringify(defEvidence))
|
|
211
|
+
throw new AppError("deferral_invalid", "Accepted DEF evidence changed during review repair", 2);
|
|
207
212
|
const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
|
|
208
213
|
let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
209
214
|
const frozenShaFile = path.join(root, ".decision-sha256");
|
|
@@ -215,7 +220,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
215
220
|
// Accept the value once. Repairs consume this decision, not a mutable
|
|
216
221
|
// operator file. Reuse the existing context artifact and review cycle.
|
|
217
222
|
const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
|
|
218
|
-
fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
|
|
223
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision, def_evidence: defEvidence }, null, 2)}\n`);
|
|
219
224
|
fs.renameSync(temporary, contextFile);
|
|
220
225
|
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(", ")}` });
|
|
221
226
|
const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, "input");
|
|
@@ -263,6 +268,9 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
263
268
|
const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
264
269
|
if (matrix)
|
|
265
270
|
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
271
|
+
if (JSON.stringify(defEvidence) !== JSON.stringify(decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id))))
|
|
272
|
+
throw new AppError("deferral_invalid", "DEF evidence changed while final checks ran", 2);
|
|
273
|
+
Object.assign(report.semantic, { def_evidence: defEvidence });
|
|
266
274
|
const preparedReport = prepareStageReport(report);
|
|
267
275
|
const completionInput = { 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", dataSha256: preparedReport.sha256, git: report.mechanical.git };
|
|
268
276
|
withStageSettlement(context, () => {
|
|
@@ -286,7 +294,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
286
294
|
}
|
|
287
295
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
288
296
|
});
|
|
289
|
-
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: {
|
|
297
|
+
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: stageSuccessor(context, { projectRoot, runId: run.id, stage: "merge", command: runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"]) }) } : {}) };
|
|
290
298
|
}
|
|
291
299
|
export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
|
|
292
300
|
if (!accepted)
|
|
@@ -356,7 +364,10 @@ export function assertCodeReviewCoverage(groups, reviewers) {
|
|
|
356
364
|
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.`; }
|
|
357
365
|
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"); }
|
|
358
366
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
359
|
-
const known = new
|
|
367
|
+
const known = new Map(canonicalCodeFindings(reviewers).map((item) => [item.finding_ref, item.finding]));
|
|
368
|
+
const refs = decision.findings.map(item => item.finding_ref);
|
|
369
|
+
if (new Set(refs).size !== refs.length || refs.some(ref => !known.has(ref)))
|
|
370
|
+
throw new AppError("review_evidence_invalid", "CODE decisions must use unique known canonical finding references", 2, { finding_refs: refs });
|
|
360
371
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
361
372
|
const duplicates = new Set();
|
|
362
373
|
for (const item of decision.findings.filter((entry) => entry.disposition === "duplicate")) {
|
|
@@ -372,6 +383,9 @@ function canonicalReviewEvidence(decision, reviewers) {
|
|
|
372
383
|
}
|
|
373
384
|
if (!cursor)
|
|
374
385
|
throw new AppError("review_evidence_invalid", "Duplicate finding chain must end at a decided canonical finding", 2, { finding_ref: item.finding_ref });
|
|
386
|
+
if (["p0", "p1"].includes(known.get(item.finding_ref).priority) && cursor.disposition !== "fix") {
|
|
387
|
+
throw new AppError("review_material_disposition_invalid", "P0/P1 duplicate findings must resolve to a fixed canonical finding", 2, { finding_ref: item.finding_ref, canonical_finding_ref: cursor.finding_ref, disposition: cursor.disposition });
|
|
388
|
+
}
|
|
375
389
|
duplicates.add(item.finding_ref);
|
|
376
390
|
}
|
|
377
391
|
return {
|
|
@@ -422,8 +436,7 @@ function validateDecision(context, input) {
|
|
|
422
436
|
if (item.disposition === "fix" && !repaired.some((work) => requiredChecks.every((ref) => reviewCheckRefs(work).includes(ref))))
|
|
423
437
|
throw new AppError("review_repair_check_missing", "A fixed CODE-REVIEW finding needs a repair Work that retains every selected causal check", 2, { finding_ref: findingRef, check_refs: requiredChecks });
|
|
424
438
|
if (finding.priority === "p2" && item.disposition === "defer") {
|
|
425
|
-
|
|
426
|
-
throw new AppError("deferral_invalid", "A deferred P2 requires a named durable DEF", 2, { finding_ref: findingRef, def_id: item.def_id ?? null });
|
|
439
|
+
reviewDefEvidence(input.workspaceRoot, item.def_id);
|
|
427
440
|
}
|
|
428
441
|
if (finding.priority === "p3" && item.disposition === "defer")
|
|
429
442
|
throw new AppError("deferral_invalid", "P3 observations are not deferred as DEF", 2, { finding_ref: findingRef });
|
|
@@ -4,6 +4,7 @@ import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
|
|
|
4
4
|
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
+
import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { AppError } from "../shared/errors.js";
|
|
9
10
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
@@ -21,11 +22,12 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
21
22
|
import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
|
|
22
23
|
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
23
24
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
24
|
-
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
25
|
+
import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
25
26
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
26
27
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
27
28
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
28
29
|
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, publishVerificationMatrix, latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
30
|
+
import { stageSuccessor } from "./vnext-contracts.js";
|
|
29
31
|
const stage = "code";
|
|
30
32
|
const stageDir = vnextStageDirectory("code");
|
|
31
33
|
/** A controller prepares slow, deterministic project tooling before it asks an
|
|
@@ -93,12 +95,13 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
93
95
|
const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
|
|
94
96
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
95
97
|
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
98
|
+
prepareAgentOutputFile(path.join(requireHome(run), "works", rootWork.work_id, "block-summary.md"));
|
|
96
99
|
if (prepared.resumed) {
|
|
97
100
|
resumeVerificationMatrix(context, project.id, run.id, stage);
|
|
98
|
-
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
101
|
+
const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
99
102
|
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
100
103
|
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
101
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage,
|
|
104
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
102
105
|
}
|
|
103
106
|
if (!sourceRepairAttempt)
|
|
104
107
|
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
|
|
@@ -108,7 +111,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
108
111
|
const graph = codeWorkGraph(context, project.id, run.id);
|
|
109
112
|
const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, prepared.home, "implementation and code-verification.json")}\n`;
|
|
110
113
|
fs.writeFileSync(promptPath, prompt);
|
|
111
|
-
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
114
|
+
const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
112
115
|
const binding = bindStageCoordinatorWork(context, {
|
|
113
116
|
workId: rootWork.work_id,
|
|
114
117
|
hookEventId: input.hookEventId,
|
|
@@ -139,8 +142,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
139
142
|
stage,
|
|
140
143
|
root_work_id: rootWork.work_id,
|
|
141
144
|
id: binding.work_session_id,
|
|
142
|
-
|
|
143
|
-
worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"),
|
|
145
|
+
...currentStagePrompt(context, promptPath),
|
|
144
146
|
...(externalContext ? { external_context: externalContext } : {}),
|
|
145
147
|
graph,
|
|
146
148
|
orchestration,
|
|
@@ -332,7 +334,7 @@ export async function finishVnextCode(context, input) {
|
|
|
332
334
|
outcome: "accepted",
|
|
333
335
|
report_path: path.join(root, "stage-report.json"),
|
|
334
336
|
next_action: next,
|
|
335
|
-
...(next === "start_code_review" ? { next: {
|
|
337
|
+
...(next === "start_code_review" ? { next: stageSuccessor(context, { projectRoot, runId: run.id, stage: "code-review", command: runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"]) }) } : next === "start_merge" ? { merge_request: mergeRequest, next: stageSuccessor(context, { projectRoot, runId: run.id, stage: "merge", command: runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"]) }) } : {})
|
|
336
338
|
};
|
|
337
339
|
}
|
|
338
340
|
export function registerAggregateCodeRepair(context, input) {
|
|
@@ -539,7 +541,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
|
|
|
539
541
|
}
|
|
540
542
|
return [...paths].sort();
|
|
541
543
|
}
|
|
542
|
-
function coordinatorPrompt(context, input) {
|
|
544
|
+
export function coordinatorPrompt(context, input) {
|
|
543
545
|
const { graph, capacity, template } = input;
|
|
544
546
|
return [
|
|
545
547
|
"<stage_identity>",
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { isControllerManagedInvocation, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
/** Successor authority belongs to the controller, not the finishing Session.
|
|
3
|
+
* Manual flows still need an executable next step. Current-stage finish and
|
|
4
|
+
* paused/child Work commands do not pass through this boundary. */
|
|
5
|
+
export function stageSuccessor(context, input) {
|
|
6
|
+
const next = { ...input.metadata, kind: "start_stage", stage: input.stage };
|
|
7
|
+
return isControllerManagedInvocation(context, input.projectRoot, input.runId)
|
|
8
|
+
? { ...next, controller_owned: true, stop_turn: true, instruction: "This Stage is complete. End this Turn now and await the controller's next Stage assignment. Do not start the successor Stage or prepare its command." }
|
|
9
|
+
: { ...next, command: managedLifecycleCommand(context, input.command) };
|
|
10
|
+
}
|
|
@@ -1,18 +1,89 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
5
|
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { canonicalPath } from "../storage/paths.js";
|
|
4
7
|
import { isExecutionRouting } from "./execution-policy.js";
|
|
8
|
+
export const RUN_OPERATIONAL_DECISION_CONTRACT = "run-operational-decision@1";
|
|
9
|
+
const object = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
10
|
+
const text = (value) => typeof value === "string" && Boolean(value.trim());
|
|
11
|
+
const keys = (value, allowed) => object(value) && Object.keys(value).every(key => allowed.includes(key));
|
|
12
|
+
/** Structural validation only: acceptance is supplied by the caller, never inferred from settings. */
|
|
13
|
+
export function validateRunOperationalDecision(value, settings) {
|
|
14
|
+
if (!object(value) || value.schema_id !== RUN_OPERATIONAL_DECISION_CONTRACT || !text(value.id)
|
|
15
|
+
|| !object(value.scope) || !text(value.scope.project_root) || !path.isAbsolute(value.scope.project_root)
|
|
16
|
+
|| [value.scope.eval_id, value.scope.execution_id].some(item => item !== undefined && !text(item))
|
|
17
|
+
|| !object(value.settings) || !object(value.settings.merge_delivery) || value.settings.merge_delivery.strategy !== settings.merge_delivery.strategy
|
|
18
|
+
|| !object(value.settings.merge_cleanup) || value.settings.merge_cleanup.source !== settings.merge_cleanup.source
|
|
19
|
+
|| !object(value.policy) || !text(value.policy.path) || !text(value.policy.sha256) || !/^[a-f0-9]{64}$/.test(value.policy.sha256)
|
|
20
|
+
|| !Array.isArray(value.policy.clauses) || !value.policy.clauses.length || !value.policy.clauses.every(text)
|
|
21
|
+
|| !object(value.acceptance) || !text(value.acceptance.source) || !text(value.acceptance.reference)
|
|
22
|
+
|| value.exception !== undefined && (!object(value.exception) || !text(value.exception.reason) || !text(value.exception.owner)
|
|
23
|
+
|| !text(value.exception.review_at) || !Number.isFinite(Date.parse(value.exception.review_at))
|
|
24
|
+
|| value.exception.expires_at !== undefined && (!text(value.exception.expires_at) || !Number.isFinite(Date.parse(value.exception.expires_at))))) {
|
|
25
|
+
throw new AppError("operational_decision_invalid", "Operational decision requires explicit acceptance, scoped authority and matching settings", 2);
|
|
26
|
+
}
|
|
27
|
+
if (!keys(value, ["schema_id", "id", "scope", "settings", "policy", "acceptance", "exception", "declaration"])
|
|
28
|
+
|| !keys(value.scope, ["project_root", "eval_id", "execution_id"])
|
|
29
|
+
|| !keys(value.settings, ["merge_delivery", "merge_cleanup"])
|
|
30
|
+
|| !keys(value.settings.merge_delivery, ["strategy"]) || !keys(value.settings.merge_cleanup, ["source"])
|
|
31
|
+
|| !keys(value.policy, ["path", "sha256", "clauses"]) || !keys(value.acceptance, ["source", "reference"])
|
|
32
|
+
|| value.exception !== undefined && !keys(value.exception, ["reason", "owner", "review_at", "expires_at"])
|
|
33
|
+
|| value.declaration !== undefined && !keys(value.declaration, ["bytes", "sha256"]))
|
|
34
|
+
throw new AppError("operational_decision_invalid", "Operational decision contains unsupported authority fields", 2);
|
|
35
|
+
if (value.declaration !== undefined) {
|
|
36
|
+
if (!object(value.declaration) || !text(value.declaration.bytes) || !text(value.declaration.sha256)
|
|
37
|
+
|| crypto.createHash("sha256").update(value.declaration.bytes).digest("hex") !== value.declaration.sha256)
|
|
38
|
+
throw new AppError("operational_decision_invalid", "Original operational declaration checksum does not match", 2);
|
|
39
|
+
let declaration;
|
|
40
|
+
try {
|
|
41
|
+
declaration = JSON.parse(value.declaration.bytes);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new AppError("operational_decision_invalid", "Original operational declaration must be JSON", 2);
|
|
45
|
+
}
|
|
46
|
+
if (!object(declaration) || !keys(declaration, ["schema_id", "id", "scope", "settings", "policy", "acceptance", "exception"])
|
|
47
|
+
|| declaration.scope !== undefined && !keys(declaration.scope, ["project_root", "eval_id", "execution_id"])
|
|
48
|
+
|| ["schema_id", "id", "settings", "policy", "acceptance", "exception"].some(key => !isDeepStrictEqual(declaration[key], value[key])))
|
|
49
|
+
throw new AppError("operational_decision_invalid", "Materialized decision changed the accepted declaration", 2);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/** Machine validation of declared policy bytes, not an NLP interpretation of Markdown. */
|
|
54
|
+
export function assertOperationalDecisionSource(decision, projectRoot, now) {
|
|
55
|
+
if (canonicalPath(decision.scope.project_root, false) !== canonicalPath(projectRoot, false))
|
|
56
|
+
throw new AppError("operational_decision_scope_mismatch", "Operational decision belongs to another project", 2);
|
|
57
|
+
const file = path.resolve(projectRoot, decision.policy.path);
|
|
58
|
+
const relative = path.relative(fs.realpathSync(projectRoot), fs.existsSync(file) ? fs.realpathSync(file) : file);
|
|
59
|
+
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || relative === ".." || !fs.existsSync(file) || crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== decision.policy.sha256)
|
|
60
|
+
throw new AppError("operational_decision_policy_changed", "Operational decision policy source checksum does not match an owned file", 2, { file });
|
|
61
|
+
if (decision.exception?.expires_at && Date.parse(decision.exception.expires_at) <= Date.parse(now))
|
|
62
|
+
throw new AppError("operational_decision_expired", "Explicit operational decision expiration has passed", 2);
|
|
63
|
+
}
|
|
5
64
|
const relativePath = path.join(".memory-bank", "dd-flow", "project-execution.json");
|
|
6
65
|
/** The project owns defaults; a RUN snapshots them once and never re-infers them. */
|
|
7
66
|
export function loadVnextExecutionProfile(projectRoot) {
|
|
67
|
+
return loadVnextExecutionProfileSnapshot(projectRoot).profile;
|
|
68
|
+
}
|
|
69
|
+
/** Parse and identify the same source bytes; a second read cannot bind another declaration. */
|
|
70
|
+
export function loadVnextExecutionProfileSnapshot(projectRoot) {
|
|
8
71
|
const file = path.join(projectRoot, relativePath);
|
|
72
|
+
let bytes;
|
|
9
73
|
let value;
|
|
10
74
|
try {
|
|
11
|
-
|
|
75
|
+
bytes = fs.readFileSync(file);
|
|
76
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
12
77
|
}
|
|
13
78
|
catch (error) {
|
|
14
79
|
throw new AppError("execution_profile_missing", "vNext flow requires .memory-bank/dd-flow/project-execution.json", 1, { file, cause: String(error) });
|
|
15
80
|
}
|
|
81
|
+
return { profile: validateVnextExecutionProfile(value), bytes };
|
|
82
|
+
}
|
|
83
|
+
/** Pure shared admission contract for project-execution@2 and @3. */
|
|
84
|
+
export function validateVnextExecutionProfile(value) {
|
|
85
|
+
if (!object(value))
|
|
86
|
+
throw new AppError("execution_profile_invalid", "Project execution profile must be an object", 1);
|
|
16
87
|
const profile = value;
|
|
17
88
|
if ((profile.schema_id !== "dd-flow/project-execution@2" && profile.schema_id !== "dd-flow/project-execution@3")
|
|
18
89
|
|| (profile.stage_session_mode !== undefined && profile.stage_session_mode !== "same_session" && profile.stage_session_mode !== "new_session")
|
|
@@ -25,8 +96,10 @@ export function loadVnextExecutionProfile(projectRoot) {
|
|
|
25
96
|
|| !profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
|
|
26
97
|
|| typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()
|
|
27
98
|
|| (profile.execution === undefined ? profile.schema_id === "dd-flow/project-execution@3" : !isExecutionRouting(profile.execution))) {
|
|
28
|
-
throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1
|
|
99
|
+
throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1);
|
|
29
100
|
}
|
|
101
|
+
if (profile.operational_decision !== undefined)
|
|
102
|
+
validateRunOperationalDecision(profile.operational_decision, profile);
|
|
30
103
|
return { ...profile, stage_session_mode: profile.stage_session_mode ?? "same_session", code_review_mode: profile.code_review_mode ?? "off" };
|
|
31
104
|
}
|
|
32
105
|
export function executionProfilePath(projectRoot) { return path.join(projectRoot, relativePath); }
|
|
@@ -10,6 +10,7 @@ import { storageSessionId } from "./session-identity.js";
|
|
|
10
10
|
import { reconcileUnfinishedChecks } from "./code-checks.js";
|
|
11
11
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
12
12
|
import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
|
|
13
|
+
import { eligibleCodexChildOverload } from "./codex-capacity.js";
|
|
13
14
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
14
15
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
15
16
|
export function getStageFanoutCapacity(context, input) {
|
|
@@ -143,6 +144,10 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
145
146
|
const link = active[0];
|
|
147
|
+
if (value.harness_id === "codex-desktop" && eligibleCodexChildOverload(child, value.parent_session_id)) {
|
|
148
|
+
issues.push({ code: "child_capacity_pending", work_id: link.work_id, session_id: child.session_id });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
146
151
|
const pendingCheck = () => context.db.get("SELECT 1 FROM check_receipts WHERE project_id = ? AND run_id = ? AND work_id = ? AND status = 'running' LIMIT 1", [project.id, input.runId, link.work_id]);
|
|
147
152
|
if (pendingCheck()) {
|
|
148
153
|
const run = context.db.get("SELECT workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|