@deksden-com/dd-flow-cli 0.7.0 → 0.8.0
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 +666 -0
- package/README.md +7 -2
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +88 -10
- package/dist/cli/run-cli.js +523 -28
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/domain/validation.js +1 -1
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/compatibility.schema.json +32 -0
- package/dist/schemas/flow-contract.schema.json +9 -5
- package/dist/schemas/flow-run.schema.json +16 -123
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/stage-start-response.schema.json +4 -2
- package/dist/schemas/status-report.schema.json +76 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +10 -2
- package/dist/services/code-checks.js +244 -0
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +12 -12
- package/dist/services/engines.js +1 -1
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +774 -18
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -1
- package/dist/services/merge-queue.js +5 -5
- package/dist/services/merge-worker.js +2 -2
- package/dist/services/migrations.js +2 -2
- package/dist/services/plan-runtime.js +1 -1
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +17 -11
- package/dist/services/protocols.js +8 -8
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +504 -51
- package/dist/services/schema-validation.js +21 -3
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +198 -75
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +305 -0
- package/dist/services/vnext-code.js +686 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +552 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +522 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +263 -34
- package/dist/storage/paths.js +47 -1
- package/package.json +12 -12
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
+
import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
8
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
9
|
+
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
|
+
import { validateSchema } from "./schema-validation.js";
|
|
11
|
+
import { flowCommand } from "./stage-pause.js";
|
|
12
|
+
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
|
+
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
|
+
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
15
|
+
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
16
|
+
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
|
+
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
|
+
import { assertStageStartHookEvent } from "./hooks.js";
|
|
19
|
+
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
|
+
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
21
|
+
const stage = "code";
|
|
22
|
+
const stageDir = vnextStageDirectory("code");
|
|
23
|
+
export async function startVnextCode(context, input) {
|
|
24
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
25
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
26
|
+
const run = requireRun(context, project.id, input.runId);
|
|
27
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
28
|
+
const home = requireHome(run);
|
|
29
|
+
requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
30
|
+
const existingRoot = path.join(home, stageDir);
|
|
31
|
+
const existingPrompt = path.join(existingRoot, "stage-prompt.md");
|
|
32
|
+
const existingStage = stageStatus(run, stage);
|
|
33
|
+
if (existingStage && fs.existsSync(existingPrompt)) {
|
|
34
|
+
const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
35
|
+
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
36
|
+
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
37
|
+
const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
38
|
+
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")) } };
|
|
39
|
+
}
|
|
40
|
+
const batch = path.join(home, "03-plan", "code-work-batch.json");
|
|
41
|
+
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
42
|
+
projectRoot,
|
|
43
|
+
workspaceRoot: run.workspace_root,
|
|
44
|
+
runId: input.runId,
|
|
45
|
+
home
|
|
46
|
+
});
|
|
47
|
+
if (handoffFailures.length) {
|
|
48
|
+
throw new AppError("validation", "CODE handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
|
|
49
|
+
}
|
|
50
|
+
validateSchema({
|
|
51
|
+
schemaName: "code-work-batch",
|
|
52
|
+
file: batch,
|
|
53
|
+
projectRoot: run.workspace_root,
|
|
54
|
+
ddFlowHome: context.ddFlowHome,
|
|
55
|
+
runId: input.runId
|
|
56
|
+
});
|
|
57
|
+
validateWorkBatchFile(batch);
|
|
58
|
+
const root = path.join(home, stageDir);
|
|
59
|
+
fs.mkdirSync(root, { recursive: true });
|
|
60
|
+
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
61
|
+
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
62
|
+
const promptPath = path.join(root, "stage-prompt.md");
|
|
63
|
+
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness });
|
|
64
|
+
fs.writeFileSync(promptPath, prompt);
|
|
65
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
66
|
+
const binding = bindStageCoordinatorWork(context, {
|
|
67
|
+
workId: rootWork.work_id,
|
|
68
|
+
hookEventId: input.hookEventId,
|
|
69
|
+
stage,
|
|
70
|
+
promptPath,
|
|
71
|
+
resultPath: path.join(root, "stage-report.json"),
|
|
72
|
+
...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
|
|
73
|
+
});
|
|
74
|
+
attachFlowRunStage(context, {
|
|
75
|
+
projectRoot,
|
|
76
|
+
runId: input.runId,
|
|
77
|
+
stage,
|
|
78
|
+
dir: stageDir,
|
|
79
|
+
status: "running",
|
|
80
|
+
dataSchemaId: "dd-flow/stage-report@1"
|
|
81
|
+
});
|
|
82
|
+
const graph = codeWorkGraph(context, project.id, input.runId);
|
|
83
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
84
|
+
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
run_id: input.runId,
|
|
88
|
+
stage,
|
|
89
|
+
root_work_id: rootWork.work_id,
|
|
90
|
+
id: binding.work_session_id,
|
|
91
|
+
prompt_path: promptPath,
|
|
92
|
+
worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"),
|
|
93
|
+
...(externalContext ? { external_context: externalContext } : {}),
|
|
94
|
+
graph,
|
|
95
|
+
orchestration,
|
|
96
|
+
readiness,
|
|
97
|
+
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")) }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function finishVnextCode(context, input) {
|
|
101
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
102
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
103
|
+
const run = requireRun(context, project.id, input.runId);
|
|
104
|
+
const home = requireHome(run);
|
|
105
|
+
const root = path.join(home, stageDir);
|
|
106
|
+
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
107
|
+
if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
|
|
108
|
+
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"))) };
|
|
109
|
+
}
|
|
110
|
+
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
111
|
+
projectRoot,
|
|
112
|
+
workspaceRoot: run.workspace_root,
|
|
113
|
+
runId: run.id,
|
|
114
|
+
home
|
|
115
|
+
});
|
|
116
|
+
if (handoffFailures.length) {
|
|
117
|
+
throw new AppError("validation", "CODE handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
|
|
118
|
+
}
|
|
119
|
+
const works = codeWorks(context, project.id, run.id);
|
|
120
|
+
if (!works.length)
|
|
121
|
+
throw new AppError("runtime_missing", "CODE has no registered implementation Work", 1);
|
|
122
|
+
const incomplete = works.filter((work) => work.status !== "completed");
|
|
123
|
+
if (incomplete.length) {
|
|
124
|
+
throw new AppError("code_graph_incomplete", "CODE cannot finish while implementation or repair Work is unsettled", 2, {
|
|
125
|
+
works: incomplete.map((work) => ({ work_id: work.work_id, status: work.status }))
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const accepted = readVnextSpecifyResult(path.join(home, "01-specify", "specify.json"));
|
|
129
|
+
const expected = new Set([...accepted.requirements, ...accepted.acceptance_criteria].map((item) => item.id));
|
|
130
|
+
const covered = new Set(works.flatMap((work) => packet(work)?.requirements.map((item) => item.id) ?? []));
|
|
131
|
+
const missing = [...expected].filter((id) => !covered.has(id));
|
|
132
|
+
if (missing.length) {
|
|
133
|
+
throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
|
|
134
|
+
}
|
|
135
|
+
// Reject a malformed semantic receipt before running the expensive gate.
|
|
136
|
+
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id });
|
|
137
|
+
if (verification.verdict !== "passed" || verification.unresolved.length > 0) {
|
|
138
|
+
throw new AppError("code_verification_incomplete", "CODE verification is not accepted; create or complete the necessary repair Work first", 2, { verification });
|
|
139
|
+
}
|
|
140
|
+
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
141
|
+
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks });
|
|
142
|
+
if (unchangedFailures.length)
|
|
143
|
+
throw new AppError("code_gate_repair_required", "CODE final gate already failed for the unchanged workspace; repair the evidenced failure before retrying", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures });
|
|
144
|
+
let receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks });
|
|
145
|
+
if (!receipts) {
|
|
146
|
+
const release = acquireAggregateGateLock(root);
|
|
147
|
+
try {
|
|
148
|
+
// A prior caller may have completed the slow gate while this caller was
|
|
149
|
+
// waiting to acquire the lock. Reuse that immutable evidence.
|
|
150
|
+
receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
|
|
151
|
+
projectId: project.id,
|
|
152
|
+
runId: run.id,
|
|
153
|
+
runHome: home,
|
|
154
|
+
workspaceRoot: run.workspace_root,
|
|
155
|
+
scope: "aggregate",
|
|
156
|
+
checks,
|
|
157
|
+
...(input.progress ? { progress: input.progress } : {})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
release();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const failed = receipts.filter((receipt) => receipt.status === "failed");
|
|
165
|
+
if (failed.length) {
|
|
166
|
+
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
167
|
+
failures: failed,
|
|
168
|
+
outcome: "repair_required",
|
|
169
|
+
retry_after_workspace_change: true,
|
|
170
|
+
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
171
|
+
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`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
|
|
175
|
+
const finalReceipts = latestReceiptsByCommand(allReceipts);
|
|
176
|
+
const now = context.now();
|
|
177
|
+
const timing = stageTiming(home, stage, now);
|
|
178
|
+
const changedPaths = [...new Set(works.flatMap((work) => resultPaths(work.result)))];
|
|
179
|
+
const next = nextAction(run, changedPaths);
|
|
180
|
+
writeProtocolFlowStatus(run.workspace_root, home, run.id, next === "start_code_review" ? "CODE complete; CODE-REVIEW is next." : "CODE complete; this RUN reached its configured terminal boundary.");
|
|
181
|
+
const report = {
|
|
182
|
+
schema_id: "dd-flow/stage-report@1",
|
|
183
|
+
run_id: run.id,
|
|
184
|
+
stage,
|
|
185
|
+
generated_at: now,
|
|
186
|
+
verdict: "done",
|
|
187
|
+
verification: verificationProjection(works, finalReceipts),
|
|
188
|
+
semantic: {
|
|
189
|
+
result: `Completed ${works.length} CODE Work item${works.length === 1 ? "" : "s"}; ${finalReceipts.filter((receipt) => receipt.status === "passed").length} current final check receipt${finalReceipts.length === 1 ? "" : "s"} passed.`,
|
|
190
|
+
acceptance: [...expected],
|
|
191
|
+
changed_files: changedPaths,
|
|
192
|
+
checks: finalReceipts.map((receipt) => receipt.command),
|
|
193
|
+
evidence: finalReceipts.map((receipt) => receipt.receipt_path),
|
|
194
|
+
next_action: next,
|
|
195
|
+
code: {
|
|
196
|
+
work_ids: works.map((work) => work.work_id),
|
|
197
|
+
requirement_coverage: [...covered].sort(),
|
|
198
|
+
repairs: works.filter((work) => packet(work)?.repair).map((work) => ({ work_id: work.work_id, ...packet(work).repair })),
|
|
199
|
+
verification
|
|
200
|
+
},
|
|
201
|
+
plan_conformance: verification
|
|
202
|
+
},
|
|
203
|
+
mechanical: {
|
|
204
|
+
started_at: timing.started_at,
|
|
205
|
+
finished_at: now,
|
|
206
|
+
wall_clock_ms: timing.wall_clock_ms,
|
|
207
|
+
git: gitFacts(run.workspace_root),
|
|
208
|
+
graph: codeWorkGraph(context, project.id, run.id),
|
|
209
|
+
session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`,
|
|
210
|
+
usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`
|
|
211
|
+
},
|
|
212
|
+
artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
|
|
213
|
+
validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
|
|
214
|
+
};
|
|
215
|
+
writeReport(root, report);
|
|
216
|
+
completeFlowRunStage(context, {
|
|
217
|
+
projectRoot,
|
|
218
|
+
runId: run.id,
|
|
219
|
+
stage,
|
|
220
|
+
status: "done",
|
|
221
|
+
data: "stage-report.json",
|
|
222
|
+
dataSchemaId: "dd-flow/stage-report@1",
|
|
223
|
+
report: "stage-report.md",
|
|
224
|
+
stageReport: "stage-report.html"
|
|
225
|
+
});
|
|
226
|
+
const rootWork = requireRootWork(context, project.id, run.id);
|
|
227
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
228
|
+
type: "code_accepted",
|
|
229
|
+
work_id: rootWork.work_id,
|
|
230
|
+
checks: finalReceipts.map((receipt) => receipt.id)
|
|
231
|
+
});
|
|
232
|
+
if (next === "code_completed") {
|
|
233
|
+
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
234
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: "start_code_review" });
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
run_id: run.id,
|
|
242
|
+
stage,
|
|
243
|
+
outcome: "accepted",
|
|
244
|
+
report_path: path.join(root, "stage-report.json"),
|
|
245
|
+
next_action: next,
|
|
246
|
+
...(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` } } : {})
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function addVnextCodeRepair(context, input) {
|
|
250
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
251
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
252
|
+
const run = requireRun(context, project.id, input.runId);
|
|
253
|
+
const home = requireHome(run);
|
|
254
|
+
if (!input.objective.trim())
|
|
255
|
+
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
256
|
+
if (!input.checkReceiptId && !input.reviewFindingIds?.length) {
|
|
257
|
+
throw new AppError("validation", "Repair needs a failed check receipt or CODE-REVIEW finding", 2);
|
|
258
|
+
}
|
|
259
|
+
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 = ?", [
|
|
260
|
+
project.id,
|
|
261
|
+
input.checkReceiptId
|
|
262
|
+
]) : undefined;
|
|
263
|
+
if (input.checkReceiptId && (!receipt || receipt.run_id !== run.id || receipt.status !== "failed")) {
|
|
264
|
+
throw new AppError("validation", "Repair requires a failed check receipt from the same RUN", 2, {
|
|
265
|
+
check_receipt_id: input.checkReceiptId
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
|
|
269
|
+
const packets = origins.map((work) => packet(work));
|
|
270
|
+
const invariantPackets = receipt
|
|
271
|
+
? codeWorks(context, project.id, run.id).map((work) => packet(work)).filter((value) => !value.repair)
|
|
272
|
+
: packets;
|
|
273
|
+
const first = packets[0];
|
|
274
|
+
const declaredChecks = finalCodeCheckDeclarations(run.workspace_root, packets.flatMap((value) => value.checks));
|
|
275
|
+
const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
|
|
276
|
+
if (receipt && !failedCheck) {
|
|
277
|
+
throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
|
|
278
|
+
check_receipt_id: receipt.id,
|
|
279
|
+
declaration_id: receipt.declaration_id
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const key = input.reviewFindingIds?.length ? "code-review-repair" : "code-gate-repair";
|
|
283
|
+
const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
|
|
284
|
+
const repair = {
|
|
285
|
+
schema_id: "dd-flow/code-work-packet@5",
|
|
286
|
+
key,
|
|
287
|
+
launch_policy: "fresh_agent_required",
|
|
288
|
+
source: first.source,
|
|
289
|
+
repair: {
|
|
290
|
+
origin_work_ids: origins.map((work) => work.work_id),
|
|
291
|
+
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
292
|
+
...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []) } : {})
|
|
293
|
+
},
|
|
294
|
+
task: input.objective,
|
|
295
|
+
semantic_spine: {
|
|
296
|
+
user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
|
|
297
|
+
component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
|
|
298
|
+
must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
|
|
299
|
+
non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
|
|
300
|
+
acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindingIds.join(", ")}`
|
|
301
|
+
},
|
|
302
|
+
requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
|
|
303
|
+
acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
|
|
304
|
+
document_updates: receipt ? [] : uniqueBy(packets.flatMap((value) => value.document_updates).filter((update) => (input.reviewEvidenceRefs ?? []).some((reference) => reference === update.path || reference.startsWith(`${update.path}:`) || reference.endsWith(`/${update.path}`))), (value) => JSON.stringify(value)),
|
|
305
|
+
required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...packets.flatMap((value) => value.required_read)]),
|
|
306
|
+
discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
|
|
307
|
+
// These are collision-avoidance hints. Receipt paths enrich the coordinator
|
|
308
|
+
// picture but never restrict the repair's project-local edits.
|
|
309
|
+
planned_write_areas: unique([...packets.flatMap((value) => value.planned_write_areas), ...receiptWriteScope]),
|
|
310
|
+
// Receipts record the resolved shell command. A repair must retain the
|
|
311
|
+
// accepted declaration (including its immutable @check alias) and only
|
|
312
|
+
// change when it runs, so work finish can validate it again.
|
|
313
|
+
checks: uniqueBy([...(failedCheck ? [{ ...failedCheck, purpose: "Re-run the failed accepted check.", run_at: "work" }] : []), ...packets.flatMap((value) => value.checks)], (check) => check.id),
|
|
314
|
+
provides_checks: [],
|
|
315
|
+
stop_conditions: unique([
|
|
316
|
+
...invariantPackets.flatMap((value) => value.stop_conditions),
|
|
317
|
+
...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : [])
|
|
318
|
+
]),
|
|
319
|
+
depends_on: origins.map((work) => work.work_id),
|
|
320
|
+
result_schema: "dd-flow/code-work-result@2"
|
|
321
|
+
};
|
|
322
|
+
const file = path.join(home, stageDir, `.repair-${crypto.randomUUID()}.json`);
|
|
323
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
324
|
+
fs.writeFileSync(file, `${JSON.stringify({ works: [repair] }, null, 2)}\n`);
|
|
325
|
+
let registered;
|
|
326
|
+
try {
|
|
327
|
+
registered = addWorkBatch(context, { parentWorkId: requireRootWork(context, project.id, run.id).work_id, file });
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
fs.rmSync(file, { force: true });
|
|
331
|
+
}
|
|
332
|
+
const id = String(registered.work_ids[key]);
|
|
333
|
+
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
|
|
334
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
335
|
+
type: "code_repair_created",
|
|
336
|
+
work_id: id,
|
|
337
|
+
origin_work_ids: input.originWorkIds,
|
|
338
|
+
...(receipt ? { check_receipt_id: receipt.id } : { review_finding_ids: input.reviewFindingIds })
|
|
339
|
+
});
|
|
340
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
341
|
+
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
342
|
+
}
|
|
343
|
+
function receiptRepairPaths(workspaceRoot, receipt) {
|
|
344
|
+
const root = path.resolve(workspaceRoot);
|
|
345
|
+
const text = [receipt.stdout_path, receipt.stderr_path]
|
|
346
|
+
.filter((file) => fs.existsSync(file))
|
|
347
|
+
.map((file) => fs.readFileSync(file, "utf8"))
|
|
348
|
+
.join("\n");
|
|
349
|
+
const paths = new Set();
|
|
350
|
+
for (const raw of text.split(/\s+/)) {
|
|
351
|
+
const token = raw
|
|
352
|
+
.replace(/^[^A-Za-z0-9_@./\\-]+|[^A-Za-z0-9_@./\\-]+$/g, "")
|
|
353
|
+
.replace(/:\d+(?::\d+)?$/, "");
|
|
354
|
+
if (!token || token.split(/[\\/]/).includes(".."))
|
|
355
|
+
continue;
|
|
356
|
+
const absolute = path.isAbsolute(token) ? path.resolve(token) : path.resolve(root, token);
|
|
357
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
358
|
+
if (!relative || relative.startsWith("../") || path.isAbsolute(relative))
|
|
359
|
+
continue;
|
|
360
|
+
if (relative.split("/").some((part) => part === ".git" || part === "node_modules"))
|
|
361
|
+
continue;
|
|
362
|
+
if (fs.existsSync(absolute) && fs.statSync(absolute).isFile())
|
|
363
|
+
paths.add(relative);
|
|
364
|
+
}
|
|
365
|
+
return [...paths].sort();
|
|
366
|
+
}
|
|
367
|
+
function coordinatorPrompt(context, input) {
|
|
368
|
+
const graph = codeWorkGraph(context, input.run.project_id, input.run.id);
|
|
369
|
+
const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.run.id });
|
|
370
|
+
const capacity = variables.variables?.["runtime.subagent.available_slots"];
|
|
371
|
+
const template = fs.readFileSync(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
372
|
+
return [
|
|
373
|
+
"<stage_identity>",
|
|
374
|
+
`- RUN: ${input.run.id}`,
|
|
375
|
+
`- root Work: ${input.rootWork.work_id}`,
|
|
376
|
+
"- stage: code",
|
|
377
|
+
"</stage_identity>",
|
|
378
|
+
"",
|
|
379
|
+
"<trusted_runtime_context>",
|
|
380
|
+
`- project root: ${input.projectRoot}`,
|
|
381
|
+
`- immutable write workspace: ${input.run.workspace_root}`,
|
|
382
|
+
`- stage workspace: ${input.root}`,
|
|
383
|
+
`- workspace readiness receipt: ${String(input.readiness.receipt_path)}`,
|
|
384
|
+
`- available subagent slots: ${typeof capacity === "number" ? capacity : "not measured; delegate conservatively"}`,
|
|
385
|
+
"The CLI validated the accepted PLAN projection, workspace route and registered Work graph. Do not repeat that discovery.",
|
|
386
|
+
"</trusted_runtime_context>",
|
|
387
|
+
"",
|
|
388
|
+
"<code_graph>",
|
|
389
|
+
JSON.stringify(graph, null, 2),
|
|
390
|
+
"</code_graph>",
|
|
391
|
+
"",
|
|
392
|
+
"<execution_commands>",
|
|
393
|
+
"Launch only entries listed in graph.ready. Use at most the known available slot count. 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, use 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`,
|
|
394
|
+
"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.",
|
|
395
|
+
`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.`,
|
|
396
|
+
`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"))}`,
|
|
397
|
+
"If the aggregate gate fails, use the returned repair command with the relevant completed origin Work IDs and a concise repair objective. Do not edit invisibly in the root orchestrator.",
|
|
398
|
+
"</execution_commands>",
|
|
399
|
+
"",
|
|
400
|
+
"<verification_contract>",
|
|
401
|
+
"```json",
|
|
402
|
+
JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed | needs_repair | blocked", summary: "Evidence-backed conclusion.", unresolved: [], deviations: [] }, null, 2),
|
|
403
|
+
"```",
|
|
404
|
+
"The CLI verifies graph coverage and executes all deterministic checks. Do not duplicate its report; record only your semantic conclusion and evidence.",
|
|
405
|
+
"</verification_contract>",
|
|
406
|
+
"",
|
|
407
|
+
"<stage_instructions>",
|
|
408
|
+
template,
|
|
409
|
+
"</stage_instructions>",
|
|
410
|
+
""
|
|
411
|
+
].join("\n");
|
|
412
|
+
}
|
|
413
|
+
export function profileCommands(workspaceRoot) {
|
|
414
|
+
return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
|
|
415
|
+
}
|
|
416
|
+
function latestReceiptsByCommand(receipts) {
|
|
417
|
+
const latest = new Map();
|
|
418
|
+
for (const receipt of receipts)
|
|
419
|
+
latest.set(receipt.declaration_id, receipt);
|
|
420
|
+
return [...latest.values()];
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* A CODE finish may be retried after a client loses the long command's output.
|
|
424
|
+
* Reusing complete, same-workspace receipts is correct; rerunning them is both
|
|
425
|
+
* expensive and can race the still-settling original invocation.
|
|
426
|
+
*/
|
|
427
|
+
export function reusableAggregateGateReceipts(context, input) {
|
|
428
|
+
const expected = new Set(input.checks.map((check) => check.id));
|
|
429
|
+
const current = workspaceFingerprint(input.workspaceRoot);
|
|
430
|
+
const latest = new Map();
|
|
431
|
+
for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
|
|
432
|
+
if (receipt.scope === "aggregate" && expected.has(receipt.declaration_id))
|
|
433
|
+
latest.set(receipt.declaration_id, receipt);
|
|
434
|
+
}
|
|
435
|
+
if (latest.size !== expected.size)
|
|
436
|
+
return null;
|
|
437
|
+
const receipts = input.checks.map((check) => latest.get(check.id));
|
|
438
|
+
return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
|
|
439
|
+
}
|
|
440
|
+
/** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
|
|
441
|
+
function acquireAggregateGateLock(root) {
|
|
442
|
+
const directory = path.join(root, "checks");
|
|
443
|
+
const lock = path.join(directory, ".aggregate-gate.lock");
|
|
444
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
445
|
+
try {
|
|
446
|
+
const descriptor = fs.openSync(lock, "wx");
|
|
447
|
+
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
448
|
+
fs.closeSync(descriptor);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
if (!isExistingLock(error))
|
|
452
|
+
throw error;
|
|
453
|
+
const activePid = lockPid(lock);
|
|
454
|
+
if (activePid && processAlive(activePid)) {
|
|
455
|
+
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 });
|
|
456
|
+
}
|
|
457
|
+
fs.rmSync(lock, { force: true });
|
|
458
|
+
return acquireAggregateGateLock(root);
|
|
459
|
+
}
|
|
460
|
+
return () => fs.rmSync(lock, { force: true });
|
|
461
|
+
}
|
|
462
|
+
function isExistingLock(error) {
|
|
463
|
+
return Boolean(error && typeof error === "object" && error.code === "EEXIST");
|
|
464
|
+
}
|
|
465
|
+
function lockPid(lock) {
|
|
466
|
+
try {
|
|
467
|
+
const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
|
|
468
|
+
return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function processAlive(pid) {
|
|
475
|
+
try {
|
|
476
|
+
process.kill(pid, 0);
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function verificationProjection(works, receipts) {
|
|
484
|
+
const packets = works.map((work) => packet(work)).filter((value) => value !== null);
|
|
485
|
+
const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
|
|
486
|
+
const receiptByDeclaration = new Map(receipts.map((receipt) => [receipt.declaration_id, receipt]));
|
|
487
|
+
const evidence = new Map();
|
|
488
|
+
for (const work of works) {
|
|
489
|
+
const result = readJsonResult(work.result);
|
|
490
|
+
for (const item of result.evidence ?? [])
|
|
491
|
+
if (item.criterion_id)
|
|
492
|
+
evidence.set(item.criterion_id, unique([...(evidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
|
|
493
|
+
}
|
|
494
|
+
const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
|
|
495
|
+
return {
|
|
496
|
+
checks: declarations.map((check) => { const receipt = receiptByDeclaration.get(check.id); 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 }; }),
|
|
497
|
+
acceptance: acceptance.map((item) => ({ criterion_id: item.criterion_id, gate: item.gate, status: ["work", "code", "readiness"].includes(item.gate) ? ((item.check_refs.every((id) => receiptByDeclaration.get(id)?.status === "passed") && (evidence.get(item.criterion_id)?.length ?? 0) > 0) ? "confirmed" : "unresolved") : "not_due", check_refs: item.check_refs, evidence_refs: evidence.get(item.criterion_id) ?? [] }))
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function readJsonResult(value) { try {
|
|
501
|
+
const parsed = JSON.parse(value ?? "{}");
|
|
502
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return {};
|
|
506
|
+
} }
|
|
507
|
+
function stageTiming(home, name, finishedAt) {
|
|
508
|
+
const run = JSON.parse(fs.readFileSync(path.join(home, "run.json"), "utf8"));
|
|
509
|
+
const startedAt = run.stage_runs?.find((item) => item.stage === name)?.started_at;
|
|
510
|
+
const started = Date.parse(startedAt ?? "");
|
|
511
|
+
const finished = Date.parse(finishedAt);
|
|
512
|
+
if (!startedAt || !Number.isFinite(started) || !Number.isFinite(finished)) {
|
|
513
|
+
throw new AppError("stage_timing_unavailable", "CODE lifecycle timestamps are unavailable or invalid", 1, { stage: name, started_at: startedAt ?? null, finished_at: finishedAt });
|
|
514
|
+
}
|
|
515
|
+
return { started_at: startedAt, wall_clock_ms: Math.max(0, finished - started) };
|
|
516
|
+
}
|
|
517
|
+
function codeWorks(context, projectId, runId) {
|
|
518
|
+
return context.db
|
|
519
|
+
.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId])
|
|
520
|
+
.filter((work) => Boolean(packet(work)));
|
|
521
|
+
}
|
|
522
|
+
function packet(work) {
|
|
523
|
+
if (!work.payload_json)
|
|
524
|
+
return null;
|
|
525
|
+
try {
|
|
526
|
+
const value = JSON.parse(work.payload_json);
|
|
527
|
+
return value.schema_id === "dd-flow/code-work-packet@5" ? value : null;
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function requireCodeWork(context, projectId, runId, workId) {
|
|
534
|
+
const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [
|
|
535
|
+
projectId,
|
|
536
|
+
runId,
|
|
537
|
+
workId
|
|
538
|
+
]);
|
|
539
|
+
if (!work || work.status !== "completed" || !packet(work)) {
|
|
540
|
+
throw new AppError("validation", "Repair origin must be a completed CODE Work in the same RUN", 2, { work_id: workId });
|
|
541
|
+
}
|
|
542
|
+
return work;
|
|
543
|
+
}
|
|
544
|
+
function requireRootWork(context, projectId, runId) {
|
|
545
|
+
const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]);
|
|
546
|
+
if (!work || work.status !== "running")
|
|
547
|
+
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1);
|
|
548
|
+
return work;
|
|
549
|
+
}
|
|
550
|
+
function requireRun(context, projectId, runId) {
|
|
551
|
+
const run = context.db.get("SELECT id, project_id, run_home_path, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
552
|
+
if (!run)
|
|
553
|
+
throw new AppError("not_found", "RUN is not registered", 1);
|
|
554
|
+
return run;
|
|
555
|
+
}
|
|
556
|
+
/** CODE, not PROTOCOLIZE, makes a checkout ready for project tooling. */
|
|
557
|
+
async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress) {
|
|
558
|
+
const profile = JSON.parse(run.index_json);
|
|
559
|
+
const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
|
|
560
|
+
if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
|
|
561
|
+
throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
|
|
562
|
+
}
|
|
563
|
+
const receiptPath = path.join(root, "workspace-readiness.json");
|
|
564
|
+
const prior = readReadiness(receiptPath);
|
|
565
|
+
if (prior?.workspace_root === run.workspace_root && prior.command === bootstrap.command && prior.status === "passed" && JSON.stringify(prior.check_declarations ?? []) === JSON.stringify(readinessChecks))
|
|
566
|
+
return { ...prior, reused: true };
|
|
567
|
+
const startedAt = new Date().toISOString();
|
|
568
|
+
progress?.(`workspace bootstrap started: ${bootstrap.command}`);
|
|
569
|
+
const result = await runBootstrap(bootstrap.command, run.workspace_root, codeExecutionEnvironment(run.workspace_root), progress);
|
|
570
|
+
const bootstrapPassed = result.exit_code === 0 && !result.error;
|
|
571
|
+
const checks = bootstrapPassed && readinessChecks.length
|
|
572
|
+
? await runCodeChecks(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, scope: "aggregate", artifactDir: "05-code/readiness", checks: readinessChecks, ...(progress ? { progress } : {}) })
|
|
573
|
+
: [];
|
|
574
|
+
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 };
|
|
575
|
+
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
576
|
+
if (receipt.status !== "passed")
|
|
577
|
+
throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
|
|
578
|
+
progress?.("workspace bootstrap passed");
|
|
579
|
+
return receipt;
|
|
580
|
+
}
|
|
581
|
+
function runBootstrap(command, cwd, environment, progress) {
|
|
582
|
+
return new Promise((resolve) => {
|
|
583
|
+
const started = Date.now();
|
|
584
|
+
const child = spawn("/bin/sh", ["-lc", command], { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
|
|
585
|
+
let stdout = "";
|
|
586
|
+
let stderr = "";
|
|
587
|
+
let error = null;
|
|
588
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
589
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
590
|
+
const heartbeat = setInterval(() => progress?.(`workspace bootstrap still running (${Math.floor((Date.now() - started) / 1000)}s)`), 15_000);
|
|
591
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), 15 * 60 * 1000);
|
|
592
|
+
child.once("error", (value) => { error = value.message; });
|
|
593
|
+
child.once("close", (exitCode, signal) => {
|
|
594
|
+
clearInterval(heartbeat);
|
|
595
|
+
clearTimeout(timeout);
|
|
596
|
+
resolve({ exit_code: exitCode, stdout, stderr, error: error ?? (signal ? `terminated by ${signal}` : null) });
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function readReadiness(file) {
|
|
601
|
+
try {
|
|
602
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function requireHome(run) {
|
|
609
|
+
if (!run.run_home_path)
|
|
610
|
+
throw new AppError("runtime_missing", "RUN workspace is unavailable", 1);
|
|
611
|
+
return run.run_home_path;
|
|
612
|
+
}
|
|
613
|
+
function finishCommand(context, runId, projectRoot, verificationFile) {
|
|
614
|
+
return `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`;
|
|
615
|
+
}
|
|
616
|
+
function readVerification(context, input) {
|
|
617
|
+
const file = path.resolve(input.file);
|
|
618
|
+
validateSchema({ schemaName: "code-verification", file, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
|
|
619
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
620
|
+
}
|
|
621
|
+
function verificationForFinish(context, input) {
|
|
622
|
+
return readVerification(context, input);
|
|
623
|
+
}
|
|
624
|
+
/** Keep protocol summaries truthful after deterministic lifecycle transitions. */
|
|
625
|
+
export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
|
|
626
|
+
const reportPath = path.join(runHome, "02-protocolize", "stage-report.json");
|
|
627
|
+
if (!fs.existsSync(reportPath))
|
|
628
|
+
return;
|
|
629
|
+
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
630
|
+
const protocolIds = Array.isArray(report.semantic?.acceptance) ? report.semantic.acceptance.filter((value) => typeof value === "string") : [];
|
|
631
|
+
for (const protocolId of protocolIds) {
|
|
632
|
+
const file = path.join(workspaceRoot, ".memory-bank", "protocol", protocolId, "summary.md");
|
|
633
|
+
if (!fs.existsSync(file))
|
|
634
|
+
continue;
|
|
635
|
+
const previous = fs.readFileSync(file, "utf8").replace(/\n## Latest RUN state\n[\s\S]*$/u, "\n");
|
|
636
|
+
fs.writeFileSync(file, `${previous.trimEnd()}\n\n## Latest RUN state\n\n- Source RUN: ${runId}\n- ${status}\n`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function nextAction(run, changedPaths) {
|
|
640
|
+
const index = JSON.parse(run.index_json);
|
|
641
|
+
const requested = index.settings?.code_review?.mode ?? "auto";
|
|
642
|
+
const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
|
|
643
|
+
return enabled && index.execution_profile?.settings?.stop_target === "code_review_completed" ? "start_code_review" : "code_completed";
|
|
644
|
+
}
|
|
645
|
+
function stageStatus(run, name) {
|
|
646
|
+
try {
|
|
647
|
+
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function changedPathsFromReport(file) {
|
|
654
|
+
try {
|
|
655
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
656
|
+
return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function resultPaths(result) {
|
|
663
|
+
try {
|
|
664
|
+
const value = JSON.parse(result ?? "");
|
|
665
|
+
return Array.isArray(value.changed_paths) ? value.changed_paths.filter((item) => typeof item === "string") : [];
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function unique(values) {
|
|
672
|
+
return [...new Set(values)];
|
|
673
|
+
}
|
|
674
|
+
function uniqueBy(values, key) {
|
|
675
|
+
const seen = new Set();
|
|
676
|
+
return values.filter((value) => {
|
|
677
|
+
const id = key(value);
|
|
678
|
+
if (seen.has(id))
|
|
679
|
+
return false;
|
|
680
|
+
seen.add(id);
|
|
681
|
+
return true;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function writeReport(root, report) {
|
|
685
|
+
writeStageReport(root, report);
|
|
686
|
+
}
|