@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100
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 +561 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +91 -15
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/run-cli.js +1506 -334
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +75 -21
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +676 -79
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +134 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +125 -80
- package/dist/services/eval-snapshots.js +838 -74
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +609 -310
- package/dist/services/lanes.js +62 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +425 -0
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +78 -27
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +27 -18
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +136 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +803 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +437 -81
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +21 -19
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +61 -80
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +112 -108
- package/dist/services/stage-pause.js +117 -73
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +108 -132
- package/dist/services/vnext-code-review.js +245 -98
- package/dist/services/vnext-code.js +355 -257
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +166 -26
- package/dist/services/vnext-merge.js +413 -147
- package/dist/services/vnext-plan-review.js +182 -109
- package/dist/services/vnext-plan.js +199 -82
- package/dist/services/vnext-protocolize.js +133 -60
- package/dist/services/vnext-specify.js +94 -71
- package/dist/services/work-registry.js +813 -160
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +569 -16
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +24 -13
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,23 +1,28 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
|
|
8
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
|
|
9
9
|
import { validateSchema } from "./schema-validation.js";
|
|
10
|
-
import {
|
|
10
|
+
import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair,
|
|
14
|
-
import { finalCodeCheckDeclarations, runCodeChecks,
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork,
|
|
13
|
+
import { addVnextCodeRepair, prepareVnextCodeRepair, registerAggregateCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
14
|
+
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
15
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
|
+
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
16
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
19
20
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
20
|
-
import {
|
|
21
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
22
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
23
|
+
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
24
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
25
|
+
import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
|
|
21
26
|
const stage = "code-review";
|
|
22
27
|
const stageDir = vnextStageDirectory(stage);
|
|
23
28
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -25,57 +30,85 @@ export function isVnextCodeReviewRun(context, input) {
|
|
|
25
30
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
26
31
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
27
32
|
}
|
|
28
|
-
export function
|
|
33
|
+
export function prepareVnextCodeReviewStart(context, input) {
|
|
29
34
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
30
35
|
const project = requireProjectByRoot(context, projectRoot);
|
|
31
36
|
const run = requireRun(context, project.id, input.runId);
|
|
32
37
|
const home = requireHome(run);
|
|
33
|
-
|
|
38
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
34
39
|
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
35
40
|
if (!fs.existsSync(path.join(home, "05-code", "stage-report.json")))
|
|
36
|
-
throw new AppError("
|
|
41
|
+
throw new AppError("runtime_artifact_missing", "CODE-REVIEW requires an accepted CODE report", 1);
|
|
37
42
|
const root = path.join(home, stageDir);
|
|
38
|
-
fs.mkdirSync(root, { recursive: true });
|
|
39
43
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
40
44
|
const prior = stageStatus(run, stage);
|
|
41
45
|
const existingPrompt = path.join(root, "stage-prompt.md");
|
|
42
|
-
if (
|
|
46
|
+
if (input.externalContext)
|
|
47
|
+
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
48
|
+
prepareOutputFile(existingPrompt);
|
|
49
|
+
const common = { projectRoot, project, run, home, root, rootWork, existingPrompt };
|
|
50
|
+
if (prior && fs.existsSync(existingPrompt))
|
|
51
|
+
return { ...common, resumed: true, prior, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(root) };
|
|
52
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
53
|
+
const mode = effectiveMode(home, run);
|
|
54
|
+
const cycle = reviewCycle(run);
|
|
55
|
+
const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
|
|
56
|
+
const groups = mode.mode === "off" ? [] : reviewGroups(home);
|
|
57
|
+
const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
|
|
58
|
+
const items = mode.mode === "off" ? [] : prepareWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch });
|
|
59
|
+
const template = mode.mode === "off" ? "" : read(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md"));
|
|
60
|
+
const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
|
|
61
|
+
for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
|
|
62
|
+
prepareOutputFile(path.join(root, name));
|
|
63
|
+
return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks };
|
|
64
|
+
}
|
|
65
|
+
export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
|
|
66
|
+
const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
|
|
67
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
68
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
69
|
+
fs.mkdirSync(root, { recursive: true });
|
|
70
|
+
if (prepared.resumed) {
|
|
43
71
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
44
72
|
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 } : {}) });
|
|
45
73
|
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
46
|
-
const orchestration =
|
|
47
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
74
|
+
const orchestration = prepared.orchestration;
|
|
75
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
48
76
|
}
|
|
49
|
-
const mode
|
|
77
|
+
const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
|
|
78
|
+
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
50
79
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
51
|
-
|
|
80
|
+
const reviewContextPath = path.join(root, "review-context.json");
|
|
81
|
+
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 } } : {}), code_report_sha256: codeReportSha256 }, null, 2)}\n`);
|
|
82
|
+
if (mode.mode === "off") {
|
|
83
|
+
fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
|
|
52
84
|
const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
|
|
53
85
|
fs.writeFileSync(promptPath, prompt);
|
|
54
86
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
55
87
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
56
88
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
57
|
-
return { ok: true, run_id: run.id, stage, mode, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
89
|
+
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
58
90
|
}
|
|
59
|
-
const groups = reviewGroups(run, home, mode);
|
|
60
|
-
const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", group, read_only: true } })) };
|
|
61
91
|
const batchFile = path.join(root, "review-work-batch.json");
|
|
62
92
|
fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
|
|
63
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode, groups });
|
|
93
|
+
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks });
|
|
64
94
|
fs.writeFileSync(promptPath, prompt);
|
|
65
95
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
66
96
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
67
|
-
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id,
|
|
68
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
97
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
|
|
98
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
69
99
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
70
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode, groups, registered });
|
|
71
|
-
return { ok: true, run_id: run.id, stage, mode, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
100
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
101
|
+
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
72
102
|
}
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
export function prepareVnextCodeReviewRepair(context, input) {
|
|
104
|
+
if (!Array.isArray(input.findingIds) || !input.findingIds.length || input.findingIds.some(id => typeof id !== "string" || !id.trim()))
|
|
105
|
+
throw new AppError("validation", "Repair requires nonempty finding IDs", 2);
|
|
106
|
+
if (!input.checkRefsByFinding || typeof input.checkRefsByFinding !== "object" || Array.isArray(input.checkRefsByFinding) || Object.entries(input.checkRefsByFinding).some(([id, refs]) => !input.findingIds.includes(id) || !Array.isArray(refs) || refs.some(ref => typeof ref !== "string" || !ref.trim())))
|
|
107
|
+
throw new AppError("validation", "Repair check references must map selected findings to check IDs", 2);
|
|
75
108
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
76
109
|
const project = requireProjectByRoot(context, projectRoot);
|
|
77
110
|
const run = requireRun(context, project.id, input.runId);
|
|
78
|
-
const reviewers = reviewerWorks(context, project.id, run.id);
|
|
111
|
+
const reviewers = reviewerWorks(context, project.id, run.id, reviewCycle(run));
|
|
79
112
|
const findings = canonicalCodeFindings(reviewers);
|
|
80
113
|
const selected = findings.filter((finding) => input.findingIds.includes(finding.finding_ref));
|
|
81
114
|
if (selected.length !== new Set(input.findingIds).size)
|
|
@@ -95,17 +128,20 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
95
128
|
const unknown = requestedCheckRefs.filter((ref) => !checks.has(ref));
|
|
96
129
|
if (unknown.length)
|
|
97
130
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
|
|
98
|
-
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref))
|
|
99
|
-
|
|
131
|
+
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref));
|
|
132
|
+
const repairInput = {
|
|
100
133
|
projectRoot,
|
|
101
134
|
runId: run.id,
|
|
102
|
-
|
|
103
|
-
reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
|
|
135
|
+
reviewFindings: selected.map(({ finding_ref, finding }) => ({ finding_ref, priority: finding.priority, problem: finding.problem, impact: finding.impact, required_outcome: finding.required_outcome, evidence_refs: finding.evidence_refs, obligation_refs: finding.obligation_refs, decision_reason: input.decisionReasonsByFinding?.[finding_ref] ?? input.objective, check_refs: input.checkRefsByFinding[finding_ref] ?? [] })),
|
|
104
136
|
reviewChecks,
|
|
105
|
-
reviewCheckRefs: requestedCheckRefs,
|
|
106
137
|
originWorkIds: origins,
|
|
107
|
-
objective: input.objective
|
|
108
|
-
|
|
138
|
+
objective: input.objective,
|
|
139
|
+
reviewCycle: reviewCycle(run)
|
|
140
|
+
};
|
|
141
|
+
return { input: repairInput, value: prepareVnextCodeRepair(context, repairInput) };
|
|
142
|
+
}
|
|
143
|
+
export function addVnextCodeReviewRepair(context, input, prepared = prepareVnextCodeReviewRepair(context, input)) {
|
|
144
|
+
return addVnextCodeRepair(context, prepared.input, prepared.value);
|
|
109
145
|
}
|
|
110
146
|
export async function finishVnextCodeReview(context, input) {
|
|
111
147
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -113,23 +149,40 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
113
149
|
const run = requireRun(context, project.id, input.runId);
|
|
114
150
|
const home = requireHome(run);
|
|
115
151
|
const root = path.join(home, stageDir);
|
|
116
|
-
const
|
|
152
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
117
153
|
const reportPath = path.join(root, "stage-report.json");
|
|
118
154
|
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
119
155
|
const report = readJson(reportPath);
|
|
120
|
-
|
|
156
|
+
const next = report.semantic?.next_action ?? "code_review_completed";
|
|
157
|
+
resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next);
|
|
158
|
+
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: next };
|
|
121
159
|
}
|
|
160
|
+
const reviewContext = readJson(path.join(root, "review-context.json"));
|
|
161
|
+
const mode = reviewContext.mode;
|
|
162
|
+
if (!mode)
|
|
163
|
+
throw new AppError("runtime_missing", "CODE-REVIEW effective mode was not frozen at stage start", 1);
|
|
164
|
+
if (reviewContext.code_report_sha256 !== sha256File(path.join(home, "05-code", "stage-report.json")))
|
|
165
|
+
throw new AppError("code_review_input_changed", "Accepted CODE report changed after CODE-REVIEW start", 2);
|
|
166
|
+
const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
|
|
167
|
+
if (handoffFailures.length)
|
|
168
|
+
throw new AppError("validation", "CODE-REVIEW handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
|
|
122
169
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
123
170
|
const coordinator = context.db.get("SELECT session_id, result_path FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [rootWork.work_id]);
|
|
124
171
|
if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
|
|
125
172
|
throw new AppError("code_review_session_binding_conflict", "CODE-REVIEW can finish only after its successful stage start bound the coordinator Session and report path", 1, { run_id: run.id, expected_result_path: reportPath, actual_result_path: coordinator?.result_path ?? null });
|
|
126
|
-
const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
173
|
+
const decisionFile = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
174
|
+
const disabledDecision = disabledReviewDecision();
|
|
175
|
+
// The implicit review-off decision is the one deliberate no-file path. An
|
|
176
|
+
// explicitly supplied --decision-file is still caller input and must be
|
|
177
|
+
// readable before any lifecycle work can proceed.
|
|
178
|
+
const decision = mode === "off" && !input.decision && !input.decisionFile && !fs.existsSync(decisionFile)
|
|
179
|
+
? disabledDecision
|
|
180
|
+
: readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile), input.decision);
|
|
181
|
+
validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
182
|
+
const cycle = reviewContext.cycle ?? "initial";
|
|
183
|
+
const reviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
132
184
|
if (mode !== "off") {
|
|
185
|
+
assertCodeReviewCoverage(reviewGroups(home), reviewers);
|
|
133
186
|
const incomplete = reviewers.filter((work) => work.status !== "completed");
|
|
134
187
|
if (incomplete.length)
|
|
135
188
|
throw new AppError("reviewer_jobs_incomplete", "CODE-REVIEW cannot finish while reviewer Work is unsettled", 2, { works: incomplete.map((work) => ({ work_id: work.work_id, status: work.status })) });
|
|
@@ -137,30 +190,35 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
137
190
|
validateReviewerResult(context, work, run);
|
|
138
191
|
const canonical = reviewDecisionForRepair(decision, reviewers);
|
|
139
192
|
const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
|
|
140
|
-
let repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
141
|
-
const decisionSha = sha256File(decisionFile);
|
|
193
|
+
let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
142
194
|
const frozenShaFile = path.join(root, ".decision-sha256");
|
|
143
|
-
|
|
195
|
+
const decisionHash = sha256Value(input.decisionText ?? JSON.stringify(decision));
|
|
196
|
+
if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionHash)
|
|
144
197
|
throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
|
|
145
198
|
const fixIds = canonical.fix_ids;
|
|
146
199
|
if (fixIds.length && repairs.length === 0) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
// Accept the value once. Repairs consume this decision, not a mutable
|
|
201
|
+
// operator file. Reuse the existing context artifact and review cycle.
|
|
202
|
+
const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
|
|
203
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
|
|
204
|
+
fs.renameSync(temporary, contextFile);
|
|
205
|
+
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(", ")}` });
|
|
206
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
|
|
150
207
|
}
|
|
151
|
-
repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
208
|
+
repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
152
209
|
const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
|
|
153
210
|
if (incompleteRepairs.length)
|
|
154
211
|
throw new AppError("review_repair_incomplete", "CODE-REVIEW cannot finish while a repair Work is unsettled", 2, { works: incompleteRepairs.map((work) => ({ work_id: work.work_id, status: work.status })) });
|
|
155
212
|
validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
|
|
156
213
|
}
|
|
157
214
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
158
|
-
const repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
215
|
+
const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
159
216
|
const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
217
|
+
if (input.retryCheckId) {
|
|
218
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, reason: input.retryReason });
|
|
219
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
220
|
+
}
|
|
221
|
+
const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}), ...(input.progress ? { progress: input.progress } : {}) });
|
|
164
222
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
165
223
|
if (failed.length)
|
|
166
224
|
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
@@ -168,42 +226,108 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
168
226
|
outcome: "repair_required",
|
|
169
227
|
retry_after_workspace_change: true,
|
|
170
228
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
171
|
-
|
|
229
|
+
continuation: registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks: finalChecks, generation }),
|
|
230
|
+
instruction: "Repair Work is already registered. End this Turn so the controller can launch a fresh child; after it finishes, repeat finish with the already accepted review decision. Do not redo review."
|
|
172
231
|
});
|
|
173
232
|
const stopTarget = executionStopTarget(run);
|
|
174
233
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
175
234
|
const now = context.now();
|
|
176
235
|
const startedAt = stageStartedAt(run, now);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]), usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"]) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
|
|
237
|
+
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
238
|
+
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
239
|
+
const acceptedPaths = changedPaths(run.workspace_root);
|
|
240
|
+
let mergeRequest = null;
|
|
241
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
242
|
+
const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
243
|
+
withStageSettlement(context, () => {
|
|
244
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
245
|
+
const currentReviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
246
|
+
const currentRepairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
247
|
+
if (currentReviewers.length !== reviewers.length || currentRepairs.length !== repairs.length || [...currentReviewers, ...currentRepairs].some(work => work.status !== "completed" || ![...reviewers, ...repairs].some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
|
|
248
|
+
throw new AppError("reviewer_jobs_incomplete", "Review or repair results changed while the final checks were running", 2);
|
|
249
|
+
}
|
|
250
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
251
|
+
writeReport(root, report);
|
|
252
|
+
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
253
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
254
|
+
if (nextAction === "start_merge")
|
|
255
|
+
advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
|
|
256
|
+
else {
|
|
257
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
258
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
|
|
259
|
+
}
|
|
260
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
261
|
+
});
|
|
262
|
+
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {}) };
|
|
263
|
+
}
|
|
264
|
+
export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
|
|
265
|
+
if (!accepted)
|
|
266
|
+
return prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value;
|
|
267
|
+
if ((prepared !== undefined || (explicit && fs.existsSync(file))) && JSON.stringify(prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value) !== JSON.stringify(accepted)) {
|
|
268
|
+
throw new AppError("review_decision_changed", "This review cycle already accepted a different decision", 2, { decision_file: file });
|
|
187
269
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
270
|
+
return accepted;
|
|
271
|
+
}
|
|
272
|
+
function disabledReviewDecision() {
|
|
273
|
+
return { schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] };
|
|
191
274
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
275
|
+
/** Retained decisions remain authoritative after their caller file is gone. */
|
|
276
|
+
export function prepareCodeReviewDecision(runRoot, file) {
|
|
277
|
+
const root = path.join(runRoot, "06-code-review");
|
|
278
|
+
const context = readJson(path.join(root, "review-context.json"));
|
|
279
|
+
const source = path.resolve(file ?? path.join(root, "decision.json"));
|
|
280
|
+
if (context.accepted_decision && !fs.existsSync(source))
|
|
281
|
+
return { value: context.accepted_decision };
|
|
282
|
+
if (context.mode === "off" && !file && !fs.existsSync(source))
|
|
283
|
+
return { value: disabledReviewDecision() };
|
|
284
|
+
const prepared = prepareJsonFile(source, "CODE-REVIEW decision");
|
|
285
|
+
return { value: readCodeReviewDecision(source, context.accepted_decision, Boolean(file), prepared.value), text: prepared.text };
|
|
286
|
+
}
|
|
287
|
+
export function reviewGroups(home) {
|
|
288
|
+
const expected = new Set(baselineAspects);
|
|
289
|
+
const declared = [];
|
|
290
|
+
for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
|
|
196
291
|
const map = readJson(file);
|
|
197
292
|
for (const aspect of map.aspects ?? [])
|
|
198
293
|
if (aspect.applicability === "applicable" && aspect.aspect_id)
|
|
199
|
-
|
|
294
|
+
expected.add(aspect.aspect_id);
|
|
295
|
+
for (const group of map.review_groups ?? [])
|
|
296
|
+
if (group.id && group.aspect_ids?.length)
|
|
297
|
+
declared.push({ key: `${map.protocol_id ?? path.basename(path.dirname(file))}/${group.id}`, aspect_ids: [...new Set(group.aspect_ids)] });
|
|
298
|
+
}
|
|
299
|
+
if (!declared.length)
|
|
300
|
+
return [{ key: "baseline", aspect_ids: [...expected].sort() }];
|
|
301
|
+
const assigned = new Set();
|
|
302
|
+
const scoped = new Set();
|
|
303
|
+
for (const group of declared)
|
|
304
|
+
for (const aspectId of group.aspect_ids) {
|
|
305
|
+
const assignment = `${group.key.slice(0, group.key.lastIndexOf("/"))}/${aspectId}`;
|
|
306
|
+
if (scoped.has(assignment))
|
|
307
|
+
throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one group of the same protocol", 2, { aspect_id: aspectId });
|
|
308
|
+
scoped.add(assignment);
|
|
309
|
+
assigned.add(aspectId);
|
|
310
|
+
}
|
|
311
|
+
const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
|
|
312
|
+
return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
|
|
313
|
+
}
|
|
314
|
+
export function assertCodeReviewCoverage(groups, reviewers) {
|
|
315
|
+
const expected = new Map(groups.map(group => [group.key, [...group.aspect_ids].sort()]));
|
|
316
|
+
if (!expected.size || expected.size !== groups.length)
|
|
317
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW expected group identities must be nonempty and unique", 2);
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
for (const work of reviewers) {
|
|
320
|
+
const group = readJsonString(work.payload_json).group;
|
|
321
|
+
if (!group?.key || seen.has(group.key) || !expected.has(group.key) || !Array.isArray(group.aspect_ids) || JSON.stringify([...group.aspect_ids].sort()) !== JSON.stringify(expected.get(group.key)))
|
|
322
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW Work does not match its expected group", 2, { work_id: work.work_id });
|
|
323
|
+
seen.add(group.key);
|
|
200
324
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
325
|
+
const missing = [...expected.keys()].filter(key => !seen.has(key));
|
|
326
|
+
if (missing.length || !expected.size)
|
|
327
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
204
328
|
}
|
|
205
|
-
function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring.
|
|
206
|
-
function orchestratorPrompt(context, input) { const template
|
|
329
|
+
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.`; }
|
|
330
|
+
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 is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "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"); }
|
|
207
331
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
208
332
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
209
333
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -252,8 +376,11 @@ function validateDecision(context, input) {
|
|
|
252
376
|
const repairedBy = new Map();
|
|
253
377
|
for (const work of input.repairs) {
|
|
254
378
|
const result = readJsonString(work.result);
|
|
255
|
-
for (const
|
|
256
|
-
|
|
379
|
+
for (const resolution of Array.isArray(result.resolutions) ? result.resolutions : []) {
|
|
380
|
+
const ref = typeof resolution === "object" && resolution !== null && typeof resolution.finding_ref === "string" ? resolution.finding_ref : null;
|
|
381
|
+
if (ref)
|
|
382
|
+
repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
|
|
383
|
+
}
|
|
257
384
|
}
|
|
258
385
|
for (const { finding_ref: findingRef, finding } of findings) {
|
|
259
386
|
const item = decisions.get(findingRef);
|
|
@@ -278,9 +405,21 @@ function validateDecision(context, input) {
|
|
|
278
405
|
if (!known.has(item.finding_ref))
|
|
279
406
|
throw new AppError("review_evidence_invalid", "Decision references an unknown reviewer finding", 2, { finding_ref: item.finding_ref });
|
|
280
407
|
}
|
|
281
|
-
function validateReviewerResult(
|
|
282
|
-
|
|
283
|
-
|
|
408
|
+
function validateReviewerResult(_context, work, _run) {
|
|
409
|
+
if (!work.result)
|
|
410
|
+
throw new AppError("review_evidence_invalid", "Reviewer Work has no result", 2, { work_id: work.work_id });
|
|
411
|
+
// Schema, group identity and portable evidence references were validated
|
|
412
|
+
// before this Work was accepted. This fan-in guard only checks semantic
|
|
413
|
+
// relationships that depend on the aggregate review decision.
|
|
414
|
+
const result = readJsonString(work.result);
|
|
415
|
+
if (result.verdict === "blocked")
|
|
416
|
+
throw new AppError("review_blocked", "A blocked reviewer result cannot be accepted as completed review evidence", 2, { work_id: work.work_id });
|
|
417
|
+
if (result.verdict === "pass" && result.findings.length)
|
|
418
|
+
throw new AppError("review_evidence_invalid", "A passing reviewer result cannot contain material findings", 2, { work_id: work.work_id });
|
|
419
|
+
if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
|
|
420
|
+
throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
|
|
421
|
+
}
|
|
422
|
+
function reviewerWorks(context, projectId, runId, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return Boolean(payload.kind === "code-review" && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle); }); }
|
|
284
423
|
function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
|
|
285
424
|
function acceptedCodeChecks(context, projectId, runId) { return codeWorks(context, projectId, runId).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }); }
|
|
286
425
|
function validateRepairChecks(context, input) { const known = new Set(acceptedCodeChecks(context, input.projectId, input.runId).map((check) => check.id)); const selected = {}; for (const item of input.decision.findings.filter((item) => item.disposition === "fix")) {
|
|
@@ -292,7 +431,7 @@ function validateRepairChecks(context, input) { const known = new Set(acceptedCo
|
|
|
292
431
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW repair selects a check absent from the accepted CODE handoff", 2, { finding_ref: item.finding_ref, check_refs: unknown });
|
|
293
432
|
selected[item.finding_ref] = refs;
|
|
294
433
|
} return selected; }
|
|
295
|
-
function reviewRepairWorks(context, projectId, runId, root) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => isCodeReviewStageRepair(work, root)); }
|
|
434
|
+
function reviewRepairWorks(context, projectId, runId, root, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return isCodeReviewStageRepair(work, root) && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle; }); }
|
|
296
435
|
export function isCodeReviewStageRepair(work, root) {
|
|
297
436
|
if (reviewFindingIds(work).length)
|
|
298
437
|
return true;
|
|
@@ -306,25 +445,31 @@ export function isCodeReviewStageRepair(work, root) {
|
|
|
306
445
|
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
307
446
|
}
|
|
308
447
|
function reviewFindingIds(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
309
|
-
return []; const
|
|
448
|
+
return []; const findings = repair.review_findings; return Array.isArray(findings) ? findings.flatMap((value) => typeof value === "object" && value !== null && typeof value.finding_ref === "string" ? [value.finding_ref] : []) : []; }
|
|
310
449
|
function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
311
|
-
return []; const
|
|
450
|
+
return []; const findings = repair.review_findings; return Array.isArray(findings) ? [...new Set(findings.flatMap((value) => typeof value === "object" && value !== null && Array.isArray(value.check_refs) ? value.check_refs.filter((ref) => typeof ref === "string") : []))] : []; }
|
|
312
451
|
function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
|
|
313
|
-
function effectiveMode(
|
|
452
|
+
function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if (index.variables?.["merge.source_repair"])
|
|
453
|
+
return { mode: "standard", source: "project_policy", reason: "A failed integration gate requires independent review of its source repair." }; const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
|
|
454
|
+
return { mode: requested, source: configured?.source === "user_instruction" ? "user_instruction" : "project_policy", reason: configured?.reason ?? "Frozen RUN configuration." }; const assessments = findFiles(path.join(home, "03-plan"), "plan.json").map((file) => readJson(file)); const deep = assessments.some((plan) => plan.assessment?.failure_impact?.level === "high" || plan.assessment?.solution_uncertainty?.level === "high"); return { mode: deep ? "deep" : "standard", source: "plan_assessment", reason: deep ? "Accepted PLAN assessment records high impact or uncertainty." : "Accepted PLAN assessment requires the standard independent review." }; }
|
|
314
455
|
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
315
|
-
function changedPaths(workspaceRoot) {
|
|
316
|
-
return execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replace(/^.* -> /, ""));
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
return [];
|
|
320
|
-
} }
|
|
456
|
+
function changedPaths(workspaceRoot) { return workspaceChangedPaths(workspaceRoot); }
|
|
321
457
|
function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
|
458
|
+
function sha256Value(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
322
459
|
function stageStatus(run, name) { try {
|
|
323
460
|
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
|
|
324
461
|
}
|
|
325
462
|
catch {
|
|
326
463
|
return null;
|
|
327
464
|
} }
|
|
465
|
+
function reviewCycle(run) { try {
|
|
466
|
+
const value = JSON.parse(run.index_json);
|
|
467
|
+
const repair = value.variables?.["merge.source_repair"];
|
|
468
|
+
return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.cycle === "string" ? String(repair.cycle) : "initial";
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return "initial";
|
|
472
|
+
} }
|
|
328
473
|
function stageStartedAt(run, fallback) { try {
|
|
329
474
|
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === stage)?.started_at ?? fallback;
|
|
330
475
|
}
|
|
@@ -334,11 +479,13 @@ catch {
|
|
|
334
479
|
function runRef(runId, home, file) { const relative = path.relative(home, file).split(path.sep).join("/"); return relative && !relative.startsWith("../") ? `run://${runId}/${relative}` : file; }
|
|
335
480
|
function requireRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work || work.status !== "running")
|
|
336
481
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
337
|
-
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root,
|
|
482
|
+
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
|
|
338
483
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
339
|
-
function requireHome(run) { if (!run.
|
|
340
|
-
throw new AppError("runtime_missing", "RUN
|
|
341
|
-
function finishCommand(context, runId, projectRoot, decision
|
|
484
|
+
function requireHome(run) { if (!run.run_root)
|
|
485
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
486
|
+
export function finishCommand(context, runId, projectRoot, decision, retryCheckId) {
|
|
487
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code-review", "--decision-file", decision, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
488
|
+
}
|
|
342
489
|
function findFiles(root, name) { if (!fs.existsSync(root))
|
|
343
490
|
return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
344
491
|
const file = path.join(root, entry.name);
|