@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.75
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 +404 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -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 +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -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 +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -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 +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -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/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-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/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -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 +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -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 +211 -0
- package/dist/services/run-controller-capture.js +134 -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 +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -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 +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +224 -216
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +605 -157
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,24 +1,26 @@
|
|
|
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
10
|
import { flowCommand } from "./stage-pause.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
14
|
-
import { finalCodeCheckDeclarations, runCodeChecks,
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork,
|
|
13
|
+
import { addVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
14
|
+
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
15
|
+
import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
|
+
import { withStageSettlement } from "./runs.js";
|
|
16
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
19
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
20
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
20
|
-
import {
|
|
21
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
21
22
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
23
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
22
24
|
const stage = "code-review";
|
|
23
25
|
const stageDir = vnextStageDirectory(stage);
|
|
24
26
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -47,38 +49,40 @@ export function startVnextCodeReview(context, input) {
|
|
|
47
49
|
const orchestration = readFanoutDescriptor(root);
|
|
48
50
|
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")) } };
|
|
49
51
|
}
|
|
50
|
-
const mode = effectiveMode(
|
|
52
|
+
const mode = effectiveMode(home, run);
|
|
53
|
+
const cycle = reviewCycle(run);
|
|
54
|
+
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
51
55
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
52
56
|
const reviewContextPath = path.join(root, "review-context.json");
|
|
53
|
-
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode,
|
|
54
|
-
if (mode === "off") {
|
|
57
|
+
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: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
|
|
58
|
+
if (mode.mode === "off") {
|
|
55
59
|
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`;
|
|
56
60
|
fs.writeFileSync(promptPath, prompt);
|
|
57
61
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
58
62
|
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 } : {}) });
|
|
59
63
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
60
|
-
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")) } };
|
|
64
|
+
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")) } };
|
|
61
65
|
}
|
|
62
|
-
const groups = reviewGroups(
|
|
63
|
-
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 } })) };
|
|
66
|
+
const groups = reviewGroups(home);
|
|
67
|
+
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 } })) };
|
|
64
68
|
const batchFile = path.join(root, "review-work-batch.json");
|
|
65
69
|
fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
|
|
66
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode, groups });
|
|
70
|
+
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
|
|
67
71
|
fs.writeFileSync(promptPath, prompt);
|
|
68
72
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
69
73
|
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 } : {}) });
|
|
70
74
|
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, file: batchFile });
|
|
71
75
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
72
76
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
73
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode, groups, registered });
|
|
74
|
-
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")) } };
|
|
77
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
78
|
+
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")) } };
|
|
75
79
|
}
|
|
76
80
|
/** Create a narrow CODE repair from accepted independent-review evidence. */
|
|
77
81
|
export function addVnextCodeReviewRepair(context, input) {
|
|
78
82
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
79
83
|
const project = requireProjectByRoot(context, projectRoot);
|
|
80
84
|
const run = requireRun(context, project.id, input.runId);
|
|
81
|
-
const reviewers = reviewerWorks(context, project.id, run.id);
|
|
85
|
+
const reviewers = reviewerWorks(context, project.id, run.id, reviewCycle(run));
|
|
82
86
|
const findings = canonicalCodeFindings(reviewers);
|
|
83
87
|
const selected = findings.filter((finding) => input.findingIds.includes(finding.finding_ref));
|
|
84
88
|
if (selected.length !== new Set(input.findingIds).size)
|
|
@@ -98,18 +102,15 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
98
102
|
const unknown = requestedCheckRefs.filter((ref) => !checks.has(ref));
|
|
99
103
|
if (unknown.length)
|
|
100
104
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
|
|
101
|
-
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref))
|
|
102
|
-
if (!reviewChecks.length)
|
|
103
|
-
throw new AppError("review_repair_executable_check_required", "CODE-REVIEW repair requires at least one executable causal check; external-only evidence cannot validate a repair Work", 2, { finding_ids: input.findingIds, check_refs: requestedCheckRefs });
|
|
105
|
+
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref));
|
|
104
106
|
return addVnextCodeRepair(context, {
|
|
105
107
|
projectRoot,
|
|
106
108
|
runId: run.id,
|
|
107
|
-
|
|
108
|
-
reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
|
|
109
|
+
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] ?? [] })),
|
|
109
110
|
reviewChecks,
|
|
110
|
-
reviewCheckRefs: requestedCheckRefs,
|
|
111
111
|
originWorkIds: origins,
|
|
112
|
-
objective: input.objective
|
|
112
|
+
objective: input.objective,
|
|
113
|
+
reviewCycle: reviewCycle(run)
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
export async function finishVnextCodeReview(context, input) {
|
|
@@ -118,6 +119,14 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
118
119
|
const run = requireRun(context, project.id, input.runId);
|
|
119
120
|
const home = requireHome(run);
|
|
120
121
|
const root = path.join(home, stageDir);
|
|
122
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
123
|
+
const reportPath = path.join(root, "stage-report.json");
|
|
124
|
+
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
125
|
+
const report = readJson(reportPath);
|
|
126
|
+
const next = report.semantic?.next_action ?? "code_review_completed";
|
|
127
|
+
resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next);
|
|
128
|
+
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: next };
|
|
129
|
+
}
|
|
121
130
|
const reviewContext = readJson(path.join(root, "review-context.json"));
|
|
122
131
|
const mode = reviewContext.mode;
|
|
123
132
|
if (!mode)
|
|
@@ -127,11 +136,6 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
127
136
|
const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
|
|
128
137
|
if (handoffFailures.length)
|
|
129
138
|
throw new AppError("validation", "CODE-REVIEW handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
|
|
130
|
-
const reportPath = path.join(root, "stage-report.json");
|
|
131
|
-
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
132
|
-
const report = readJson(reportPath);
|
|
133
|
-
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: report.semantic?.next_action ?? "code_review_completed" };
|
|
134
|
-
}
|
|
135
139
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
136
140
|
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]);
|
|
137
141
|
if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
|
|
@@ -139,10 +143,12 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
139
143
|
const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
|
|
140
144
|
if (!fs.existsSync(decisionFile) && mode === "off")
|
|
141
145
|
fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
146
|
+
const decision = readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile));
|
|
147
|
+
validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
148
|
+
const cycle = reviewContext.cycle ?? "initial";
|
|
149
|
+
const reviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
145
150
|
if (mode !== "off") {
|
|
151
|
+
assertCodeReviewCoverage(reviewGroups(home), reviewers);
|
|
146
152
|
const incomplete = reviewers.filter((work) => work.status !== "completed");
|
|
147
153
|
if (incomplete.length)
|
|
148
154
|
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 })) });
|
|
@@ -150,30 +156,34 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
150
156
|
validateReviewerResult(context, work, run);
|
|
151
157
|
const canonical = reviewDecisionForRepair(decision, reviewers);
|
|
152
158
|
const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
|
|
153
|
-
let repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
154
|
-
const decisionSha = sha256File(decisionFile);
|
|
159
|
+
let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
155
160
|
const frozenShaFile = path.join(root, ".decision-sha256");
|
|
156
|
-
if (fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !==
|
|
161
|
+
if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== sha256File(decisionFile))
|
|
157
162
|
throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
|
|
158
163
|
const fixIds = canonical.fix_ids;
|
|
159
164
|
if (fixIds.length && repairs.length === 0) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
// Accept the value once. Repairs consume this decision, not a mutable
|
|
166
|
+
// operator file. Reuse the existing context artifact and review cycle.
|
|
167
|
+
const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
|
|
168
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
|
|
169
|
+
fs.renameSync(temporary, contextFile);
|
|
170
|
+
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(", ")}` });
|
|
171
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "Run the returned repair Work in one 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) } };
|
|
163
172
|
}
|
|
164
|
-
repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
173
|
+
repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
165
174
|
const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
|
|
166
175
|
if (incompleteRepairs.length)
|
|
167
176
|
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 })) });
|
|
168
177
|
validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
|
|
169
178
|
}
|
|
170
179
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
171
|
-
const repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
180
|
+
const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
172
181
|
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 : []; }));
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
if (input.retryCheckId) {
|
|
183
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, reason: input.retryReason });
|
|
184
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
185
|
+
}
|
|
186
|
+
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 } : {}) });
|
|
177
187
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
178
188
|
if (failed.length)
|
|
179
189
|
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
@@ -181,7 +191,8 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
181
191
|
outcome: "repair_required",
|
|
182
192
|
retry_after_workspace_change: true,
|
|
183
193
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
184
|
-
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json
|
|
194
|
+
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json`,
|
|
195
|
+
retry_command: finishCommand(context, run.id, projectRoot, decisionFile, failed[0].id)
|
|
185
196
|
});
|
|
186
197
|
const stopTarget = executionStopTarget(run);
|
|
187
198
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
@@ -191,34 +202,82 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
191
202
|
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
192
203
|
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
193
204
|
const acceptedPaths = changedPaths(run.workspace_root);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
let mergeRequest = null;
|
|
206
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
207
|
+
const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
208
|
+
withStageSettlement(context, () => {
|
|
209
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
210
|
+
const currentReviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
211
|
+
const currentRepairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
212
|
+
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))) {
|
|
213
|
+
throw new AppError("reviewer_jobs_incomplete", "Review or repair results changed while the final checks were running", 2);
|
|
214
|
+
}
|
|
215
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
216
|
+
writeReport(root, report);
|
|
217
|
+
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" });
|
|
218
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
219
|
+
if (nextAction === "start_merge")
|
|
220
|
+
advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
|
|
221
|
+
else {
|
|
222
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
223
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
|
|
224
|
+
}
|
|
225
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
226
|
+
});
|
|
227
|
+
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, `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } } : {}) };
|
|
228
|
+
}
|
|
229
|
+
export function readCodeReviewDecision(file, accepted, explicit = false) {
|
|
230
|
+
if (!accepted)
|
|
231
|
+
return readJson(file);
|
|
232
|
+
if (explicit && fs.existsSync(file) && JSON.stringify(readJson(file)) !== JSON.stringify(accepted)) {
|
|
233
|
+
throw new AppError("review_decision_changed", "This review cycle already accepted a different decision", 2, { decision_file: file });
|
|
203
234
|
}
|
|
204
|
-
|
|
205
|
-
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {}) };
|
|
235
|
+
return accepted;
|
|
206
236
|
}
|
|
207
|
-
function reviewGroups(
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
for (const file of
|
|
237
|
+
export function reviewGroups(home) {
|
|
238
|
+
const expected = new Set(baselineAspects);
|
|
239
|
+
const declared = [];
|
|
240
|
+
for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
|
|
211
241
|
const map = readJson(file);
|
|
212
242
|
for (const aspect of map.aspects ?? [])
|
|
213
243
|
if (aspect.applicability === "applicable" && aspect.aspect_id)
|
|
214
|
-
|
|
244
|
+
expected.add(aspect.aspect_id);
|
|
245
|
+
for (const group of map.review_groups ?? [])
|
|
246
|
+
if (group.id && group.aspect_ids?.length)
|
|
247
|
+
declared.push({ key: `${map.protocol_id ?? path.basename(path.dirname(file))}/${group.id}`, aspect_ids: [...new Set(group.aspect_ids)] });
|
|
215
248
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
249
|
+
if (!declared.length)
|
|
250
|
+
return [{ key: "baseline", aspect_ids: [...expected].sort() }];
|
|
251
|
+
const assigned = new Set();
|
|
252
|
+
const scoped = new Set();
|
|
253
|
+
for (const group of declared)
|
|
254
|
+
for (const aspectId of group.aspect_ids) {
|
|
255
|
+
const assignment = `${group.key.slice(0, group.key.lastIndexOf("/"))}/${aspectId}`;
|
|
256
|
+
if (scoped.has(assignment))
|
|
257
|
+
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 });
|
|
258
|
+
scoped.add(assignment);
|
|
259
|
+
assigned.add(aspectId);
|
|
260
|
+
}
|
|
261
|
+
const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
|
|
262
|
+
return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
|
|
263
|
+
}
|
|
264
|
+
export function assertCodeReviewCoverage(groups, reviewers) {
|
|
265
|
+
const expected = new Map(groups.map(group => [group.key, [...group.aspect_ids].sort()]));
|
|
266
|
+
if (!expected.size || expected.size !== groups.length)
|
|
267
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW expected group identities must be nonempty and unique", 2);
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
for (const work of reviewers) {
|
|
270
|
+
const group = readJsonString(work.payload_json).group;
|
|
271
|
+
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)))
|
|
272
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW Work does not match its expected group", 2, { work_id: work.work_id });
|
|
273
|
+
seen.add(group.key);
|
|
274
|
+
}
|
|
275
|
+
const missing = [...expected.keys()].filter(key => !seen.has(key));
|
|
276
|
+
if (missing.length || !expected.size)
|
|
277
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
219
278
|
}
|
|
220
|
-
function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
|
|
221
|
-
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.
|
|
279
|
+
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.`; }
|
|
280
|
+
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract at the Work-graph boundary. Each reviewer Work must run in one fresh child session; the coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the returned next.finish_command; do not reuse an earlier invocation-id. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
|
|
222
281
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
223
282
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
224
283
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -267,8 +326,11 @@ function validateDecision(context, input) {
|
|
|
267
326
|
const repairedBy = new Map();
|
|
268
327
|
for (const work of input.repairs) {
|
|
269
328
|
const result = readJsonString(work.result);
|
|
270
|
-
for (const
|
|
271
|
-
|
|
329
|
+
for (const resolution of Array.isArray(result.resolutions) ? result.resolutions : []) {
|
|
330
|
+
const ref = typeof resolution === "object" && resolution !== null && typeof resolution.finding_ref === "string" ? resolution.finding_ref : null;
|
|
331
|
+
if (ref)
|
|
332
|
+
repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
|
|
333
|
+
}
|
|
272
334
|
}
|
|
273
335
|
for (const { finding_ref: findingRef, finding } of findings) {
|
|
274
336
|
const item = decisions.get(findingRef);
|
|
@@ -307,7 +369,7 @@ function validateReviewerResult(_context, work, _run) {
|
|
|
307
369
|
if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
|
|
308
370
|
throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
|
|
309
371
|
}
|
|
310
|
-
function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const
|
|
372
|
+
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); }); }
|
|
311
373
|
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); }
|
|
312
374
|
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 : []; }); }
|
|
313
375
|
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")) {
|
|
@@ -319,7 +381,7 @@ function validateRepairChecks(context, input) { const known = new Set(acceptedCo
|
|
|
319
381
|
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 });
|
|
320
382
|
selected[item.finding_ref] = refs;
|
|
321
383
|
} return selected; }
|
|
322
|
-
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)); }
|
|
384
|
+
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; }); }
|
|
323
385
|
export function isCodeReviewStageRepair(work, root) {
|
|
324
386
|
if (reviewFindingIds(work).length)
|
|
325
387
|
return true;
|
|
@@ -333,25 +395,15 @@ export function isCodeReviewStageRepair(work, root) {
|
|
|
333
395
|
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
334
396
|
}
|
|
335
397
|
function reviewFindingIds(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
336
|
-
return []; const
|
|
398
|
+
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] : []) : []; }
|
|
337
399
|
function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
338
|
-
return []; const
|
|
400
|
+
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") : []))] : []; }
|
|
339
401
|
function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
|
|
340
|
-
function effectiveMode(
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
return Array.isArray(report.semantic?.changed_files) ? report.semantic.changed_files.filter((item) => typeof item === "string") : [];
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
return [];
|
|
347
|
-
} }
|
|
402
|
+
function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if (index.variables?.["merge.source_repair"])
|
|
403
|
+
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")
|
|
404
|
+
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." }; }
|
|
348
405
|
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
349
|
-
function changedPaths(workspaceRoot) {
|
|
350
|
-
return execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replace(/^.* -> /, ""));
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
return [];
|
|
354
|
-
} }
|
|
406
|
+
function changedPaths(workspaceRoot) { return workspaceChangedPaths(workspaceRoot); }
|
|
355
407
|
function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
|
356
408
|
function stageStatus(run, name) { try {
|
|
357
409
|
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
|
|
@@ -359,6 +411,14 @@ function stageStatus(run, name) { try {
|
|
|
359
411
|
catch {
|
|
360
412
|
return null;
|
|
361
413
|
} }
|
|
414
|
+
function reviewCycle(run) { try {
|
|
415
|
+
const value = JSON.parse(run.index_json);
|
|
416
|
+
const repair = value.variables?.["merge.source_repair"];
|
|
417
|
+
return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.cycle === "string" ? String(repair.cycle) : "initial";
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return "initial";
|
|
421
|
+
} }
|
|
362
422
|
function stageStartedAt(run, fallback) { try {
|
|
363
423
|
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === stage)?.started_at ?? fallback;
|
|
364
424
|
}
|
|
@@ -368,11 +428,11 @@ catch {
|
|
|
368
428
|
function runRef(runId, home, file) { const relative = path.relative(home, file).split(path.sep).join("/"); return relative && !relative.startsWith("../") ? `run://${runId}/${relative}` : file; }
|
|
369
429
|
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")
|
|
370
430
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
371
|
-
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root,
|
|
431
|
+
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)
|
|
372
432
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
373
|
-
function requireHome(run) { if (!run.
|
|
374
|
-
throw new AppError("runtime_missing", "RUN
|
|
375
|
-
function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage code-review --decision-file ${JSON.stringify(decision)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl
|
|
433
|
+
function requireHome(run) { if (!run.run_root)
|
|
434
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
435
|
+
export function finishCommand(context, runId, projectRoot, decision, retryCheckId) { const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : ""; return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code-review --decision-file ${JSON.stringify(decision)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`); }
|
|
376
436
|
function findFiles(root, name) { if (!fs.existsSync(root))
|
|
377
437
|
return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
378
438
|
const file = path.join(root, entry.name);
|