@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +404 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +224 -216
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +605 -157
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,26 +1,44 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
|
-
import {
|
|
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, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
11
|
import { validateSchema } from "./schema-validation.js";
|
|
11
12
|
import { flowCommand } from "./stage-pause.js";
|
|
12
13
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
14
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
15
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph,
|
|
16
|
+
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
16
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
|
-
import {
|
|
19
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
19
20
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
20
21
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
21
22
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
22
24
|
const stage = "code";
|
|
23
25
|
const stageDir = vnextStageDirectory("code");
|
|
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
|
+
}
|
|
33
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
34
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
35
|
+
const run = requireRun(context, project.id, input.runId);
|
|
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
|
+
}
|
|
24
42
|
export async function startVnextCode(context, input) {
|
|
25
43
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
26
44
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -31,10 +49,12 @@ export async function startVnextCode(context, input) {
|
|
|
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);
|
|
34
53
|
if (existingStage && fs.existsSync(existingPrompt)) {
|
|
35
54
|
const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
36
55
|
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
37
56
|
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
57
|
+
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(existingRoot, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
38
58
|
const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
39
59
|
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")) } };
|
|
40
60
|
}
|
|
@@ -53,7 +73,8 @@ export async function startVnextCode(context, input) {
|
|
|
53
73
|
file: batch,
|
|
54
74
|
projectRoot: run.workspace_root,
|
|
55
75
|
ddFlowHome: context.ddFlowHome,
|
|
56
|
-
runId: input.runId
|
|
76
|
+
runId: input.runId,
|
|
77
|
+
runRoot: home
|
|
57
78
|
});
|
|
58
79
|
validateWorkBatchFile(batch);
|
|
59
80
|
const root = path.join(home, stageDir);
|
|
@@ -72,14 +93,16 @@ export async function startVnextCode(context, input) {
|
|
|
72
93
|
resultPath: path.join(root, "stage-report.json"),
|
|
73
94
|
...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
|
|
74
95
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
if (!sourceRepairAttempt) {
|
|
97
|
+
attachFlowRunStage(context, {
|
|
98
|
+
projectRoot,
|
|
99
|
+
runId: input.runId,
|
|
100
|
+
stage,
|
|
101
|
+
dir: stageDir,
|
|
102
|
+
status: "running",
|
|
103
|
+
dataSchemaId: "dd-flow/stage-report@1"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
83
106
|
const graph = codeWorkGraph(context, project.id, input.runId);
|
|
84
107
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
85
108
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
@@ -104,8 +127,10 @@ export async function finishVnextCode(context, input) {
|
|
|
104
127
|
const run = requireRun(context, project.id, input.runId);
|
|
105
128
|
const home = requireHome(run);
|
|
106
129
|
const root = path.join(home, stageDir);
|
|
130
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
107
131
|
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
108
132
|
if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
|
|
133
|
+
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
134
|
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
135
|
}
|
|
111
136
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
@@ -134,7 +159,7 @@ export async function finishVnextCode(context, input) {
|
|
|
134
159
|
throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
|
|
135
160
|
}
|
|
136
161
|
// 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 });
|
|
162
|
+
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
138
163
|
if (verification.verdict === "blocked") {
|
|
139
164
|
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." };
|
|
140
165
|
}
|
|
@@ -153,29 +178,15 @@ export async function finishVnextCode(context, input) {
|
|
|
153
178
|
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. Run the returned repair Work, then update code-verification.json and invoke this same stage finish command again." };
|
|
154
179
|
}
|
|
155
180
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
|
|
166
|
-
projectId: project.id,
|
|
167
|
-
runId: run.id,
|
|
168
|
-
runHome: home,
|
|
169
|
-
workspaceRoot: run.workspace_root,
|
|
170
|
-
scope: "aggregate",
|
|
171
|
-
checks,
|
|
172
|
-
...(input.progress ? { progress: input.progress } : {})
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
finally {
|
|
176
|
-
release();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
181
|
+
if (input.retryCheckId) {
|
|
182
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
183
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
184
|
+
}
|
|
185
|
+
const receipts = await runCodeChecks(context, {
|
|
186
|
+
projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
|
|
187
|
+
...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
|
|
188
|
+
...(input.progress ? { progress: input.progress } : {})
|
|
189
|
+
});
|
|
179
190
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
180
191
|
if (failed.length) {
|
|
181
192
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
@@ -183,23 +194,21 @@ export async function finishVnextCode(context, input) {
|
|
|
183
194
|
outcome: "repair_required",
|
|
184
195
|
retry_after_workspace_change: true,
|
|
185
196
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
186
|
-
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
|
|
197
|
+
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`,
|
|
198
|
+
retry_command: finishCommand(context, run.id, projectRoot, input.verificationFile, failed[0].id)
|
|
187
199
|
});
|
|
188
200
|
}
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const projectedVerification = verificationProjection(works, finalReceipts);
|
|
201
|
+
const finalReceipts = receipts;
|
|
202
|
+
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
|
|
192
203
|
const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
|
|
193
204
|
if (unresolvedAcceptance.length)
|
|
194
205
|
throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
|
|
195
206
|
const now = context.now();
|
|
196
207
|
const timing = stageTiming(home, stage, now);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
if (missingReportedPaths.length)
|
|
202
|
-
throw new AppError("changed_path_not_materialized", "CODE Work reported paths that are not changed in the accepted workspace", 2, { paths: missingReportedPaths });
|
|
208
|
+
// Work result paths are provenance, not a mutable filesystem oracle. The
|
|
209
|
+
// stage reports one shared Git observation; ignored machine repairs are
|
|
210
|
+
// rejected before a Work can become completed.
|
|
211
|
+
const changedPaths = workspaceChangedPaths(run.workspace_root);
|
|
203
212
|
const next = nextAction(run, changedPaths);
|
|
204
213
|
const report = {
|
|
205
214
|
schema_id: "dd-flow/stage-report@1",
|
|
@@ -238,31 +247,41 @@ export async function finishVnextCode(context, input) {
|
|
|
238
247
|
// Creating the MERGE request is part of accepting this terminal handoff.
|
|
239
248
|
// Do it before materialising a terminal stage report or changing RUN state,
|
|
240
249
|
// so an invalid merge policy leaves CODE safely running and retryable.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
status
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
250
|
+
let mergeRequest = null;
|
|
251
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
252
|
+
const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
253
|
+
withStageSettlement(context, () => {
|
|
254
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
255
|
+
const currentWorks = codeWorks(context, project.id, run.id);
|
|
256
|
+
if (currentWorks.length !== works.length || currentWorks.some(work => work.status !== "completed" || !works.some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
|
|
257
|
+
throw new AppError("code_graph_incomplete", "CODE results changed while the final checks were running", 2);
|
|
258
|
+
}
|
|
259
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
260
|
+
writeReport(root, report);
|
|
261
|
+
completeFlowRunStage(context, {
|
|
262
|
+
projectRoot,
|
|
263
|
+
runId: run.id,
|
|
264
|
+
stage,
|
|
265
|
+
status: "done",
|
|
266
|
+
data: "stage-report.json",
|
|
267
|
+
dataSchemaId: "dd-flow/stage-report@1",
|
|
268
|
+
report: "stage-report.md",
|
|
269
|
+
stageReport: "stage-report.html"
|
|
270
|
+
});
|
|
271
|
+
const rootWork = requireRootWork(context, project.id, run.id);
|
|
272
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
273
|
+
type: "code_accepted",
|
|
274
|
+
work_id: rootWork.work_id,
|
|
275
|
+
checks: finalReceipts.map((receipt) => receipt.id)
|
|
276
|
+
});
|
|
277
|
+
if (next === "code_completed") {
|
|
278
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
279
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
283
|
+
}
|
|
258
284
|
});
|
|
259
|
-
if (next === "code_completed") {
|
|
260
|
-
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
261
|
-
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
265
|
-
}
|
|
266
285
|
return {
|
|
267
286
|
ok: true,
|
|
268
287
|
run_id: run.id,
|
|
@@ -270,7 +289,7 @@ export async function finishVnextCode(context, input) {
|
|
|
270
289
|
outcome: "accepted",
|
|
271
290
|
report_path: path.join(root, "stage-report.json"),
|
|
272
291
|
next_action: next,
|
|
273
|
-
...(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` } } : {})
|
|
292
|
+
...(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`) } } : {})
|
|
274
293
|
};
|
|
275
294
|
}
|
|
276
295
|
export function addVnextCodeRepair(context, input) {
|
|
@@ -280,7 +299,12 @@ export function addVnextCodeRepair(context, input) {
|
|
|
280
299
|
const home = requireHome(run);
|
|
281
300
|
if (!input.objective.trim())
|
|
282
301
|
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
283
|
-
if (
|
|
302
|
+
if (input.reviewCycle) {
|
|
303
|
+
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
|
|
304
|
+
if (previous)
|
|
305
|
+
return { ok: true, run_id: run.id, repair_work_id: previous.work_id, start_command: workStartCommand(context, previous), reused: true };
|
|
306
|
+
}
|
|
307
|
+
if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
|
|
284
308
|
throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
|
|
285
309
|
}
|
|
286
310
|
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 = ?", [
|
|
@@ -303,10 +327,10 @@ export function addVnextCodeRepair(context, input) {
|
|
|
303
327
|
// repair that fixes one failed aggregate check can make a second, already
|
|
304
328
|
// declared aggregate failure impossible to repair. The immutable source
|
|
305
329
|
// of repair eligibility is the original CODE graph.
|
|
306
|
-
const declaredChecks =
|
|
330
|
+
const declaredChecks = effectiveCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
|
|
307
331
|
.map((work) => packet(work))
|
|
308
332
|
.filter((value) => !value.repair)
|
|
309
|
-
.flatMap((value) => value.checks));
|
|
333
|
+
.flatMap((value) => value.checks), ["work", "code", "readiness", "merge"]);
|
|
310
334
|
const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
|
|
311
335
|
if (receipt && !failedCheck) {
|
|
312
336
|
throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
|
|
@@ -315,7 +339,7 @@ export function addVnextCodeRepair(context, input) {
|
|
|
315
339
|
});
|
|
316
340
|
}
|
|
317
341
|
const semanticRepair = Boolean(input.semanticUnresolved?.length);
|
|
318
|
-
const key = input.
|
|
342
|
+
const key = input.reviewFindings?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
|
|
319
343
|
const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
|
|
320
344
|
const repair = {
|
|
321
345
|
schema_id: "dd-flow/code-work-packet@5",
|
|
@@ -325,8 +349,12 @@ export function addVnextCodeRepair(context, input) {
|
|
|
325
349
|
repair: {
|
|
326
350
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
327
351
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
328
|
-
...(input.
|
|
329
|
-
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
|
|
352
|
+
...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
|
|
353
|
+
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
|
|
354
|
+
// `run_at` remains the accepted aggregate placement. This separate
|
|
355
|
+
// repair obligation is deliberately executed before this repair Work is
|
|
356
|
+
// accepted, so a local fix cannot close without re-proving its cause.
|
|
357
|
+
verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
|
|
330
358
|
},
|
|
331
359
|
task: input.objective,
|
|
332
360
|
semantic_spine: {
|
|
@@ -334,14 +362,14 @@ export function addVnextCodeRepair(context, input) {
|
|
|
334
362
|
component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
|
|
335
363
|
must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
|
|
336
364
|
non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
|
|
337
|
-
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.
|
|
365
|
+
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(", ")}`
|
|
338
366
|
},
|
|
339
367
|
requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
|
|
340
368
|
acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
|
|
341
369
|
// A CODE-REVIEW repair changes delivered code/evidence, never the
|
|
342
370
|
// already accepted PLAN or its ownership declaration.
|
|
343
371
|
document_updates: [],
|
|
344
|
-
required_read: unique([...(receipt ? [receipt.receipt_path] : input.
|
|
372
|
+
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)]),
|
|
345
373
|
discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
|
|
346
374
|
// These are collision-avoidance hints. Receipt paths enrich the coordinator
|
|
347
375
|
// picture but never restrict the repair's project-local edits.
|
|
@@ -356,7 +384,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
356
384
|
...(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."])
|
|
357
385
|
]),
|
|
358
386
|
depends_on: origins.map((work) => work.work_id),
|
|
359
|
-
result_schema: "dd-flow/code-work-result@
|
|
387
|
+
result_schema: "dd-flow/code-work-result@3",
|
|
388
|
+
...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
|
|
360
389
|
};
|
|
361
390
|
const file = path.join(home, stageDir, `.repair-${crypto.randomUUID()}.json`);
|
|
362
391
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -374,16 +403,13 @@ export function addVnextCodeRepair(context, input) {
|
|
|
374
403
|
type: "code_repair_created",
|
|
375
404
|
work_id: id,
|
|
376
405
|
origin_work_ids: input.originWorkIds,
|
|
377
|
-
...(receipt ? { check_receipt_id: receipt.id } : input.
|
|
406
|
+
...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindings?.length ? { review_finding_ids: input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: input.semanticUnresolved })
|
|
378
407
|
});
|
|
379
408
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
380
409
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
381
410
|
}
|
|
382
|
-
/** Retain the causal declaration for worker context; run_at
|
|
411
|
+
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
383
412
|
export function selectRepairChecks(input) {
|
|
384
|
-
// Gate placement belongs to the accepted PLAN. `runCodeChecks` executes only
|
|
385
|
-
// work-scoped entries, while retaining the aggregate declaration tells the
|
|
386
|
-
// worker exactly what will be rerun by the coordinator.
|
|
387
413
|
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
388
414
|
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
389
415
|
return uniqueBy(checks, (check) => check.id);
|
|
@@ -438,8 +464,8 @@ function coordinatorPrompt(context, input) {
|
|
|
438
464
|
"</code_graph>",
|
|
439
465
|
"",
|
|
440
466
|
"<execution_commands>",
|
|
441
|
-
"
|
|
442
|
-
"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: do not 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.",
|
|
467
|
+
"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 owns dispatch and the stage conclusion, not implementation Work. Every child starts with its exact start_command and receives its complete packet from dd-flow. 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`,
|
|
468
|
+
"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. Read that returned error and run its repair command; do not 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.",
|
|
443
469
|
`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.`,
|
|
444
470
|
`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"))}`,
|
|
445
471
|
"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.",
|
|
@@ -461,103 +487,57 @@ function coordinatorPrompt(context, input) {
|
|
|
461
487
|
export function profileCommands(workspaceRoot) {
|
|
462
488
|
return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
|
|
463
489
|
}
|
|
464
|
-
function latestReceiptsByCommand(receipts) {
|
|
465
|
-
const latest = new Map();
|
|
466
|
-
for (const receipt of receipts)
|
|
467
|
-
for (const ref of receipt.check_refs)
|
|
468
|
-
latest.set(ref, receipt);
|
|
469
|
-
return [...new Set(latest.values())];
|
|
470
|
-
}
|
|
471
490
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
491
|
+
* The immutable Work result is a repair history. The stage report is a current
|
|
492
|
+
* acceptance projection, so a registered receipt from an older check epoch
|
|
493
|
+
* must never stand in for the receipt that currently proves the declaration.
|
|
475
494
|
*/
|
|
476
|
-
export function
|
|
477
|
-
const expected = new Set(input.checks.map((check) => check.id));
|
|
478
|
-
const current = workspaceFingerprint(input.workspaceRoot);
|
|
479
|
-
const latest = new Map();
|
|
480
|
-
for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
|
|
481
|
-
if (receipt.scope === "aggregate")
|
|
482
|
-
for (const ref of receipt.check_refs)
|
|
483
|
-
if (expected.has(ref))
|
|
484
|
-
latest.set(ref, receipt);
|
|
485
|
-
}
|
|
486
|
-
if (latest.size !== expected.size)
|
|
487
|
-
return null;
|
|
488
|
-
const receipts = input.checks.map((check) => latest.get(check.id));
|
|
489
|
-
return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
|
|
490
|
-
}
|
|
491
|
-
/** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
|
|
492
|
-
function acquireAggregateGateLock(root) {
|
|
493
|
-
const directory = path.join(root, "checks");
|
|
494
|
-
const lock = path.join(directory, ".aggregate-gate.lock");
|
|
495
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
496
|
-
try {
|
|
497
|
-
const descriptor = fs.openSync(lock, "wx");
|
|
498
|
-
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
499
|
-
fs.closeSync(descriptor);
|
|
500
|
-
}
|
|
501
|
-
catch (error) {
|
|
502
|
-
if (!isExistingLock(error))
|
|
503
|
-
throw error;
|
|
504
|
-
const activePid = lockPid(lock);
|
|
505
|
-
if (activePid && processAlive(activePid)) {
|
|
506
|
-
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 });
|
|
507
|
-
}
|
|
508
|
-
fs.rmSync(lock, { force: true });
|
|
509
|
-
return acquireAggregateGateLock(root);
|
|
510
|
-
}
|
|
511
|
-
return () => fs.rmSync(lock, { force: true });
|
|
512
|
-
}
|
|
513
|
-
function isExistingLock(error) {
|
|
514
|
-
return Boolean(error && typeof error === "object" && error.code === "EEXIST");
|
|
515
|
-
}
|
|
516
|
-
function lockPid(lock) {
|
|
517
|
-
try {
|
|
518
|
-
const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
|
|
519
|
-
return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
function processAlive(pid) {
|
|
526
|
-
try {
|
|
527
|
-
process.kill(pid, 0);
|
|
528
|
-
return true;
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
export function verificationProjection(works, receipts) {
|
|
495
|
+
export function verificationProjection(works, receipts, options = {}) {
|
|
535
496
|
const packets = works.map((work) => packet(work)).filter((value) => value !== null);
|
|
536
497
|
const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
|
|
537
498
|
const receiptByDeclaration = new Map(receipts.flatMap((receipt) => receipt.check_refs.map((ref) => [ref, receipt])));
|
|
538
|
-
const
|
|
499
|
+
const rawEvidence = new Map();
|
|
539
500
|
for (const work of works) {
|
|
540
501
|
const result = readJsonResult(work.result);
|
|
541
502
|
for (const item of result.evidence ?? [])
|
|
542
|
-
if (item.
|
|
543
|
-
|
|
503
|
+
if (item.obligation_ref)
|
|
504
|
+
rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
|
|
544
505
|
}
|
|
545
506
|
const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
|
|
546
|
-
const
|
|
547
|
-
const
|
|
507
|
+
const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
|
|
508
|
+
const acceptanceEvidence = new Map(acceptance.map((item) => {
|
|
509
|
+
const reported = rawEvidence.get(item.criterion_id) ?? [];
|
|
510
|
+
const current = unique(item.check_refs.flatMap((checkRef) => {
|
|
511
|
+
const receipt = receiptByDeclaration.get(checkRef);
|
|
512
|
+
return receipt?.status === "passed" ? [currentReceiptRef(receipt, options)] : [];
|
|
513
|
+
}));
|
|
514
|
+
const evidenceRefs = unique([...reported.filter((ref) => !historicalReceiptRefs.has(ref)), ...current]);
|
|
515
|
+
const historical = reported.filter((ref) => historicalReceiptRefs.has(ref) && !current.includes(ref));
|
|
516
|
+
return [item.criterion_id, { evidenceRefs, historical }];
|
|
517
|
+
}));
|
|
518
|
+
const hasEvidence = (criterionId) => (acceptanceEvidence.get(criterionId)?.evidenceRefs.length ?? 0) > 0;
|
|
519
|
+
const isPassed = (checkId) => receiptByDeclaration.get(checkId)?.status === "passed";
|
|
548
520
|
return {
|
|
549
521
|
checks: declarations.map((check) => {
|
|
550
522
|
const receipt = receiptByDeclaration.get(check.id);
|
|
551
|
-
|
|
552
|
-
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 };
|
|
523
|
+
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 };
|
|
553
524
|
}),
|
|
554
525
|
acceptance: acceptance.map((item) => {
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ?
|
|
526
|
+
const confirmed = item.check_refs.every((id) => isPassed(id)) && hasEvidence(item.criterion_id);
|
|
527
|
+
const evidence = acceptanceEvidence.get(item.criterion_id) ?? { evidenceRefs: [], historical: [] };
|
|
528
|
+
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 } : {}) };
|
|
558
529
|
})
|
|
559
530
|
};
|
|
560
531
|
}
|
|
532
|
+
function currentReceiptRef(receipt, options) {
|
|
533
|
+
if (!options.runId || !options.runHome)
|
|
534
|
+
return receipt.receipt_path;
|
|
535
|
+
const relative = path.relative(options.runHome, receipt.receipt_path).split(path.sep).join("/");
|
|
536
|
+
return relative && !relative.startsWith("../") ? `run://${options.runId}/${relative}` : receipt.receipt_path;
|
|
537
|
+
}
|
|
538
|
+
function receiptReferences(receipt, options) {
|
|
539
|
+
return unique([receipt.receipt_path, currentReceiptRef(receipt, options)]);
|
|
540
|
+
}
|
|
561
541
|
function readJsonResult(value) { try {
|
|
562
542
|
const parsed = JSON.parse(value ?? "{}");
|
|
563
543
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -609,75 +589,79 @@ function requireRootWork(context, projectId, runId) {
|
|
|
609
589
|
return work;
|
|
610
590
|
}
|
|
611
591
|
function requireRun(context, projectId, runId) {
|
|
612
|
-
const run = context.db.get("SELECT id, project_id,
|
|
592
|
+
const run = context.db.get("SELECT id, project_id, run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
613
593
|
if (!run)
|
|
614
594
|
throw new AppError("not_found", "RUN is not registered", 1);
|
|
615
595
|
return run;
|
|
616
596
|
}
|
|
617
597
|
/** CODE, not PROTOCOLIZE, makes a checkout ready for project tooling. */
|
|
618
|
-
|
|
598
|
+
export function isReusablePreparedCodeReadiness(value, expected) {
|
|
599
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
600
|
+
return false;
|
|
601
|
+
const receipt = value;
|
|
602
|
+
return receipt.schema_id === "dd-flow/workspace-readiness@2"
|
|
603
|
+
&& receipt.status === "passed"
|
|
604
|
+
&& receipt.prepared_by === expected.ownerId
|
|
605
|
+
&& receipt.workspace_root === expected.workspaceRoot
|
|
606
|
+
&& receipt.command === expected.command
|
|
607
|
+
&& receipt.policy_ref === expected.policyRef;
|
|
608
|
+
}
|
|
609
|
+
function readPreparedReadiness(file) {
|
|
610
|
+
try {
|
|
611
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress, options = {}) {
|
|
619
618
|
const profile = JSON.parse(run.index_json);
|
|
620
619
|
const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
|
|
621
620
|
if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
|
|
622
621
|
throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
|
|
623
622
|
}
|
|
624
623
|
const receiptPath = path.join(root, "workspace-readiness.json");
|
|
625
|
-
const
|
|
626
|
-
if (
|
|
627
|
-
|
|
624
|
+
const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
|
|
625
|
+
if (!options.force && ownerId && isReusablePreparedCodeReadiness(readPreparedReadiness(receiptPath), {
|
|
626
|
+
ownerId,
|
|
627
|
+
workspaceRoot: run.workspace_root,
|
|
628
|
+
command: bootstrap.command,
|
|
629
|
+
policyRef: bootstrap.policy_ref
|
|
630
|
+
})) {
|
|
631
|
+
progress?.("workspace bootstrap already prepared by this controller");
|
|
632
|
+
return readPreparedReadiness(receiptPath);
|
|
633
|
+
}
|
|
634
|
+
// Unowned and stale receipts are never trusted: a direct stage entry still
|
|
635
|
+
// performs its own readiness check. Only the same controller can hand a
|
|
636
|
+
// freshly prepared workspace to its immediately following lifecycle call.
|
|
628
637
|
const startedAt = new Date().toISOString();
|
|
629
638
|
progress?.(`workspace bootstrap started: ${bootstrap.command}`);
|
|
630
|
-
const result = await
|
|
639
|
+
const result = await runWorkspaceBootstrap(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, command: bootstrap.command, artifactDir: "05-code/readiness", progress });
|
|
631
640
|
const bootstrapPassed = result.exit_code === 0 && !result.error;
|
|
632
641
|
const checks = bootstrapPassed && readinessChecks.length
|
|
633
642
|
? await runCodeChecks(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, scope: "aggregate", artifactDir: "05-code/readiness", checks: readinessChecks, ...(progress ? { progress } : {}) })
|
|
634
643
|
: [];
|
|
635
|
-
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 };
|
|
644
|
+
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 };
|
|
636
645
|
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
637
646
|
if (receipt.status !== "passed")
|
|
638
647
|
throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
|
|
639
648
|
progress?.("workspace bootstrap passed");
|
|
640
649
|
return receipt;
|
|
641
650
|
}
|
|
642
|
-
function runBootstrap(command, cwd, environment, progress) {
|
|
643
|
-
return new Promise((resolve) => {
|
|
644
|
-
const started = Date.now();
|
|
645
|
-
const child = spawn("/bin/sh", ["-lc", command], { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
|
|
646
|
-
let stdout = "";
|
|
647
|
-
let stderr = "";
|
|
648
|
-
let error = null;
|
|
649
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
650
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
651
|
-
const heartbeat = setInterval(() => progress?.(`workspace bootstrap still running (${Math.floor((Date.now() - started) / 1000)}s)`), 15_000);
|
|
652
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), 15 * 60 * 1000);
|
|
653
|
-
child.once("error", (value) => { error = value.message; });
|
|
654
|
-
child.once("close", (exitCode, signal) => {
|
|
655
|
-
clearInterval(heartbeat);
|
|
656
|
-
clearTimeout(timeout);
|
|
657
|
-
resolve({ exit_code: exitCode, stdout, stderr, error: error ?? (signal ? `terminated by ${signal}` : null) });
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
function readReadiness(file) {
|
|
662
|
-
try {
|
|
663
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
664
|
-
}
|
|
665
|
-
catch {
|
|
666
|
-
return null;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
651
|
function requireHome(run) {
|
|
670
|
-
if (!run.
|
|
671
|
-
throw new AppError("runtime_missing", "RUN
|
|
672
|
-
return run.
|
|
652
|
+
if (!run.run_root)
|
|
653
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
|
|
654
|
+
return run.run_root;
|
|
673
655
|
}
|
|
674
|
-
function finishCommand(context, runId, projectRoot, verificationFile) {
|
|
675
|
-
|
|
656
|
+
export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
|
|
657
|
+
const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : "";
|
|
658
|
+
return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`);
|
|
676
659
|
}
|
|
677
660
|
function readVerification(context, input) {
|
|
678
661
|
const file = path.resolve(input.file);
|
|
679
|
-
|
|
680
|
-
|
|
662
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
663
|
+
validateSchema({ schemaName: "code-verification", file, data, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot });
|
|
664
|
+
return data;
|
|
681
665
|
}
|
|
682
666
|
function verificationForFinish(context, input) {
|
|
683
667
|
return readVerification(context, input);
|
|
@@ -699,6 +683,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
|
|
|
699
683
|
}
|
|
700
684
|
function nextAction(run, changedPaths) {
|
|
701
685
|
const index = JSON.parse(run.index_json);
|
|
686
|
+
if (index.variables?.["merge.source_repair"])
|
|
687
|
+
return "start_code_review";
|
|
702
688
|
const requested = index.settings?.code_review?.mode ?? "auto";
|
|
703
689
|
const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
|
|
704
690
|
const stopTarget = index.execution_profile?.settings?.stop_target;
|
|
@@ -714,19 +700,19 @@ function stageStatus(run, name) {
|
|
|
714
700
|
return null;
|
|
715
701
|
}
|
|
716
702
|
}
|
|
717
|
-
function
|
|
703
|
+
function isMergeSourceRepairAttempt(run) {
|
|
718
704
|
try {
|
|
719
|
-
const value = JSON.parse(
|
|
720
|
-
return
|
|
705
|
+
const value = JSON.parse(run.index_json);
|
|
706
|
+
return Boolean(value.variables?.["merge.source_repair"]);
|
|
721
707
|
}
|
|
722
708
|
catch {
|
|
723
|
-
return
|
|
709
|
+
return false;
|
|
724
710
|
}
|
|
725
711
|
}
|
|
726
|
-
function
|
|
712
|
+
function changedPathsFromReport(file) {
|
|
727
713
|
try {
|
|
728
|
-
const value = JSON.parse(
|
|
729
|
-
return Array.isArray(value.
|
|
714
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
715
|
+
return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
|
|
730
716
|
}
|
|
731
717
|
catch {
|
|
732
718
|
return [];
|
|
@@ -745,6 +731,28 @@ function uniqueBy(values, key) {
|
|
|
745
731
|
return true;
|
|
746
732
|
});
|
|
747
733
|
}
|
|
734
|
+
/** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
|
|
735
|
+
export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
|
|
736
|
+
const index = JSON.parse(run.index_json);
|
|
737
|
+
if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
|
|
738
|
+
return;
|
|
739
|
+
const terminal = next === "code_completed" || next === "code_review_completed";
|
|
740
|
+
if (!terminal && index.next_action === next)
|
|
741
|
+
return;
|
|
742
|
+
withStageSettlement(context, () => {
|
|
743
|
+
if (!terminal) {
|
|
744
|
+
advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
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]);
|
|
748
|
+
if (!work)
|
|
749
|
+
throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
|
|
750
|
+
if (work.status !== "completed")
|
|
751
|
+
finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
|
|
752
|
+
if (index.status !== "done")
|
|
753
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
|
|
754
|
+
});
|
|
755
|
+
}
|
|
748
756
|
function writeReport(root, report) {
|
|
749
757
|
writeStageReport(root, report);
|
|
750
758
|
}
|