@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,43 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
|
|
3
|
+
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
6
7
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { aggregateCheckDeclarations,
|
|
8
|
+
import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
8
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
|
-
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
|
+
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
11
|
import { validateSchema } from "./schema-validation.js";
|
|
11
|
-
import {
|
|
12
|
+
import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
|
|
13
|
+
import { stageCycle, stageGraphScope } from "./stage-work-graph.js";
|
|
12
14
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
15
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
16
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph,
|
|
17
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
16
18
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
19
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
|
-
import {
|
|
20
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
19
21
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
20
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
22
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
23
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
24
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
25
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
26
|
const stage = "code";
|
|
23
27
|
const stageDir = vnextStageDirectory("code");
|
|
24
|
-
|
|
28
|
+
/** A controller prepares slow, deterministic project tooling before it asks an
|
|
29
|
+
* agent to enter CODE or a restored CODE_REVIEW. The subsequent lifecycle tool call remains the sole
|
|
30
|
+
* authority for attaching and binding the Stage. */
|
|
31
|
+
export async function prepareVnextCodeWorkspace(context, input) {
|
|
32
|
+
if (context.env.DD_FLOW_CODE_READINESS_OWNER !== input.ownerId) {
|
|
33
|
+
throw new AppError("code_readiness_owner_mismatch", "CODE readiness preparation must use its controller's frozen owner", 1, { owner_id: input.ownerId });
|
|
34
|
+
}
|
|
25
35
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
26
36
|
const project = requireProjectByRoot(context, projectRoot);
|
|
27
37
|
const run = requireRun(context, project.id, input.runId);
|
|
28
|
-
|
|
38
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
39
|
+
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
|
|
40
|
+
const root = path.join(requireHome(run), stageDir);
|
|
41
|
+
fs.mkdirSync(root, { recursive: true });
|
|
42
|
+
return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
|
|
43
|
+
}
|
|
44
|
+
export function prepareVnextCodeStart(context, input) {
|
|
45
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
46
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
47
|
+
const run = requireRun(context, project.id, input.runId);
|
|
48
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
29
49
|
const home = requireHome(run);
|
|
30
50
|
requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
31
51
|
const existingRoot = path.join(home, stageDir);
|
|
32
52
|
const existingPrompt = path.join(existingRoot, "stage-prompt.md");
|
|
33
53
|
const existingStage = stageStatus(run, stage);
|
|
54
|
+
const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
|
|
55
|
+
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
56
|
+
if (input.externalContext)
|
|
57
|
+
prepareExternalStageContext({ stageRoot: existingRoot, loaded: input.externalContext });
|
|
58
|
+
prepareOutputFile(existingPrompt);
|
|
59
|
+
prepareOutputFile(path.join(existingRoot, "fanout.json"));
|
|
60
|
+
const common = { projectRoot, project, run, home, root: existingRoot, promptPath: existingPrompt, rootWork, sourceRepairAttempt };
|
|
34
61
|
if (existingStage && fs.existsSync(existingPrompt)) {
|
|
35
|
-
|
|
36
|
-
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
37
|
-
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
38
|
-
const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
39
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: existingStage, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: `${flowCommand(context)} work ls --run ${run.id} --ready --project-root ${JSON.stringify(projectRoot)} --json`, finish_command: finishCommand(context, input.runId, projectRoot, path.join(existingRoot, "code-verification.json")) } };
|
|
62
|
+
return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
|
|
40
63
|
}
|
|
64
|
+
if (!sourceRepairAttempt)
|
|
65
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
41
66
|
const batch = path.join(home, "03-plan", "code-work-batch.json");
|
|
42
67
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
43
68
|
projectRoot,
|
|
@@ -53,15 +78,29 @@ export async function startVnextCode(context, input) {
|
|
|
53
78
|
file: batch,
|
|
54
79
|
projectRoot: run.workspace_root,
|
|
55
80
|
ddFlowHome: context.ddFlowHome,
|
|
56
|
-
runId: input.runId
|
|
81
|
+
runId: input.runId,
|
|
82
|
+
runRoot: home
|
|
57
83
|
});
|
|
58
84
|
validateWorkBatchFile(batch);
|
|
59
|
-
const
|
|
85
|
+
const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
86
|
+
const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
87
|
+
return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"] };
|
|
88
|
+
}
|
|
89
|
+
export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
|
|
90
|
+
const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
|
|
91
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
92
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
93
|
+
if (prepared.resumed) {
|
|
94
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
95
|
+
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
96
|
+
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
97
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
98
|
+
}
|
|
60
99
|
fs.mkdirSync(root, { recursive: true });
|
|
61
100
|
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness });
|
|
101
|
+
// Graph rendering may issue managed child commands; it belongs to execution.
|
|
102
|
+
const graph = codeWorkGraph(context, project.id, run.id);
|
|
103
|
+
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph });
|
|
65
104
|
fs.writeFileSync(promptPath, prompt);
|
|
66
105
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
67
106
|
const binding = bindStageCoordinatorWork(context, {
|
|
@@ -72,16 +111,17 @@ export async function startVnextCode(context, input) {
|
|
|
72
111
|
resultPath: path.join(root, "stage-report.json"),
|
|
73
112
|
...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
|
|
74
113
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
if (!sourceRepairAttempt) {
|
|
115
|
+
attachFlowRunStage(context, {
|
|
116
|
+
projectRoot,
|
|
117
|
+
runId: input.runId,
|
|
118
|
+
stage,
|
|
119
|
+
dir: stageDir,
|
|
120
|
+
status: "running",
|
|
121
|
+
dataSchemaId: "dd-flow/stage-report@1"
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
85
125
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
86
126
|
return {
|
|
87
127
|
ok: true,
|
|
@@ -95,7 +135,7 @@ export async function startVnextCode(context, input) {
|
|
|
95
135
|
graph,
|
|
96
136
|
orchestration,
|
|
97
137
|
readiness,
|
|
98
|
-
next: { refresh_graph_command:
|
|
138
|
+
next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) }
|
|
99
139
|
};
|
|
100
140
|
}
|
|
101
141
|
export async function finishVnextCode(context, input) {
|
|
@@ -104,8 +144,10 @@ export async function finishVnextCode(context, input) {
|
|
|
104
144
|
const run = requireRun(context, project.id, input.runId);
|
|
105
145
|
const home = requireHome(run);
|
|
106
146
|
const root = path.join(home, stageDir);
|
|
147
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
107
148
|
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
108
149
|
if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
|
|
150
|
+
resumeAcceptedCodeStage(context, run, projectRoot, stage, JSON.parse(fs.readFileSync(path.join(root, "stage-report.json"), "utf8")), nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))));
|
|
109
151
|
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: path.join(root, "stage-report.json"), next_action: nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))) };
|
|
110
152
|
}
|
|
111
153
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
@@ -134,58 +176,67 @@ export async function finishVnextCode(context, input) {
|
|
|
134
176
|
throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
|
|
135
177
|
}
|
|
136
178
|
// Reject a malformed semantic receipt before running the expensive gate.
|
|
137
|
-
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id });
|
|
138
|
-
|
|
139
|
-
|
|
179
|
+
const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
180
|
+
validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
181
|
+
if (verification.verdict === "blocked") {
|
|
182
|
+
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
|
|
183
|
+
}
|
|
184
|
+
if (verification.verdict === "passed" && verification.unresolved.length > 0) {
|
|
185
|
+
throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
|
|
186
|
+
}
|
|
187
|
+
if (verification.verdict === "needs_repair") {
|
|
188
|
+
const repair = addVnextCodeRepair(context, {
|
|
189
|
+
projectRoot,
|
|
190
|
+
runId: run.id,
|
|
191
|
+
originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
|
|
192
|
+
semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
|
|
193
|
+
verificationPath: input.verificationFile,
|
|
194
|
+
verification,
|
|
195
|
+
objective: verification.summary
|
|
196
|
+
});
|
|
197
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. 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. After that Work completes, update code-verification.json and invoke this same stage finish command again." };
|
|
140
198
|
}
|
|
141
199
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
|
|
152
|
-
projectId: project.id,
|
|
153
|
-
runId: run.id,
|
|
154
|
-
runHome: home,
|
|
155
|
-
workspaceRoot: run.workspace_root,
|
|
156
|
-
scope: "aggregate",
|
|
157
|
-
checks,
|
|
158
|
-
...(input.progress ? { progress: input.progress } : {})
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
release();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
200
|
+
if (input.retryCheckId) {
|
|
201
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
202
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
203
|
+
}
|
|
204
|
+
const receipts = await runCodeChecks(context, {
|
|
205
|
+
projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
|
|
206
|
+
...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
|
|
207
|
+
...(input.progress ? { progress: input.progress } : {})
|
|
208
|
+
});
|
|
165
209
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
166
210
|
if (failed.length) {
|
|
211
|
+
const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
|
|
167
212
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
168
213
|
failures: failed,
|
|
169
214
|
outcome: "repair_required",
|
|
170
215
|
retry_after_workspace_change: true,
|
|
171
216
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
172
|
-
|
|
217
|
+
continuation,
|
|
218
|
+
instruction: "Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage."
|
|
173
219
|
});
|
|
174
220
|
}
|
|
175
|
-
const
|
|
176
|
-
const
|
|
221
|
+
const finalReceipts = receipts;
|
|
222
|
+
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
|
|
223
|
+
const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
|
|
224
|
+
if (unresolvedAcceptance.length)
|
|
225
|
+
throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
|
|
177
226
|
const now = context.now();
|
|
178
227
|
const timing = stageTiming(home, stage, now);
|
|
179
|
-
|
|
228
|
+
// Work result paths are provenance, not a mutable filesystem oracle. The
|
|
229
|
+
// stage reports one shared Git observation; ignored machine repairs are
|
|
230
|
+
// rejected before a Work can become completed.
|
|
231
|
+
const changedPaths = workspaceChangedPaths(run.workspace_root);
|
|
180
232
|
const next = nextAction(run, changedPaths);
|
|
181
|
-
writeProtocolFlowStatus(run.workspace_root, home, run.id, next === "start_code_review" ? "CODE complete; CODE-REVIEW is next." : next === "start_merge" ? "CODE complete; MERGE is queued." : "CODE complete; this RUN reached its configured terminal boundary.");
|
|
182
233
|
const report = {
|
|
183
234
|
schema_id: "dd-flow/stage-report@1",
|
|
184
235
|
run_id: run.id,
|
|
185
236
|
stage,
|
|
186
237
|
generated_at: now,
|
|
187
238
|
verdict: "done",
|
|
188
|
-
verification:
|
|
239
|
+
verification: projectedVerification,
|
|
189
240
|
semantic: {
|
|
190
241
|
result: `Completed ${works.length} CODE Work item${works.length === 1 ? "" : "s"}; ${finalReceipts.filter((receipt) => receipt.status === "passed").length} current final check receipt${finalReceipts.length === 1 ? "" : "s"} passed.`,
|
|
191
242
|
acceptance: [...expected],
|
|
@@ -207,37 +258,50 @@ export async function finishVnextCode(context, input) {
|
|
|
207
258
|
wall_clock_ms: timing.wall_clock_ms,
|
|
208
259
|
git: gitFacts(run.workspace_root),
|
|
209
260
|
graph: codeWorkGraph(context, project.id, run.id),
|
|
210
|
-
session_stats_command:
|
|
211
|
-
usage_stats_command:
|
|
261
|
+
session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]),
|
|
262
|
+
usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"])
|
|
212
263
|
},
|
|
213
264
|
artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
|
|
214
265
|
validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
|
|
215
266
|
};
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
267
|
+
// Creating the MERGE request is part of accepting this terminal handoff.
|
|
268
|
+
// Do it before materialising a terminal stage report or changing RUN state,
|
|
269
|
+
// so an invalid merge policy leaves CODE safely running and retryable.
|
|
270
|
+
let mergeRequest = null;
|
|
271
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
272
|
+
const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
273
|
+
withStageSettlement(context, () => {
|
|
274
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
275
|
+
const currentWorks = codeWorks(context, project.id, run.id);
|
|
276
|
+
if (currentWorks.length !== works.length || currentWorks.some(work => work.status !== "completed" || !works.some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
|
|
277
|
+
throw new AppError("code_graph_incomplete", "CODE results changed while the final checks were running", 2);
|
|
278
|
+
}
|
|
279
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
280
|
+
writeReport(root, report);
|
|
281
|
+
completeFlowRunStage(context, {
|
|
282
|
+
projectRoot,
|
|
283
|
+
runId: run.id,
|
|
284
|
+
stage,
|
|
285
|
+
status: "done",
|
|
286
|
+
data: "stage-report.json",
|
|
287
|
+
dataSchemaId: "dd-flow/stage-report@1",
|
|
288
|
+
report: "stage-report.md",
|
|
289
|
+
stageReport: "stage-report.html"
|
|
290
|
+
});
|
|
291
|
+
const rootWork = requireRootWork(context, project.id, run.id);
|
|
292
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
293
|
+
type: "code_accepted",
|
|
294
|
+
work_id: rootWork.work_id,
|
|
295
|
+
checks: finalReceipts.map((receipt) => receipt.id)
|
|
296
|
+
});
|
|
297
|
+
if (next === "code_completed") {
|
|
298
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
299
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
303
|
+
}
|
|
232
304
|
});
|
|
233
|
-
if (next === "code_completed") {
|
|
234
|
-
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
235
|
-
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
239
|
-
}
|
|
240
|
-
const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
|
|
241
305
|
return {
|
|
242
306
|
ok: true,
|
|
243
307
|
run_id: run.id,
|
|
@@ -245,18 +309,57 @@ export async function finishVnextCode(context, input) {
|
|
|
245
309
|
outcome: "accepted",
|
|
246
310
|
report_path: path.join(root, "stage-report.json"),
|
|
247
311
|
next_action: next,
|
|
248
|
-
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command:
|
|
312
|
+
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"])) } } : next === "start_merge" ? { 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"])) } } : {})
|
|
249
313
|
};
|
|
250
314
|
}
|
|
251
|
-
export function
|
|
315
|
+
export function registerAggregateCodeRepair(context, input) {
|
|
316
|
+
const project = requireProjectByRoot(context, input.projectRoot);
|
|
317
|
+
const origins = codeWorks(context, project.id, input.runId).filter(work => !packet(work)?.repair && work.status === "completed");
|
|
318
|
+
const failedDeclarations = new Set(input.failures.flatMap(receipt => [receipt.declaration_id, ...receipt.check_refs]));
|
|
319
|
+
const objective = ["Investigate and repair all retained aggregate check failures while preserving accepted requirements and non-goals. If evidence proves an external blocker or a contradiction, report it; do not invent a product change.", ...input.failures.map(receipt => `Receipt: ${receipt.receipt_path}\nstdout: ${receipt.stdout_path}\nstderr: ${receipt.stderr_path}`)].join("\n\n");
|
|
320
|
+
try {
|
|
321
|
+
return ensureAggregateRepair(context, { projectId: project.id, runId: input.runId, stage: input.stage, failures: input.failures, origins: origins.map(work => work.work_id), ...(input.generation !== undefined ? { generation: input.generation } : {}) }, () => String(addVnextCodeRepair(context, { registrationOwned: true, projectRoot: input.projectRoot, runId: input.runId, stage: input.stage, ...(input.stage === "code-review" ? { reviewCycle: stageCycle(context, project.id, input.runId, input.stage) } : {}), checkReceiptId: input.failures[0].id, originWorkIds: origins.map(work => work.work_id), objective, reviewChecks: input.checks.filter(check => failedDeclarations.has(check.id) || failedDeclarations.has(check.canonical_ref ?? "")) }).repair_work_id));
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (!(error instanceof AppError))
|
|
325
|
+
throw error;
|
|
326
|
+
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, stage: input.stage, run_id: input.runId, failures: input.failures, cause: { code: input.stage === "code" ? "code_gate_failed" : "code_review_gate_failed", message: "Aggregate checks failed; repair registration was rejected" } });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function prepareVnextCodeRepair(context, input) {
|
|
252
330
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
253
331
|
const project = requireProjectByRoot(context, projectRoot);
|
|
254
332
|
const run = requireRun(context, project.id, input.runId);
|
|
255
333
|
const home = requireHome(run);
|
|
256
|
-
if (
|
|
334
|
+
if (input.reviewCycle) {
|
|
335
|
+
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle && (input.checkReceiptId ? packet(work)?.repair?.check_receipt_id === input.checkReceiptId : Boolean(packet(work)?.repair?.review_findings?.length)));
|
|
336
|
+
if (previous)
|
|
337
|
+
return { kind: "replay", project, run, previous };
|
|
338
|
+
}
|
|
339
|
+
if (typeof input.objective !== "string" || !input.objective.trim())
|
|
257
340
|
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
258
|
-
if (
|
|
259
|
-
throw new AppError("validation", "
|
|
341
|
+
if (input.semanticUnresolved !== undefined && (!Array.isArray(input.semanticUnresolved) || input.semanticUnresolved.some(value => typeof value !== "string" || !value.trim())))
|
|
342
|
+
throw new AppError("validation", "Semantic unresolved items must be nonempty strings", 2);
|
|
343
|
+
if (input.reviewFindings !== undefined && !Array.isArray(input.reviewFindings))
|
|
344
|
+
throw new AppError("validation", "Review findings must be an array", 2);
|
|
345
|
+
if (input.reviewChecks !== undefined && !Array.isArray(input.reviewChecks))
|
|
346
|
+
throw new AppError("validation", "Review checks must be an array", 2);
|
|
347
|
+
if (!Array.isArray(input.originWorkIds) || !input.originWorkIds.length || input.originWorkIds.some(id => typeof id !== "string" || !id.trim()))
|
|
348
|
+
throw new AppError("validation", "Repair requires at least one origin Work", 2);
|
|
349
|
+
if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
|
|
350
|
+
throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
|
|
351
|
+
}
|
|
352
|
+
if ([Boolean(input.checkReceiptId), Boolean(input.reviewFindings?.length), Boolean(input.semanticUnresolved?.length)].filter(Boolean).length !== 1)
|
|
353
|
+
throw new AppError("validation", "Repair requires exactly one evidence source", 2);
|
|
354
|
+
if (input.semanticUnresolved?.length) {
|
|
355
|
+
if (!input.verificationPath)
|
|
356
|
+
throw new AppError("usage", "Semantic repair requires a verification file", 2);
|
|
357
|
+
const verification = input.verification ?? readVerification(context, { file: input.verificationPath, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
358
|
+
if (input.verification !== undefined)
|
|
359
|
+
validateSchema({ schemaName: "code-verification", file: input.verificationPath, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
360
|
+
const unresolved = verification.unresolved.length ? verification.unresolved : [verification.summary];
|
|
361
|
+
if (verification.verdict !== "needs_repair" || input.semanticUnresolved.some(value => !unresolved.includes(value)))
|
|
362
|
+
throw new AppError("validation", "Semantic repair must reference unresolved items from a needs_repair verification", 2);
|
|
260
363
|
}
|
|
261
364
|
const receipt = input.checkReceiptId ? context.db.get("SELECT id, declaration_id, run_id, command, status, receipt_path, stdout_path, stderr_path FROM check_receipts WHERE project_id = ? AND id = ?", [
|
|
262
365
|
project.id,
|
|
@@ -269,11 +372,19 @@ export function addVnextCodeRepair(context, input) {
|
|
|
269
372
|
}
|
|
270
373
|
const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
|
|
271
374
|
const packets = origins.map((work) => packet(work));
|
|
272
|
-
const invariantPackets = receipt
|
|
375
|
+
const invariantPackets = receipt || input.semanticUnresolved?.length
|
|
273
376
|
? codeWorks(context, project.id, run.id).map((work) => packet(work)).filter((value) => !value.repair)
|
|
274
377
|
: packets;
|
|
275
378
|
const first = packets[0];
|
|
276
|
-
|
|
379
|
+
// A later repair may be caused by any accepted aggregate declaration, not
|
|
380
|
+
// only the checks copied into its immediate repair parent. Otherwise a
|
|
381
|
+
// repair that fixes one failed aggregate check can make a second, already
|
|
382
|
+
// declared aggregate failure impossible to repair. The immutable source
|
|
383
|
+
// of repair eligibility is the original CODE graph.
|
|
384
|
+
const declaredChecks = effectiveCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
|
|
385
|
+
.map((work) => packet(work))
|
|
386
|
+
.filter((value) => !value.repair)
|
|
387
|
+
.flatMap((value) => value.checks), ["work", "code", "readiness", "merge"]);
|
|
277
388
|
const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
|
|
278
389
|
if (receipt && !failedCheck) {
|
|
279
390
|
throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
|
|
@@ -281,7 +392,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
281
392
|
declaration_id: receipt.declaration_id
|
|
282
393
|
});
|
|
283
394
|
}
|
|
284
|
-
const
|
|
395
|
+
const semanticRepair = Boolean(input.semanticUnresolved?.length);
|
|
396
|
+
const key = input.reviewFindings?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
|
|
285
397
|
const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
|
|
286
398
|
const repair = {
|
|
287
399
|
schema_id: "dd-flow/code-work-packet@5",
|
|
@@ -291,22 +403,27 @@ export function addVnextCodeRepair(context, input) {
|
|
|
291
403
|
repair: {
|
|
292
404
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
293
405
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
294
|
-
...(input.
|
|
406
|
+
...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
|
|
407
|
+
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
|
|
408
|
+
// `run_at` remains the accepted aggregate placement. This separate
|
|
409
|
+
// repair obligation is deliberately executed before this repair Work is
|
|
410
|
+
// accepted, so a local fix cannot close without re-proving its cause.
|
|
411
|
+
verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
|
|
295
412
|
},
|
|
296
413
|
task: input.objective,
|
|
297
414
|
semantic_spine: {
|
|
298
|
-
user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
|
|
415
|
+
user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : semanticRepair ? `Close the unresolved CODE verification: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
|
|
299
416
|
component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
|
|
300
417
|
must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
|
|
301
418
|
non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
|
|
302
|
-
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : `Resolve CODE-REVIEW finding(s): ${input.
|
|
419
|
+
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindings.map((finding) => finding.finding_ref).join(", ")}`
|
|
303
420
|
},
|
|
304
421
|
requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
|
|
305
422
|
acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
|
|
306
423
|
// A CODE-REVIEW repair changes delivered code/evidence, never the
|
|
307
424
|
// already accepted PLAN or its ownership declaration.
|
|
308
425
|
document_updates: [],
|
|
309
|
-
required_read: unique([...(receipt ? [receipt.receipt_path] : input.
|
|
426
|
+
required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewFindings?.flatMap((finding) => finding.evidence_refs) ?? []), ...(semanticRepair && input.verificationPath ? [input.verificationPath] : []), ...packets.flatMap((value) => value.required_read)]),
|
|
310
427
|
discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
|
|
311
428
|
// These are collision-avoidance hints. Receipt paths enrich the coordinator
|
|
312
429
|
// picture but never restrict the repair's project-local edits.
|
|
@@ -314,41 +431,44 @@ export function addVnextCodeRepair(context, input) {
|
|
|
314
431
|
// Receipts record the resolved shell command. A repair must retain the
|
|
315
432
|
// accepted declaration (including its immutable @check alias) and only
|
|
316
433
|
// change when it runs, so work finish can validate it again.
|
|
317
|
-
checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}) }),
|
|
434
|
+
checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) }),
|
|
318
435
|
provides_checks: [],
|
|
319
436
|
stop_conditions: unique([
|
|
320
437
|
...invariantPackets.flatMap((value) => value.stop_conditions),
|
|
321
438
|
...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : ["Do not modify accepted PLAN artifacts, code-work-batch.json, or review decisions. Stop and report if the finding cannot be repaired within accepted scope."])
|
|
322
439
|
]),
|
|
323
440
|
depends_on: origins.map((work) => work.work_id),
|
|
324
|
-
result_schema: "dd-flow/code-work-result@
|
|
441
|
+
result_schema: "dd-flow/code-work-result@3",
|
|
442
|
+
...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
|
|
325
443
|
};
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
const id =
|
|
444
|
+
const parentWorkId = requireRootWork(context, project.id, run.id).work_id;
|
|
445
|
+
const items = prepareWorkBatch(context, { parentWorkId, value: { works: [repair] } });
|
|
446
|
+
return { kind: "create", project, run, input, parentWorkId, items, key, receipt };
|
|
447
|
+
}
|
|
448
|
+
export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
|
|
449
|
+
const { project, run } = prepared;
|
|
450
|
+
if (prepared.kind === "replay")
|
|
451
|
+
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
452
|
+
const { parentWorkId, items, key, receipt } = prepared;
|
|
453
|
+
const create = () => String(addWorkBatch(context, { parentWorkId, prepared: items, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"), ...(input.reviewCycle ? { cycle: input.reviewCycle } : {}) }).work_ids[key]);
|
|
454
|
+
const id = input.registrationOwned ? create() : ensureRepairIntent(context, {
|
|
455
|
+
projectId: project.id, runId: run.id, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"),
|
|
456
|
+
causes: input.checkReceiptId ? { receipt: input.checkReceiptId, origins: [...input.originWorkIds].sort() } : input.reviewCycle ? { review_cycle: input.reviewCycle, findings: input.reviewFindings } : { verification: input.verification, unresolved: input.semanticUnresolved, origins: [...input.originWorkIds].sort() }
|
|
457
|
+
}, create).work_id;
|
|
337
458
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
|
|
338
459
|
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
339
460
|
type: "code_repair_created",
|
|
340
461
|
work_id: id,
|
|
341
|
-
origin_work_ids: input.originWorkIds,
|
|
342
|
-
...(receipt ? { check_receipt_id: receipt.id } : { review_finding_ids: input.
|
|
462
|
+
origin_work_ids: prepared.input.originWorkIds,
|
|
463
|
+
...(receipt ? { check_receipt_id: receipt.id } : prepared.input.reviewFindings?.length ? { review_finding_ids: prepared.input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: prepared.input.semanticUnresolved })
|
|
343
464
|
});
|
|
344
465
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
345
466
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
346
467
|
}
|
|
347
|
-
/**
|
|
468
|
+
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
348
469
|
export function selectRepairChecks(input) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
: (input.reviewChecks ?? []).map((check) => ({ ...check, purpose: `Prove the CODE-REVIEW repair: ${check.purpose}`, run_at: "work" }));
|
|
470
|
+
const candidates = input.failedCheck ? [input.failedCheck, ...(input.reviewChecks ?? [])] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
471
|
+
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
352
472
|
return uniqueBy(checks, (check) => check.id);
|
|
353
473
|
}
|
|
354
474
|
function receiptRepairPaths(workspaceRoot, receipt) {
|
|
@@ -376,10 +496,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
|
|
|
376
496
|
return [...paths].sort();
|
|
377
497
|
}
|
|
378
498
|
function coordinatorPrompt(context, input) {
|
|
379
|
-
const graph
|
|
380
|
-
const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.run.id });
|
|
381
|
-
const capacity = variables.variables?.["runtime.subagent.available_slots"];
|
|
382
|
-
const template = fs.readFileSync(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
499
|
+
const { graph, capacity, template } = input;
|
|
383
500
|
return [
|
|
384
501
|
"<stage_identity>",
|
|
385
502
|
`- RUN: ${input.run.id}`,
|
|
@@ -401,17 +518,18 @@ function coordinatorPrompt(context, input) {
|
|
|
401
518
|
"</code_graph>",
|
|
402
519
|
"",
|
|
403
520
|
"<execution_commands>",
|
|
404
|
-
"
|
|
405
|
-
"A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return:
|
|
406
|
-
`A repairable engine, harness, or environment failure is not a user question.
|
|
521
|
+
"Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]),
|
|
522
|
+
"A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return once: a completed command with a non-zero exit and structured `code_gate_failed` output is its terminal result, not a reason to keep waiting. A repair_required continuation already registered the repair: end this Turn. Do not invoke that Work's start_command from the coordinator Session, inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
|
|
523
|
+
`A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to ${path.join(requireHome(input.run), "works", input.rootWork.work_id, "block-summary.md")}, then record it with a separate command: ${runtimeCommand(context, ["stage", "block", input.run.id, "--stage", "code", "--work", input.rootWork.work_id, "--kind", "<engine|harness|environment>", "--code", "<stable-code>", "--summary-file", `@run/works/${input.rootWork.work_id}/block-summary.md`, "--retryable", "--project-root", input.projectRoot, "--json"])}. Choose kind and stable-code from evidence; this example is not an executable continuation. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
407
524
|
`When every CODE and repair Work is completed, write ${path.join(input.root, "code-verification.json")} using the exact contract below. Mark passed only when all accepted requirements and current-gate acceptance criteria are implemented or explicitly evidenced; list every remaining issue in unresolved. Every evidence_refs item must already exist as a relative workspace path or run://${input.run.id}/ path. Do not claim a browser or other check receipt that was not retained. Then finish: ${finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "code-verification.json"))}`,
|
|
408
|
-
"
|
|
525
|
+
"A code_gate_failed response with continuation.kind=repair_required has already registered one repair Work with all failed receipts. End this Turn so the controller can dispatch it in a fresh child. Do not recreate the repair, copy origin IDs, write a repair task file or execute the child start command yourself. After repair, update semantic verification and finish again.",
|
|
409
526
|
"</execution_commands>",
|
|
410
527
|
"",
|
|
411
528
|
"<verification_contract>",
|
|
412
529
|
"```json",
|
|
413
|
-
JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed
|
|
530
|
+
JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed", summary: "Evidence-backed conclusion.", unresolved: [], deviations: [] }, null, 2),
|
|
414
531
|
"```",
|
|
532
|
+
"Allowed verdict values are passed, needs_repair, and blocked. The example is valid JSON using passed; choose exactly one value.",
|
|
415
533
|
"The CLI verifies graph coverage and executes all deterministic checks. Do not duplicate its report; record only your semantic conclusion and evidence.",
|
|
416
534
|
"</verification_contract>",
|
|
417
535
|
"",
|
|
@@ -424,103 +542,57 @@ function coordinatorPrompt(context, input) {
|
|
|
424
542
|
export function profileCommands(workspaceRoot) {
|
|
425
543
|
return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
|
|
426
544
|
}
|
|
427
|
-
function latestReceiptsByCommand(receipts) {
|
|
428
|
-
const latest = new Map();
|
|
429
|
-
for (const receipt of receipts)
|
|
430
|
-
for (const ref of receipt.check_refs)
|
|
431
|
-
latest.set(ref, receipt);
|
|
432
|
-
return [...new Set(latest.values())];
|
|
433
|
-
}
|
|
434
545
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
546
|
+
* The immutable Work result is a repair history. The stage report is a current
|
|
547
|
+
* acceptance projection, so a registered receipt from an older check epoch
|
|
548
|
+
* must never stand in for the receipt that currently proves the declaration.
|
|
438
549
|
*/
|
|
439
|
-
export function
|
|
440
|
-
const expected = new Set(input.checks.map((check) => check.id));
|
|
441
|
-
const current = workspaceFingerprint(input.workspaceRoot);
|
|
442
|
-
const latest = new Map();
|
|
443
|
-
for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
|
|
444
|
-
if (receipt.scope === "aggregate")
|
|
445
|
-
for (const ref of receipt.check_refs)
|
|
446
|
-
if (expected.has(ref))
|
|
447
|
-
latest.set(ref, receipt);
|
|
448
|
-
}
|
|
449
|
-
if (latest.size !== expected.size)
|
|
450
|
-
return null;
|
|
451
|
-
const receipts = input.checks.map((check) => latest.get(check.id));
|
|
452
|
-
return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
|
|
453
|
-
}
|
|
454
|
-
/** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
|
|
455
|
-
function acquireAggregateGateLock(root) {
|
|
456
|
-
const directory = path.join(root, "checks");
|
|
457
|
-
const lock = path.join(directory, ".aggregate-gate.lock");
|
|
458
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
459
|
-
try {
|
|
460
|
-
const descriptor = fs.openSync(lock, "wx");
|
|
461
|
-
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
462
|
-
fs.closeSync(descriptor);
|
|
463
|
-
}
|
|
464
|
-
catch (error) {
|
|
465
|
-
if (!isExistingLock(error))
|
|
466
|
-
throw error;
|
|
467
|
-
const activePid = lockPid(lock);
|
|
468
|
-
if (activePid && processAlive(activePid)) {
|
|
469
|
-
throw new AppError("code_gate_in_progress", "CODE aggregate gate is already running; wait for that exact stage finish command to return", 1, { pid: activePid, lock });
|
|
470
|
-
}
|
|
471
|
-
fs.rmSync(lock, { force: true });
|
|
472
|
-
return acquireAggregateGateLock(root);
|
|
473
|
-
}
|
|
474
|
-
return () => fs.rmSync(lock, { force: true });
|
|
475
|
-
}
|
|
476
|
-
function isExistingLock(error) {
|
|
477
|
-
return Boolean(error && typeof error === "object" && error.code === "EEXIST");
|
|
478
|
-
}
|
|
479
|
-
function lockPid(lock) {
|
|
480
|
-
try {
|
|
481
|
-
const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
|
|
482
|
-
return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function processAlive(pid) {
|
|
489
|
-
try {
|
|
490
|
-
process.kill(pid, 0);
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
catch {
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
export function verificationProjection(works, receipts) {
|
|
550
|
+
export function verificationProjection(works, receipts, options = {}) {
|
|
498
551
|
const packets = works.map((work) => packet(work)).filter((value) => value !== null);
|
|
499
552
|
const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
|
|
500
553
|
const receiptByDeclaration = new Map(receipts.flatMap((receipt) => receipt.check_refs.map((ref) => [ref, receipt])));
|
|
501
|
-
const
|
|
554
|
+
const rawEvidence = new Map();
|
|
502
555
|
for (const work of works) {
|
|
503
556
|
const result = readJsonResult(work.result);
|
|
504
557
|
for (const item of result.evidence ?? [])
|
|
505
|
-
if (item.
|
|
506
|
-
|
|
558
|
+
if (item.obligation_ref)
|
|
559
|
+
rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
|
|
507
560
|
}
|
|
508
561
|
const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
|
|
509
|
-
const
|
|
510
|
-
const
|
|
562
|
+
const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
|
|
563
|
+
const acceptanceEvidence = new Map(acceptance.map((item) => {
|
|
564
|
+
const reported = rawEvidence.get(item.criterion_id) ?? [];
|
|
565
|
+
const current = unique(item.check_refs.flatMap((checkRef) => {
|
|
566
|
+
const receipt = receiptByDeclaration.get(checkRef);
|
|
567
|
+
return receipt?.status === "passed" ? [currentReceiptRef(receipt, options)] : [];
|
|
568
|
+
}));
|
|
569
|
+
const evidenceRefs = unique([...reported.filter((ref) => !historicalReceiptRefs.has(ref)), ...current]);
|
|
570
|
+
const historical = reported.filter((ref) => historicalReceiptRefs.has(ref) && !current.includes(ref));
|
|
571
|
+
return [item.criterion_id, { evidenceRefs, historical }];
|
|
572
|
+
}));
|
|
573
|
+
const hasEvidence = (criterionId) => (acceptanceEvidence.get(criterionId)?.evidenceRefs.length ?? 0) > 0;
|
|
574
|
+
const isPassed = (checkId) => receiptByDeclaration.get(checkId)?.status === "passed";
|
|
511
575
|
return {
|
|
512
576
|
checks: declarations.map((check) => {
|
|
513
577
|
const receipt = receiptByDeclaration.get(check.id);
|
|
514
|
-
|
|
515
|
-
return { id: check.id, run_at: check.run_at, status: receipt?.status ?? (externallyEvidenced ? "evidence_recorded" : ["work", "code", "readiness"].includes(check.run_at) ? "failed" : "not_due"), receipt_ref: receipt?.receipt_path ?? null };
|
|
578
|
+
return { id: check.id, run_at: check.run_at, status: receipt?.status ?? (["work", "code", "readiness"].includes(check.run_at) ? "failed" : "not_due"), receipt_ref: receipt?.receipt_path ?? null };
|
|
516
579
|
}),
|
|
517
580
|
acceptance: acceptance.map((item) => {
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ?
|
|
581
|
+
const confirmed = item.check_refs.every((id) => isPassed(id)) && hasEvidence(item.criterion_id);
|
|
582
|
+
const evidence = acceptanceEvidence.get(item.criterion_id) ?? { evidenceRefs: [], historical: [] };
|
|
583
|
+
return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ? "confirmed" : ["work", "code", "readiness"].includes(item.gate) ? "unresolved" : "not_due", check_refs: item.check_refs, evidence_refs: evidence.evidenceRefs, ...(evidence.historical.length ? { reported_evidence_refs: evidence.historical } : {}) };
|
|
521
584
|
})
|
|
522
585
|
};
|
|
523
586
|
}
|
|
587
|
+
function currentReceiptRef(receipt, options) {
|
|
588
|
+
if (!options.runId || !options.runHome)
|
|
589
|
+
return receipt.receipt_path;
|
|
590
|
+
const relative = path.relative(options.runHome, receipt.receipt_path).split(path.sep).join("/");
|
|
591
|
+
return relative && !relative.startsWith("../") ? `run://${options.runId}/${relative}` : receipt.receipt_path;
|
|
592
|
+
}
|
|
593
|
+
function receiptReferences(receipt, options) {
|
|
594
|
+
return unique([receipt.receipt_path, currentReceiptRef(receipt, options)]);
|
|
595
|
+
}
|
|
524
596
|
function readJsonResult(value) { try {
|
|
525
597
|
const parsed = JSON.parse(value ?? "{}");
|
|
526
598
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -572,75 +644,77 @@ function requireRootWork(context, projectId, runId) {
|
|
|
572
644
|
return work;
|
|
573
645
|
}
|
|
574
646
|
function requireRun(context, projectId, runId) {
|
|
575
|
-
const run = context.db.get("SELECT id, project_id,
|
|
647
|
+
const run = context.db.get("SELECT id, project_id, run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
576
648
|
if (!run)
|
|
577
649
|
throw new AppError("not_found", "RUN is not registered", 1);
|
|
578
650
|
return run;
|
|
579
651
|
}
|
|
580
652
|
/** CODE, not PROTOCOLIZE, makes a checkout ready for project tooling. */
|
|
581
|
-
|
|
653
|
+
export function isReusablePreparedCodeReadiness(value, expected) {
|
|
654
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
655
|
+
return false;
|
|
656
|
+
const receipt = value;
|
|
657
|
+
return receipt.schema_id === "dd-flow/workspace-readiness@2"
|
|
658
|
+
&& receipt.status === "passed"
|
|
659
|
+
&& receipt.prepared_by === expected.ownerId
|
|
660
|
+
&& receipt.workspace_root === expected.workspaceRoot
|
|
661
|
+
&& receipt.command === expected.command
|
|
662
|
+
&& receipt.policy_ref === expected.policyRef;
|
|
663
|
+
}
|
|
664
|
+
function readPreparedReadiness(file) {
|
|
665
|
+
try {
|
|
666
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress, options = {}) {
|
|
582
673
|
const profile = JSON.parse(run.index_json);
|
|
583
674
|
const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
|
|
584
675
|
if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
|
|
585
676
|
throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
|
|
586
677
|
}
|
|
587
678
|
const receiptPath = path.join(root, "workspace-readiness.json");
|
|
588
|
-
const
|
|
589
|
-
if (
|
|
590
|
-
|
|
679
|
+
const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
|
|
680
|
+
if (!options.force && ownerId && isReusablePreparedCodeReadiness(readPreparedReadiness(receiptPath), {
|
|
681
|
+
ownerId,
|
|
682
|
+
workspaceRoot: run.workspace_root,
|
|
683
|
+
command: bootstrap.command,
|
|
684
|
+
policyRef: bootstrap.policy_ref
|
|
685
|
+
})) {
|
|
686
|
+
progress?.("workspace bootstrap already prepared by this controller");
|
|
687
|
+
return readPreparedReadiness(receiptPath);
|
|
688
|
+
}
|
|
689
|
+
// Unowned and stale receipts are never trusted: a direct stage entry still
|
|
690
|
+
// performs its own readiness check. Only the same controller can hand a
|
|
691
|
+
// freshly prepared workspace to its immediately following lifecycle call.
|
|
591
692
|
const startedAt = new Date().toISOString();
|
|
592
693
|
progress?.(`workspace bootstrap started: ${bootstrap.command}`);
|
|
593
|
-
const result = await
|
|
694
|
+
const result = await runWorkspaceBootstrap(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, command: bootstrap.command, artifactDir: "05-code/readiness", progress });
|
|
594
695
|
const bootstrapPassed = result.exit_code === 0 && !result.error;
|
|
595
696
|
const checks = bootstrapPassed && readinessChecks.length
|
|
596
697
|
? await runCodeChecks(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, scope: "aggregate", artifactDir: "05-code/readiness", checks: readinessChecks, ...(progress ? { progress } : {}) })
|
|
597
698
|
: [];
|
|
598
|
-
const receipt = { schema_id: "dd-flow/workspace-readiness@2", workspace_root: run.workspace_root, command: bootstrap.command, policy_ref: bootstrap.policy_ref, started_at: startedAt, finished_at: new Date().toISOString(), status: bootstrapPassed && checks.every((check) => check.status === "passed") ? "passed" : "failed", exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, check_declarations: readinessChecks, checks: checks.map((check) => ({ id: check.id, status: check.status, receipt_path: check.receipt_path })), ...(result.error ? { error: result.error } : {}), receipt_path: receiptPath };
|
|
699
|
+
const receipt = { schema_id: "dd-flow/workspace-readiness@2", workspace_root: run.workspace_root, command: bootstrap.command, policy_ref: bootstrap.policy_ref, started_at: startedAt, finished_at: new Date().toISOString(), status: bootstrapPassed && checks.every((check) => check.status === "passed") ? "passed" : "failed", ...(ownerId ? { prepared_by: ownerId } : {}), process_id: result.process_id, stdout_path: result.stdout_path, stderr_path: result.stderr_path, completion_path: result.completion_path, exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, check_declarations: readinessChecks, checks: checks.map((check) => ({ id: check.id, status: check.status, receipt_path: check.receipt_path })), ...(result.error ? { error: result.error } : {}), receipt_path: receiptPath };
|
|
599
700
|
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
600
701
|
if (receipt.status !== "passed")
|
|
601
702
|
throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
|
|
602
703
|
progress?.("workspace bootstrap passed");
|
|
603
704
|
return receipt;
|
|
604
705
|
}
|
|
605
|
-
function runBootstrap(command, cwd, environment, progress) {
|
|
606
|
-
return new Promise((resolve) => {
|
|
607
|
-
const started = Date.now();
|
|
608
|
-
const child = spawn("/bin/sh", ["-lc", command], { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
|
|
609
|
-
let stdout = "";
|
|
610
|
-
let stderr = "";
|
|
611
|
-
let error = null;
|
|
612
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
613
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
614
|
-
const heartbeat = setInterval(() => progress?.(`workspace bootstrap still running (${Math.floor((Date.now() - started) / 1000)}s)`), 15_000);
|
|
615
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), 15 * 60 * 1000);
|
|
616
|
-
child.once("error", (value) => { error = value.message; });
|
|
617
|
-
child.once("close", (exitCode, signal) => {
|
|
618
|
-
clearInterval(heartbeat);
|
|
619
|
-
clearTimeout(timeout);
|
|
620
|
-
resolve({ exit_code: exitCode, stdout, stderr, error: error ?? (signal ? `terminated by ${signal}` : null) });
|
|
621
|
-
});
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
function readReadiness(file) {
|
|
625
|
-
try {
|
|
626
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
627
|
-
}
|
|
628
|
-
catch {
|
|
629
|
-
return null;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
706
|
function requireHome(run) {
|
|
633
|
-
if (!run.
|
|
634
|
-
throw new AppError("runtime_missing", "RUN
|
|
635
|
-
return run.
|
|
707
|
+
if (!run.run_root)
|
|
708
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
|
|
709
|
+
return run.run_root;
|
|
636
710
|
}
|
|
637
|
-
function finishCommand(context, runId, projectRoot, verificationFile) {
|
|
638
|
-
return
|
|
711
|
+
export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
|
|
712
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code", "--verification-file", verificationFile, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
639
713
|
}
|
|
640
714
|
function readVerification(context, input) {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
715
|
+
return prepareJsonSchema(input.file, "CODE verification", {
|
|
716
|
+
schemaName: "code-verification", projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot
|
|
717
|
+
}).value;
|
|
644
718
|
}
|
|
645
719
|
function verificationForFinish(context, input) {
|
|
646
720
|
return readVerification(context, input);
|
|
@@ -662,6 +736,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
|
|
|
662
736
|
}
|
|
663
737
|
function nextAction(run, changedPaths) {
|
|
664
738
|
const index = JSON.parse(run.index_json);
|
|
739
|
+
if (index.variables?.["merge.source_repair"])
|
|
740
|
+
return "start_code_review";
|
|
665
741
|
const requested = index.settings?.code_review?.mode ?? "auto";
|
|
666
742
|
const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
|
|
667
743
|
const stopTarget = index.execution_profile?.settings?.stop_target;
|
|
@@ -677,19 +753,19 @@ function stageStatus(run, name) {
|
|
|
677
753
|
return null;
|
|
678
754
|
}
|
|
679
755
|
}
|
|
680
|
-
function
|
|
756
|
+
function isMergeSourceRepairAttempt(run) {
|
|
681
757
|
try {
|
|
682
|
-
const value = JSON.parse(
|
|
683
|
-
return
|
|
758
|
+
const value = JSON.parse(run.index_json);
|
|
759
|
+
return Boolean(value.variables?.["merge.source_repair"]);
|
|
684
760
|
}
|
|
685
761
|
catch {
|
|
686
|
-
return
|
|
762
|
+
return false;
|
|
687
763
|
}
|
|
688
764
|
}
|
|
689
|
-
function
|
|
765
|
+
function changedPathsFromReport(file) {
|
|
690
766
|
try {
|
|
691
|
-
const value = JSON.parse(
|
|
692
|
-
return Array.isArray(value.
|
|
767
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
768
|
+
return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
|
|
693
769
|
}
|
|
694
770
|
catch {
|
|
695
771
|
return [];
|
|
@@ -708,6 +784,28 @@ function uniqueBy(values, key) {
|
|
|
708
784
|
return true;
|
|
709
785
|
});
|
|
710
786
|
}
|
|
787
|
+
/** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
|
|
788
|
+
export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
|
|
789
|
+
const index = JSON.parse(run.index_json);
|
|
790
|
+
if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
|
|
791
|
+
return;
|
|
792
|
+
const terminal = next === "code_completed" || next === "code_review_completed";
|
|
793
|
+
if (!terminal && index.next_action === next)
|
|
794
|
+
return;
|
|
795
|
+
withStageSettlement(context, () => {
|
|
796
|
+
if (!terminal) {
|
|
797
|
+
advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
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", [run.project_id, run.id]);
|
|
801
|
+
if (!work)
|
|
802
|
+
throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
|
|
803
|
+
if (work.status !== "completed")
|
|
804
|
+
finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
|
|
805
|
+
if (index.status !== "done")
|
|
806
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
|
|
807
|
+
});
|
|
808
|
+
}
|
|
711
809
|
function writeReport(root, report) {
|
|
712
810
|
writeStageReport(root, report);
|
|
713
811
|
}
|