@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.80
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 +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1342 -375
- package/dist/cli.js +6 -2
- 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.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 +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 +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 +260 -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/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/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 +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- 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 +2 -0
- package/dist/services/hooks.js +553 -285
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- 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/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- 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/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 +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 +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +342 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -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 +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +65 -23
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +707 -156
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,43 +1,66 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { aggregateCheckDeclarations,
|
|
7
|
+
import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
8
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
|
-
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
9
|
+
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
10
|
import { validateSchema } from "./schema-validation.js";
|
|
11
11
|
import { flowCommand } from "./stage-pause.js";
|
|
12
12
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
13
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
14
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph,
|
|
15
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
16
16
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
|
-
import {
|
|
18
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
19
19
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
20
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
22
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
23
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
24
|
const stage = "code";
|
|
23
25
|
const stageDir = vnextStageDirectory("code");
|
|
24
|
-
|
|
26
|
+
/** A controller prepares slow, deterministic project tooling before it asks an
|
|
27
|
+
* agent to enter CODE or a restored CODE_REVIEW. The subsequent lifecycle tool call remains the sole
|
|
28
|
+
* authority for attaching and binding the Stage. */
|
|
29
|
+
export async function prepareVnextCodeWorkspace(context, input) {
|
|
30
|
+
if (context.env.DD_FLOW_CODE_READINESS_OWNER !== input.ownerId) {
|
|
31
|
+
throw new AppError("code_readiness_owner_mismatch", "CODE readiness preparation must use its controller's frozen owner", 1, { owner_id: input.ownerId });
|
|
32
|
+
}
|
|
25
33
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
26
34
|
const project = requireProjectByRoot(context, projectRoot);
|
|
27
35
|
const run = requireRun(context, project.id, input.runId);
|
|
28
|
-
|
|
36
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
37
|
+
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
|
|
38
|
+
const root = path.join(requireHome(run), stageDir);
|
|
39
|
+
fs.mkdirSync(root, { recursive: true });
|
|
40
|
+
return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
|
|
41
|
+
}
|
|
42
|
+
export function prepareVnextCodeStart(context, input) {
|
|
43
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
44
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
45
|
+
const run = requireRun(context, project.id, input.runId);
|
|
46
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
29
47
|
const home = requireHome(run);
|
|
30
48
|
requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
31
49
|
const existingRoot = path.join(home, stageDir);
|
|
32
50
|
const existingPrompt = path.join(existingRoot, "stage-prompt.md");
|
|
33
51
|
const existingStage = stageStatus(run, stage);
|
|
52
|
+
const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
|
|
53
|
+
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
54
|
+
if (input.externalContext)
|
|
55
|
+
prepareExternalStageContext({ stageRoot: existingRoot, loaded: input.externalContext });
|
|
56
|
+
prepareOutputFile(existingPrompt);
|
|
57
|
+
prepareOutputFile(path.join(existingRoot, "fanout.json"));
|
|
58
|
+
const common = { projectRoot, project, run, home, root: existingRoot, promptPath: existingPrompt, rootWork, sourceRepairAttempt };
|
|
34
59
|
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")) } };
|
|
60
|
+
return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
|
|
40
61
|
}
|
|
62
|
+
if (!sourceRepairAttempt)
|
|
63
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
41
64
|
const batch = path.join(home, "03-plan", "code-work-batch.json");
|
|
42
65
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
43
66
|
projectRoot,
|
|
@@ -57,12 +80,25 @@ export async function startVnextCode(context, input) {
|
|
|
57
80
|
runRoot: home
|
|
58
81
|
});
|
|
59
82
|
validateWorkBatchFile(batch);
|
|
60
|
-
const
|
|
83
|
+
const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
84
|
+
const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
85
|
+
return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"] };
|
|
86
|
+
}
|
|
87
|
+
export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
|
|
88
|
+
const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
|
|
89
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
90
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
91
|
+
if (prepared.resumed) {
|
|
92
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
93
|
+
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
94
|
+
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
95
|
+
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: `${flowCommand(context)} work ls --run ${run.id} --ready --project-root ${JSON.stringify(projectRoot)} --json`, finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
96
|
+
}
|
|
61
97
|
fs.mkdirSync(root, { recursive: true });
|
|
62
98
|
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness });
|
|
99
|
+
// Graph rendering may issue managed child commands; it belongs to execution.
|
|
100
|
+
const graph = codeWorkGraph(context, project.id, run.id);
|
|
101
|
+
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph });
|
|
66
102
|
fs.writeFileSync(promptPath, prompt);
|
|
67
103
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
68
104
|
const binding = bindStageCoordinatorWork(context, {
|
|
@@ -73,15 +109,16 @@ export async function startVnextCode(context, input) {
|
|
|
73
109
|
resultPath: path.join(root, "stage-report.json"),
|
|
74
110
|
...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
|
|
75
111
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
if (!sourceRepairAttempt) {
|
|
113
|
+
attachFlowRunStage(context, {
|
|
114
|
+
projectRoot,
|
|
115
|
+
runId: input.runId,
|
|
116
|
+
stage,
|
|
117
|
+
dir: stageDir,
|
|
118
|
+
status: "running",
|
|
119
|
+
dataSchemaId: "dd-flow/stage-report@1"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
85
122
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
86
123
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
87
124
|
return {
|
|
@@ -105,8 +142,10 @@ export async function finishVnextCode(context, input) {
|
|
|
105
142
|
const run = requireRun(context, project.id, input.runId);
|
|
106
143
|
const home = requireHome(run);
|
|
107
144
|
const root = path.join(home, stageDir);
|
|
145
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
108
146
|
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
109
147
|
if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
|
|
148
|
+
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"))));
|
|
110
149
|
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"))) };
|
|
111
150
|
}
|
|
112
151
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
@@ -135,7 +174,8 @@ export async function finishVnextCode(context, input) {
|
|
|
135
174
|
throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
|
|
136
175
|
}
|
|
137
176
|
// Reject a malformed semantic receipt before running the expensive gate.
|
|
138
|
-
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
177
|
+
const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
178
|
+
validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
139
179
|
if (verification.verdict === "blocked") {
|
|
140
180
|
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." };
|
|
141
181
|
}
|
|
@@ -149,34 +189,21 @@ export async function finishVnextCode(context, input) {
|
|
|
149
189
|
originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
|
|
150
190
|
semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
|
|
151
191
|
verificationPath: input.verificationFile,
|
|
192
|
+
verification,
|
|
152
193
|
objective: verification.summary
|
|
153
194
|
});
|
|
154
|
-
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted.
|
|
195
|
+
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." };
|
|
155
196
|
}
|
|
156
197
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
|
|
167
|
-
projectId: project.id,
|
|
168
|
-
runId: run.id,
|
|
169
|
-
runHome: home,
|
|
170
|
-
workspaceRoot: run.workspace_root,
|
|
171
|
-
scope: "aggregate",
|
|
172
|
-
checks,
|
|
173
|
-
...(input.progress ? { progress: input.progress } : {})
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
release();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
198
|
+
if (input.retryCheckId) {
|
|
199
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
200
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
201
|
+
}
|
|
202
|
+
const receipts = await runCodeChecks(context, {
|
|
203
|
+
projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
|
|
204
|
+
...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
|
|
205
|
+
...(input.progress ? { progress: input.progress } : {})
|
|
206
|
+
});
|
|
180
207
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
181
208
|
if (failed.length) {
|
|
182
209
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
@@ -184,23 +211,21 @@ export async function finishVnextCode(context, input) {
|
|
|
184
211
|
outcome: "repair_required",
|
|
185
212
|
retry_after_workspace_change: true,
|
|
186
213
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
187
|
-
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
|
|
214
|
+
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`,
|
|
215
|
+
retry_command: finishCommand(context, run.id, projectRoot, input.verificationFile, failed[0].id)
|
|
188
216
|
});
|
|
189
217
|
}
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: allReceipts, runId: run.id, runHome: home });
|
|
218
|
+
const finalReceipts = receipts;
|
|
219
|
+
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
|
|
193
220
|
const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
|
|
194
221
|
if (unresolvedAcceptance.length)
|
|
195
222
|
throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
|
|
196
223
|
const now = context.now();
|
|
197
224
|
const timing = stageTiming(home, stage, now);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
if (missingReportedPaths.length)
|
|
203
|
-
throw new AppError("changed_path_not_materialized", "CODE Work reported paths that are not changed in the accepted workspace", 2, { paths: missingReportedPaths });
|
|
225
|
+
// Work result paths are provenance, not a mutable filesystem oracle. The
|
|
226
|
+
// stage reports one shared Git observation; ignored machine repairs are
|
|
227
|
+
// rejected before a Work can become completed.
|
|
228
|
+
const changedPaths = workspaceChangedPaths(run.workspace_root);
|
|
204
229
|
const next = nextAction(run, changedPaths);
|
|
205
230
|
const report = {
|
|
206
231
|
schema_id: "dd-flow/stage-report@1",
|
|
@@ -239,31 +264,41 @@ export async function finishVnextCode(context, input) {
|
|
|
239
264
|
// Creating the MERGE request is part of accepting this terminal handoff.
|
|
240
265
|
// Do it before materialising a terminal stage report or changing RUN state,
|
|
241
266
|
// so an invalid merge policy leaves CODE safely running and retryable.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
status
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
let mergeRequest = null;
|
|
268
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
269
|
+
const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
270
|
+
withStageSettlement(context, () => {
|
|
271
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
272
|
+
const currentWorks = codeWorks(context, project.id, run.id);
|
|
273
|
+
if (currentWorks.length !== works.length || currentWorks.some(work => work.status !== "completed" || !works.some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
|
|
274
|
+
throw new AppError("code_graph_incomplete", "CODE results changed while the final checks were running", 2);
|
|
275
|
+
}
|
|
276
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
277
|
+
writeReport(root, report);
|
|
278
|
+
completeFlowRunStage(context, {
|
|
279
|
+
projectRoot,
|
|
280
|
+
runId: run.id,
|
|
281
|
+
stage,
|
|
282
|
+
status: "done",
|
|
283
|
+
data: "stage-report.json",
|
|
284
|
+
dataSchemaId: "dd-flow/stage-report@1",
|
|
285
|
+
report: "stage-report.md",
|
|
286
|
+
stageReport: "stage-report.html"
|
|
287
|
+
});
|
|
288
|
+
const rootWork = requireRootWork(context, project.id, run.id);
|
|
289
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
290
|
+
type: "code_accepted",
|
|
291
|
+
work_id: rootWork.work_id,
|
|
292
|
+
checks: finalReceipts.map((receipt) => receipt.id)
|
|
293
|
+
});
|
|
294
|
+
if (next === "code_completed") {
|
|
295
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
296
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
300
|
+
}
|
|
259
301
|
});
|
|
260
|
-
if (next === "code_completed") {
|
|
261
|
-
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
262
|
-
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
266
|
-
}
|
|
267
302
|
return {
|
|
268
303
|
ok: true,
|
|
269
304
|
run_id: run.id,
|
|
@@ -271,19 +306,44 @@ export async function finishVnextCode(context, input) {
|
|
|
271
306
|
outcome: "accepted",
|
|
272
307
|
report_path: path.join(root, "stage-report.json"),
|
|
273
308
|
next_action: next,
|
|
274
|
-
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: `${flowCommand(context)} stage start ${run.id} --stage code-review --project-root ${JSON.stringify(projectRoot)} --json` } } : next === "start_merge" ? { 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` } } : {})
|
|
309
|
+
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage code-review --project-root ${JSON.stringify(projectRoot)} --json`) } } : next === "start_merge" ? { 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`) } } : {})
|
|
275
310
|
};
|
|
276
311
|
}
|
|
277
|
-
export function
|
|
312
|
+
export function prepareVnextCodeRepair(context, input) {
|
|
278
313
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
279
314
|
const project = requireProjectByRoot(context, projectRoot);
|
|
280
315
|
const run = requireRun(context, project.id, input.runId);
|
|
281
316
|
const home = requireHome(run);
|
|
282
|
-
if (
|
|
317
|
+
if (input.reviewCycle) {
|
|
318
|
+
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
|
|
319
|
+
if (previous)
|
|
320
|
+
return { kind: "replay", project, run, previous };
|
|
321
|
+
}
|
|
322
|
+
if (typeof input.objective !== "string" || !input.objective.trim())
|
|
283
323
|
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
284
|
-
if (
|
|
324
|
+
if (input.semanticUnresolved !== undefined && (!Array.isArray(input.semanticUnresolved) || input.semanticUnresolved.some(value => typeof value !== "string" || !value.trim())))
|
|
325
|
+
throw new AppError("validation", "Semantic unresolved items must be nonempty strings", 2);
|
|
326
|
+
if (input.reviewFindings !== undefined && !Array.isArray(input.reviewFindings))
|
|
327
|
+
throw new AppError("validation", "Review findings must be an array", 2);
|
|
328
|
+
if (input.reviewChecks !== undefined && !Array.isArray(input.reviewChecks))
|
|
329
|
+
throw new AppError("validation", "Review checks must be an array", 2);
|
|
330
|
+
if (!Array.isArray(input.originWorkIds) || !input.originWorkIds.length || input.originWorkIds.some(id => typeof id !== "string" || !id.trim()))
|
|
331
|
+
throw new AppError("validation", "Repair requires at least one origin Work", 2);
|
|
332
|
+
if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
|
|
285
333
|
throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
|
|
286
334
|
}
|
|
335
|
+
if ([Boolean(input.checkReceiptId), Boolean(input.reviewFindings?.length), Boolean(input.semanticUnresolved?.length)].filter(Boolean).length !== 1)
|
|
336
|
+
throw new AppError("validation", "Repair requires exactly one evidence source", 2);
|
|
337
|
+
if (input.semanticUnresolved?.length) {
|
|
338
|
+
if (!input.verificationPath)
|
|
339
|
+
throw new AppError("usage", "Semantic repair requires a verification file", 2);
|
|
340
|
+
const verification = input.verification ?? readVerification(context, { file: input.verificationPath, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
341
|
+
if (input.verification !== undefined)
|
|
342
|
+
validateSchema({ schemaName: "code-verification", file: input.verificationPath, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
343
|
+
const unresolved = verification.unresolved.length ? verification.unresolved : [verification.summary];
|
|
344
|
+
if (verification.verdict !== "needs_repair" || input.semanticUnresolved.some(value => !unresolved.includes(value)))
|
|
345
|
+
throw new AppError("validation", "Semantic repair must reference unresolved items from a needs_repair verification", 2);
|
|
346
|
+
}
|
|
287
347
|
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 = ?", [
|
|
288
348
|
project.id,
|
|
289
349
|
input.checkReceiptId
|
|
@@ -304,10 +364,10 @@ export function addVnextCodeRepair(context, input) {
|
|
|
304
364
|
// repair that fixes one failed aggregate check can make a second, already
|
|
305
365
|
// declared aggregate failure impossible to repair. The immutable source
|
|
306
366
|
// of repair eligibility is the original CODE graph.
|
|
307
|
-
const declaredChecks =
|
|
367
|
+
const declaredChecks = effectiveCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
|
|
308
368
|
.map((work) => packet(work))
|
|
309
369
|
.filter((value) => !value.repair)
|
|
310
|
-
.flatMap((value) => value.checks));
|
|
370
|
+
.flatMap((value) => value.checks), ["work", "code", "readiness", "merge"]);
|
|
311
371
|
const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
|
|
312
372
|
if (receipt && !failedCheck) {
|
|
313
373
|
throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
|
|
@@ -316,7 +376,7 @@ export function addVnextCodeRepair(context, input) {
|
|
|
316
376
|
});
|
|
317
377
|
}
|
|
318
378
|
const semanticRepair = Boolean(input.semanticUnresolved?.length);
|
|
319
|
-
const key = input.
|
|
379
|
+
const key = input.reviewFindings?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
|
|
320
380
|
const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
|
|
321
381
|
const repair = {
|
|
322
382
|
schema_id: "dd-flow/code-work-packet@5",
|
|
@@ -326,8 +386,12 @@ export function addVnextCodeRepair(context, input) {
|
|
|
326
386
|
repair: {
|
|
327
387
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
328
388
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
329
|
-
...(input.
|
|
330
|
-
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
|
|
389
|
+
...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
|
|
390
|
+
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
|
|
391
|
+
// `run_at` remains the accepted aggregate placement. This separate
|
|
392
|
+
// repair obligation is deliberately executed before this repair Work is
|
|
393
|
+
// accepted, so a local fix cannot close without re-proving its cause.
|
|
394
|
+
verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
|
|
331
395
|
},
|
|
332
396
|
task: input.objective,
|
|
333
397
|
semantic_spine: {
|
|
@@ -335,14 +399,14 @@ export function addVnextCodeRepair(context, input) {
|
|
|
335
399
|
component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
|
|
336
400
|
must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
|
|
337
401
|
non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
|
|
338
|
-
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.
|
|
402
|
+
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(", ")}`
|
|
339
403
|
},
|
|
340
404
|
requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
|
|
341
405
|
acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
|
|
342
406
|
// A CODE-REVIEW repair changes delivered code/evidence, never the
|
|
343
407
|
// already accepted PLAN or its ownership declaration.
|
|
344
408
|
document_updates: [],
|
|
345
|
-
required_read: unique([...(receipt ? [receipt.receipt_path] : input.
|
|
409
|
+
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)]),
|
|
346
410
|
discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
|
|
347
411
|
// These are collision-avoidance hints. Receipt paths enrich the coordinator
|
|
348
412
|
// picture but never restrict the repair's project-local edits.
|
|
@@ -357,34 +421,32 @@ export function addVnextCodeRepair(context, input) {
|
|
|
357
421
|
...(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."])
|
|
358
422
|
]),
|
|
359
423
|
depends_on: origins.map((work) => work.work_id),
|
|
360
|
-
result_schema: "dd-flow/code-work-result@
|
|
424
|
+
result_schema: "dd-flow/code-work-result@3",
|
|
425
|
+
...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
|
|
361
426
|
};
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
427
|
+
const parentWorkId = requireRootWork(context, project.id, run.id).work_id;
|
|
428
|
+
const items = prepareWorkBatch(context, { parentWorkId, value: { works: [repair] } });
|
|
429
|
+
return { kind: "create", project, run, input, parentWorkId, items, key, receipt };
|
|
430
|
+
}
|
|
431
|
+
export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
|
|
432
|
+
const { project, run } = prepared;
|
|
433
|
+
if (prepared.kind === "replay")
|
|
434
|
+
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
435
|
+
const { parentWorkId, items, key, receipt } = prepared;
|
|
436
|
+
const registered = addWorkBatch(context, { parentWorkId, prepared: items });
|
|
372
437
|
const id = String(registered.work_ids[key]);
|
|
373
438
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
|
|
374
439
|
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
375
440
|
type: "code_repair_created",
|
|
376
441
|
work_id: id,
|
|
377
|
-
origin_work_ids: input.originWorkIds,
|
|
378
|
-
...(receipt ? { check_receipt_id: receipt.id } : input.
|
|
442
|
+
origin_work_ids: prepared.input.originWorkIds,
|
|
443
|
+
...(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 })
|
|
379
444
|
});
|
|
380
445
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
381
446
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
382
447
|
}
|
|
383
|
-
/** Retain the causal declaration for worker context; run_at
|
|
448
|
+
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
384
449
|
export function selectRepairChecks(input) {
|
|
385
|
-
// Gate placement belongs to the accepted PLAN. `runCodeChecks` executes only
|
|
386
|
-
// work-scoped entries, while retaining the aggregate declaration tells the
|
|
387
|
-
// worker exactly what will be rerun by the coordinator.
|
|
388
450
|
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
389
451
|
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
390
452
|
return uniqueBy(checks, (check) => check.id);
|
|
@@ -414,10 +476,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
|
|
|
414
476
|
return [...paths].sort();
|
|
415
477
|
}
|
|
416
478
|
function coordinatorPrompt(context, input) {
|
|
417
|
-
const graph
|
|
418
|
-
const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.run.id });
|
|
419
|
-
const capacity = variables.variables?.["runtime.subagent.available_slots"];
|
|
420
|
-
const template = fs.readFileSync(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
479
|
+
const { graph, capacity, template } = input;
|
|
421
480
|
return [
|
|
422
481
|
"<stage_identity>",
|
|
423
482
|
`- RUN: ${input.run.id}`,
|
|
@@ -439,8 +498,8 @@ function coordinatorPrompt(context, input) {
|
|
|
439
498
|
"</code_graph>",
|
|
440
499
|
"",
|
|
441
500
|
"<execution_commands>",
|
|
442
|
-
"
|
|
443
|
-
"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:
|
|
501
|
+
"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: " + `${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`,
|
|
502
|
+
"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. Its repair_command only creates the repair Work; use it with the required origin Work IDs, then end the Turn. Do not invoke that new 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.",
|
|
444
503
|
`A repairable engine, harness, or environment failure is not a user question. Record it without finishing CODE: ${flowCommand(context)} stage block ${input.run.id} --stage code --work ${input.rootWork.work_id} --kind <engine|harness|environment> --code <stable-code> --summary-stdin --retryable --project-root ${JSON.stringify(input.projectRoot)} --json. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
445
504
|
`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"))}`,
|
|
446
505
|
"If the aggregate gate fails, do not stop after `code_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use its returned repair command with the relevant completed origin Work IDs and a concise repair objective, then stop so the runner can dispatch the newly declared repair. Do not edit invisibly in the root orchestrator.",
|
|
@@ -462,76 +521,6 @@ function coordinatorPrompt(context, input) {
|
|
|
462
521
|
export function profileCommands(workspaceRoot) {
|
|
463
522
|
return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
|
|
464
523
|
}
|
|
465
|
-
function latestReceiptsByCommand(receipts) {
|
|
466
|
-
const latest = new Map();
|
|
467
|
-
for (const receipt of receipts)
|
|
468
|
-
for (const ref of receipt.check_refs)
|
|
469
|
-
latest.set(ref, receipt);
|
|
470
|
-
return [...new Set(latest.values())];
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* A CODE finish may be retried after a client loses the long command's output.
|
|
474
|
-
* Reusing complete, same-workspace receipts is correct; rerunning them is both
|
|
475
|
-
* expensive and can race the still-settling original invocation.
|
|
476
|
-
*/
|
|
477
|
-
export function reusableAggregateGateReceipts(context, input) {
|
|
478
|
-
const expected = new Set(input.checks.map((check) => check.id));
|
|
479
|
-
const current = workspaceFingerprint(input.workspaceRoot);
|
|
480
|
-
const latest = new Map();
|
|
481
|
-
for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
|
|
482
|
-
if (receipt.scope === "aggregate")
|
|
483
|
-
for (const ref of receipt.check_refs)
|
|
484
|
-
if (expected.has(ref))
|
|
485
|
-
latest.set(ref, receipt);
|
|
486
|
-
}
|
|
487
|
-
if (latest.size !== expected.size)
|
|
488
|
-
return null;
|
|
489
|
-
const receipts = input.checks.map((check) => latest.get(check.id));
|
|
490
|
-
return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
|
|
491
|
-
}
|
|
492
|
-
/** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
|
|
493
|
-
function acquireAggregateGateLock(root) {
|
|
494
|
-
const directory = path.join(root, "checks");
|
|
495
|
-
const lock = path.join(directory, ".aggregate-gate.lock");
|
|
496
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
497
|
-
try {
|
|
498
|
-
const descriptor = fs.openSync(lock, "wx");
|
|
499
|
-
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
500
|
-
fs.closeSync(descriptor);
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
if (!isExistingLock(error))
|
|
504
|
-
throw error;
|
|
505
|
-
const activePid = lockPid(lock);
|
|
506
|
-
if (activePid && processAlive(activePid)) {
|
|
507
|
-
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 });
|
|
508
|
-
}
|
|
509
|
-
fs.rmSync(lock, { force: true });
|
|
510
|
-
return acquireAggregateGateLock(root);
|
|
511
|
-
}
|
|
512
|
-
return () => fs.rmSync(lock, { force: true });
|
|
513
|
-
}
|
|
514
|
-
function isExistingLock(error) {
|
|
515
|
-
return Boolean(error && typeof error === "object" && error.code === "EEXIST");
|
|
516
|
-
}
|
|
517
|
-
function lockPid(lock) {
|
|
518
|
-
try {
|
|
519
|
-
const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
|
|
520
|
-
return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
function processAlive(pid) {
|
|
527
|
-
try {
|
|
528
|
-
process.kill(pid, 0);
|
|
529
|
-
return true;
|
|
530
|
-
}
|
|
531
|
-
catch {
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
524
|
/**
|
|
536
525
|
* The immutable Work result is a repair history. The stage report is a current
|
|
537
526
|
* acceptance projection, so a registered receipt from an older check epoch
|
|
@@ -545,8 +534,8 @@ export function verificationProjection(works, receipts, options = {}) {
|
|
|
545
534
|
for (const work of works) {
|
|
546
535
|
const result = readJsonResult(work.result);
|
|
547
536
|
for (const item of result.evidence ?? [])
|
|
548
|
-
if (item.
|
|
549
|
-
rawEvidence.set(item.
|
|
537
|
+
if (item.obligation_ref)
|
|
538
|
+
rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
|
|
550
539
|
}
|
|
551
540
|
const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
|
|
552
541
|
const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
|
|
@@ -561,18 +550,16 @@ export function verificationProjection(works, receipts, options = {}) {
|
|
|
561
550
|
return [item.criterion_id, { evidenceRefs, historical }];
|
|
562
551
|
}));
|
|
563
552
|
const hasEvidence = (criterionId) => (acceptanceEvidence.get(criterionId)?.evidenceRefs.length ?? 0) > 0;
|
|
564
|
-
const isPassed = (checkId
|
|
553
|
+
const isPassed = (checkId) => receiptByDeclaration.get(checkId)?.status === "passed";
|
|
565
554
|
return {
|
|
566
555
|
checks: declarations.map((check) => {
|
|
567
556
|
const receipt = receiptByDeclaration.get(check.id);
|
|
568
|
-
|
|
569
|
-
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 };
|
|
557
|
+
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 };
|
|
570
558
|
}),
|
|
571
559
|
acceptance: acceptance.map((item) => {
|
|
572
|
-
const
|
|
573
|
-
const confirmed = item.check_refs.every((id) => isPassed(id, item.criterion_id)) && hasEvidence(item.criterion_id);
|
|
560
|
+
const confirmed = item.check_refs.every((id) => isPassed(id)) && hasEvidence(item.criterion_id);
|
|
574
561
|
const evidence = acceptanceEvidence.get(item.criterion_id) ?? { evidenceRefs: [], historical: [] };
|
|
575
|
-
return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ?
|
|
562
|
+
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 } : {}) };
|
|
576
563
|
})
|
|
577
564
|
};
|
|
578
565
|
}
|
|
@@ -642,69 +629,72 @@ function requireRun(context, projectId, runId) {
|
|
|
642
629
|
return run;
|
|
643
630
|
}
|
|
644
631
|
/** CODE, not PROTOCOLIZE, makes a checkout ready for project tooling. */
|
|
645
|
-
|
|
632
|
+
export function isReusablePreparedCodeReadiness(value, expected) {
|
|
633
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
634
|
+
return false;
|
|
635
|
+
const receipt = value;
|
|
636
|
+
return receipt.schema_id === "dd-flow/workspace-readiness@2"
|
|
637
|
+
&& receipt.status === "passed"
|
|
638
|
+
&& receipt.prepared_by === expected.ownerId
|
|
639
|
+
&& receipt.workspace_root === expected.workspaceRoot
|
|
640
|
+
&& receipt.command === expected.command
|
|
641
|
+
&& receipt.policy_ref === expected.policyRef;
|
|
642
|
+
}
|
|
643
|
+
function readPreparedReadiness(file) {
|
|
644
|
+
try {
|
|
645
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress, options = {}) {
|
|
646
652
|
const profile = JSON.parse(run.index_json);
|
|
647
653
|
const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
|
|
648
654
|
if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
|
|
649
655
|
throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
|
|
650
656
|
}
|
|
651
657
|
const receiptPath = path.join(root, "workspace-readiness.json");
|
|
652
|
-
const
|
|
653
|
-
if (
|
|
654
|
-
|
|
658
|
+
const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
|
|
659
|
+
if (!options.force && ownerId && isReusablePreparedCodeReadiness(readPreparedReadiness(receiptPath), {
|
|
660
|
+
ownerId,
|
|
661
|
+
workspaceRoot: run.workspace_root,
|
|
662
|
+
command: bootstrap.command,
|
|
663
|
+
policyRef: bootstrap.policy_ref
|
|
664
|
+
})) {
|
|
665
|
+
progress?.("workspace bootstrap already prepared by this controller");
|
|
666
|
+
return readPreparedReadiness(receiptPath);
|
|
667
|
+
}
|
|
668
|
+
// Unowned and stale receipts are never trusted: a direct stage entry still
|
|
669
|
+
// performs its own readiness check. Only the same controller can hand a
|
|
670
|
+
// freshly prepared workspace to its immediately following lifecycle call.
|
|
655
671
|
const startedAt = new Date().toISOString();
|
|
656
672
|
progress?.(`workspace bootstrap started: ${bootstrap.command}`);
|
|
657
|
-
const result = await
|
|
673
|
+
const result = await runWorkspaceBootstrap(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, command: bootstrap.command, artifactDir: "05-code/readiness", progress });
|
|
658
674
|
const bootstrapPassed = result.exit_code === 0 && !result.error;
|
|
659
675
|
const checks = bootstrapPassed && readinessChecks.length
|
|
660
676
|
? await runCodeChecks(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, scope: "aggregate", artifactDir: "05-code/readiness", checks: readinessChecks, ...(progress ? { progress } : {}) })
|
|
661
677
|
: [];
|
|
662
|
-
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 };
|
|
678
|
+
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 };
|
|
663
679
|
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
664
680
|
if (receipt.status !== "passed")
|
|
665
681
|
throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
|
|
666
682
|
progress?.("workspace bootstrap passed");
|
|
667
683
|
return receipt;
|
|
668
684
|
}
|
|
669
|
-
function runBootstrap(command, cwd, environment, progress) {
|
|
670
|
-
return new Promise((resolve) => {
|
|
671
|
-
const started = Date.now();
|
|
672
|
-
const child = spawn("/bin/sh", ["-lc", command], { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
|
|
673
|
-
let stdout = "";
|
|
674
|
-
let stderr = "";
|
|
675
|
-
let error = null;
|
|
676
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
677
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
678
|
-
const heartbeat = setInterval(() => progress?.(`workspace bootstrap still running (${Math.floor((Date.now() - started) / 1000)}s)`), 15_000);
|
|
679
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), 15 * 60 * 1000);
|
|
680
|
-
child.once("error", (value) => { error = value.message; });
|
|
681
|
-
child.once("close", (exitCode, signal) => {
|
|
682
|
-
clearInterval(heartbeat);
|
|
683
|
-
clearTimeout(timeout);
|
|
684
|
-
resolve({ exit_code: exitCode, stdout, stderr, error: error ?? (signal ? `terminated by ${signal}` : null) });
|
|
685
|
-
});
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
function readReadiness(file) {
|
|
689
|
-
try {
|
|
690
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
691
|
-
}
|
|
692
|
-
catch {
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
685
|
function requireHome(run) {
|
|
697
686
|
if (!run.run_root)
|
|
698
687
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
|
|
699
688
|
return run.run_root;
|
|
700
689
|
}
|
|
701
|
-
function finishCommand(context, runId, projectRoot, verificationFile) {
|
|
702
|
-
|
|
690
|
+
export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
|
|
691
|
+
const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : "";
|
|
692
|
+
return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`);
|
|
703
693
|
}
|
|
704
694
|
function readVerification(context, input) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
695
|
+
return prepareJsonSchema(input.file, "CODE verification", {
|
|
696
|
+
schemaName: "code-verification", projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot
|
|
697
|
+
}).value;
|
|
708
698
|
}
|
|
709
699
|
function verificationForFinish(context, input) {
|
|
710
700
|
return readVerification(context, input);
|
|
@@ -726,6 +716,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
|
|
|
726
716
|
}
|
|
727
717
|
function nextAction(run, changedPaths) {
|
|
728
718
|
const index = JSON.parse(run.index_json);
|
|
719
|
+
if (index.variables?.["merge.source_repair"])
|
|
720
|
+
return "start_code_review";
|
|
729
721
|
const requested = index.settings?.code_review?.mode ?? "auto";
|
|
730
722
|
const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
|
|
731
723
|
const stopTarget = index.execution_profile?.settings?.stop_target;
|
|
@@ -741,19 +733,19 @@ function stageStatus(run, name) {
|
|
|
741
733
|
return null;
|
|
742
734
|
}
|
|
743
735
|
}
|
|
744
|
-
function
|
|
736
|
+
function isMergeSourceRepairAttempt(run) {
|
|
745
737
|
try {
|
|
746
|
-
const value = JSON.parse(
|
|
747
|
-
return
|
|
738
|
+
const value = JSON.parse(run.index_json);
|
|
739
|
+
return Boolean(value.variables?.["merge.source_repair"]);
|
|
748
740
|
}
|
|
749
741
|
catch {
|
|
750
|
-
return
|
|
742
|
+
return false;
|
|
751
743
|
}
|
|
752
744
|
}
|
|
753
|
-
function
|
|
745
|
+
function changedPathsFromReport(file) {
|
|
754
746
|
try {
|
|
755
|
-
const value = JSON.parse(
|
|
756
|
-
return Array.isArray(value.
|
|
747
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
748
|
+
return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
|
|
757
749
|
}
|
|
758
750
|
catch {
|
|
759
751
|
return [];
|
|
@@ -772,6 +764,28 @@ function uniqueBy(values, key) {
|
|
|
772
764
|
return true;
|
|
773
765
|
});
|
|
774
766
|
}
|
|
767
|
+
/** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
|
|
768
|
+
export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
|
|
769
|
+
const index = JSON.parse(run.index_json);
|
|
770
|
+
if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
|
|
771
|
+
return;
|
|
772
|
+
const terminal = next === "code_completed" || next === "code_review_completed";
|
|
773
|
+
if (!terminal && index.next_action === next)
|
|
774
|
+
return;
|
|
775
|
+
withStageSettlement(context, () => {
|
|
776
|
+
if (!terminal) {
|
|
777
|
+
advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
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]);
|
|
781
|
+
if (!work)
|
|
782
|
+
throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
|
|
783
|
+
if (work.status !== "completed")
|
|
784
|
+
finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
|
|
785
|
+
if (index.status !== "done")
|
|
786
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
|
|
787
|
+
});
|
|
788
|
+
}
|
|
775
789
|
function writeReport(root, report) {
|
|
776
790
|
writeStageReport(root, report);
|
|
777
791
|
}
|