@deksden-com/dd-flow-cli 0.9.0-beta.48 → 0.9.0-beta.51
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 +22 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +4 -1
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/code-checks.js +237 -32
- package/dist/services/runs.js +16 -0
- package/dist/services/usage.js +4 -3
- package/dist/services/vnext-code-review.js +45 -21
- package/dist/services/vnext-code.js +63 -123
- package/dist/services/vnext-merge.js +73 -24
- package/dist/services/vnext-plan-review.js +24 -19
- package/dist/services/vnext-plan.js +4 -0
- package/dist/services/work-registry.js +32 -8
- package/dist/services/workspace-bootstrap.js +29 -12
- package/dist/storage/database.js +6 -0
- package/package.json +1 -1
|
@@ -10,15 +10,17 @@ import { validateSchema } from "./schema-validation.js";
|
|
|
10
10
|
import { flowCommand } from "./stage-pause.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
14
|
-
import { finalCodeCheckDeclarations, runCodeChecks,
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork,
|
|
13
|
+
import { addVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
14
|
+
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
15
|
+
import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
|
+
import { withStageSettlement } from "./runs.js";
|
|
16
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
19
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
20
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
20
21
|
import { ensureVnextMergeRequest } from "./vnext-merge.js";
|
|
21
22
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
23
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
22
24
|
const stage = "code-review";
|
|
23
25
|
const stageDir = vnextStageDirectory(stage);
|
|
24
26
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -123,6 +125,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
123
125
|
const mode = reviewContext.mode;
|
|
124
126
|
if (!mode)
|
|
125
127
|
throw new AppError("runtime_missing", "CODE-REVIEW effective mode was not frozen at stage start", 1);
|
|
128
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
126
129
|
if (reviewContext.code_report_sha256 !== sha256File(path.join(home, "05-code", "stage-report.json")))
|
|
127
130
|
throw new AppError("code_review_input_changed", "Accepted CODE report changed after CODE-REVIEW start", 2);
|
|
128
131
|
const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
|
|
@@ -131,7 +134,9 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
131
134
|
const reportPath = path.join(root, "stage-report.json");
|
|
132
135
|
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
133
136
|
const report = readJson(reportPath);
|
|
134
|
-
|
|
137
|
+
const next = report.semantic?.next_action ?? "code_review_completed";
|
|
138
|
+
resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next);
|
|
139
|
+
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: next };
|
|
135
140
|
}
|
|
136
141
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
137
142
|
const coordinator = context.db.get("SELECT session_id, result_path FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [rootWork.work_id]);
|
|
@@ -145,6 +150,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
145
150
|
const cycle = reviewContext.cycle ?? "initial";
|
|
146
151
|
const reviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
147
152
|
if (mode !== "off") {
|
|
153
|
+
assertCodeReviewCoverage(reviewGroups(home), reviewers);
|
|
148
154
|
const incomplete = reviewers.filter((work) => work.status !== "completed");
|
|
149
155
|
if (incomplete.length)
|
|
150
156
|
throw new AppError("reviewer_jobs_incomplete", "CODE-REVIEW cannot finish while reviewer Work is unsettled", 2, { works: incomplete.map((work) => ({ work_id: work.work_id, status: work.status })) });
|
|
@@ -173,13 +179,10 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
173
179
|
const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
174
180
|
const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
|
|
175
181
|
if (input.retryCheckId) {
|
|
176
|
-
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks,
|
|
182
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, reason: input.retryReason });
|
|
177
183
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
178
184
|
}
|
|
179
|
-
const
|
|
180
|
-
if (unchangedFailures.length)
|
|
181
|
-
throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; choose source repair or restore the environment and retry that receipt", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures, retry_command: `${finishCommand(context, run.id, projectRoot, decisionFile)} --retry-check ${unchangedFailures[0].id} --reason "<environment recovery evidence>"` });
|
|
182
|
-
const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.progress ? { progress: input.progress } : {}) });
|
|
185
|
+
const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}), ...(input.progress ? { progress: input.progress } : {}) });
|
|
183
186
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
184
187
|
if (failed.length)
|
|
185
188
|
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
@@ -200,18 +203,21 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
200
203
|
const acceptedPaths = changedPaths(run.workspace_root);
|
|
201
204
|
const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
202
205
|
writeReport(root, report);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
withStageSettlement(context, () => {
|
|
207
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
208
|
+
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
209
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
210
|
+
if (nextAction === "start_merge")
|
|
211
|
+
advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
|
|
212
|
+
else {
|
|
213
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
214
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
|
|
215
|
+
}
|
|
216
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
217
|
+
});
|
|
212
218
|
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {}) };
|
|
213
219
|
}
|
|
214
|
-
function reviewGroups(home) {
|
|
220
|
+
export function reviewGroups(home) {
|
|
215
221
|
const expected = new Set(baselineAspects);
|
|
216
222
|
const declared = [];
|
|
217
223
|
for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
|
|
@@ -226,15 +232,33 @@ function reviewGroups(home) {
|
|
|
226
232
|
if (!declared.length)
|
|
227
233
|
return [{ key: "baseline", aspect_ids: [...expected].sort() }];
|
|
228
234
|
const assigned = new Set();
|
|
235
|
+
const scoped = new Set();
|
|
229
236
|
for (const group of declared)
|
|
230
237
|
for (const aspectId of group.aspect_ids) {
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
const assignment = `${group.key.slice(0, group.key.lastIndexOf("/"))}/${aspectId}`;
|
|
239
|
+
if (scoped.has(assignment))
|
|
240
|
+
throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one group of the same protocol", 2, { aspect_id: aspectId });
|
|
241
|
+
scoped.add(assignment);
|
|
233
242
|
assigned.add(aspectId);
|
|
234
243
|
}
|
|
235
244
|
const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
|
|
236
245
|
return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
|
|
237
246
|
}
|
|
247
|
+
export function assertCodeReviewCoverage(groups, reviewers) {
|
|
248
|
+
const expected = new Map(groups.map(group => [group.key, [...group.aspect_ids].sort()]));
|
|
249
|
+
if (!expected.size || expected.size !== groups.length)
|
|
250
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW expected group identities must be nonempty and unique", 2);
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
for (const work of reviewers) {
|
|
253
|
+
const group = readJsonString(work.payload_json).group;
|
|
254
|
+
if (!group?.key || seen.has(group.key) || !expected.has(group.key) || !Array.isArray(group.aspect_ids) || JSON.stringify([...group.aspect_ids].sort()) !== JSON.stringify(expected.get(group.key)))
|
|
255
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW Work does not match its expected group", 2, { work_id: work.work_id });
|
|
256
|
+
seen.add(group.key);
|
|
257
|
+
}
|
|
258
|
+
const missing = [...expected.keys()].filter(key => !seen.has(key));
|
|
259
|
+
if (missing.length || !expected.size)
|
|
260
|
+
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
261
|
+
}
|
|
238
262
|
export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
|
|
239
263
|
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the same Finish command again. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
|
|
240
264
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
@@ -4,21 +4,22 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { aggregateCheckDeclarations,
|
|
7
|
+
import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
8
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
|
-
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
9
|
+
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
10
|
import { validateSchema } from "./schema-validation.js";
|
|
11
11
|
import { flowCommand } from "./stage-pause.js";
|
|
12
12
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
13
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
14
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph,
|
|
15
|
+
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
16
16
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
18
|
import { ensureVnextMergeRequest } from "./vnext-merge.js";
|
|
19
19
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
20
20
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
22
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
22
23
|
const stage = "code";
|
|
23
24
|
const stageDir = vnextStageDirectory("code");
|
|
24
25
|
export async function startVnextCode(context, input) {
|
|
@@ -36,6 +37,7 @@ export async function startVnextCode(context, input) {
|
|
|
36
37
|
const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
37
38
|
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
38
39
|
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
40
|
+
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(existingRoot, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
39
41
|
const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
40
42
|
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")) } };
|
|
41
43
|
}
|
|
@@ -108,8 +110,10 @@ export async function finishVnextCode(context, input) {
|
|
|
108
110
|
const run = requireRun(context, project.id, input.runId);
|
|
109
111
|
const home = requireHome(run);
|
|
110
112
|
const root = path.join(home, stageDir);
|
|
113
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
111
114
|
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
112
115
|
if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
|
|
116
|
+
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"))));
|
|
113
117
|
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"))) };
|
|
114
118
|
}
|
|
115
119
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
@@ -158,32 +162,14 @@ export async function finishVnextCode(context, input) {
|
|
|
158
162
|
}
|
|
159
163
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
160
164
|
if (input.retryCheckId) {
|
|
161
|
-
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks,
|
|
165
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
162
166
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
163
167
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const release = acquireAggregateGateLock(root);
|
|
170
|
-
try {
|
|
171
|
-
// A prior caller may have completed the slow gate while this caller was
|
|
172
|
-
// waiting to acquire the lock. Reuse that immutable evidence.
|
|
173
|
-
receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
|
|
174
|
-
projectId: project.id,
|
|
175
|
-
runId: run.id,
|
|
176
|
-
runHome: home,
|
|
177
|
-
workspaceRoot: run.workspace_root,
|
|
178
|
-
scope: "aggregate",
|
|
179
|
-
checks,
|
|
180
|
-
...(input.progress ? { progress: input.progress } : {})
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
release();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
168
|
+
const receipts = await runCodeChecks(context, {
|
|
169
|
+
projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
|
|
170
|
+
...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
|
|
171
|
+
...(input.progress ? { progress: input.progress } : {})
|
|
172
|
+
});
|
|
187
173
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
188
174
|
if (failed.length) {
|
|
189
175
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
@@ -195,9 +181,8 @@ export async function finishVnextCode(context, input) {
|
|
|
195
181
|
retry_command: `${finishCommand(context, run.id, projectRoot, input.verificationFile)} --retry-check ${failed[0].id} --reason "<environment recovery evidence>"`
|
|
196
182
|
});
|
|
197
183
|
}
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: allReceipts, runId: run.id, runHome: home });
|
|
184
|
+
const finalReceipts = receipts;
|
|
185
|
+
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
|
|
201
186
|
const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
|
|
202
187
|
if (unresolvedAcceptance.length)
|
|
203
188
|
throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
|
|
@@ -249,29 +234,32 @@ export async function finishVnextCode(context, input) {
|
|
|
249
234
|
// so an invalid merge policy leaves CODE safely running and retryable.
|
|
250
235
|
const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
251
236
|
writeReport(root, report);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
237
|
+
withStageSettlement(context, () => {
|
|
238
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
239
|
+
completeFlowRunStage(context, {
|
|
240
|
+
projectRoot,
|
|
241
|
+
runId: run.id,
|
|
242
|
+
stage,
|
|
243
|
+
status: "done",
|
|
244
|
+
data: "stage-report.json",
|
|
245
|
+
dataSchemaId: "dd-flow/stage-report@1",
|
|
246
|
+
report: "stage-report.md",
|
|
247
|
+
stageReport: "stage-report.html"
|
|
248
|
+
});
|
|
249
|
+
const rootWork = requireRootWork(context, project.id, run.id);
|
|
250
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
251
|
+
type: "code_accepted",
|
|
252
|
+
work_id: rootWork.work_id,
|
|
253
|
+
checks: finalReceipts.map((receipt) => receipt.id)
|
|
254
|
+
});
|
|
255
|
+
if (next === "code_completed") {
|
|
256
|
+
finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
257
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
261
|
+
}
|
|
267
262
|
});
|
|
268
|
-
if (next === "code_completed") {
|
|
269
|
-
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
270
|
-
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
274
|
-
}
|
|
275
263
|
return {
|
|
276
264
|
ok: true,
|
|
277
265
|
run_id: run.id,
|
|
@@ -455,7 +443,7 @@ function coordinatorPrompt(context, input) {
|
|
|
455
443
|
"",
|
|
456
444
|
"<execution_commands>",
|
|
457
445
|
"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`,
|
|
458
|
-
"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.",
|
|
446
|
+
"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.",
|
|
459
447
|
`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.`,
|
|
460
448
|
`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"))}`,
|
|
461
449
|
"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.",
|
|
@@ -477,76 +465,6 @@ function coordinatorPrompt(context, input) {
|
|
|
477
465
|
export function profileCommands(workspaceRoot) {
|
|
478
466
|
return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
|
|
479
467
|
}
|
|
480
|
-
function latestReceiptsByCommand(receipts) {
|
|
481
|
-
const latest = new Map();
|
|
482
|
-
for (const receipt of receipts)
|
|
483
|
-
for (const ref of receipt.check_refs)
|
|
484
|
-
latest.set(ref, receipt);
|
|
485
|
-
return [...new Set(latest.values())];
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* A CODE finish may be retried after a client loses the long command's output.
|
|
489
|
-
* Reusing complete, same-workspace receipts is correct; rerunning them is both
|
|
490
|
-
* expensive and can race the still-settling original invocation.
|
|
491
|
-
*/
|
|
492
|
-
export function reusableAggregateGateReceipts(context, input) {
|
|
493
|
-
const expected = new Set(input.checks.map((check) => check.id));
|
|
494
|
-
const current = workspaceFingerprint(input.workspaceRoot);
|
|
495
|
-
const latest = new Map();
|
|
496
|
-
for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
|
|
497
|
-
if (receipt.scope === "aggregate")
|
|
498
|
-
for (const ref of receipt.check_refs)
|
|
499
|
-
if (expected.has(ref))
|
|
500
|
-
latest.set(ref, receipt);
|
|
501
|
-
}
|
|
502
|
-
if (latest.size !== expected.size)
|
|
503
|
-
return null;
|
|
504
|
-
const receipts = input.checks.map((check) => latest.get(check.id));
|
|
505
|
-
return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
|
|
506
|
-
}
|
|
507
|
-
/** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
|
|
508
|
-
function acquireAggregateGateLock(root) {
|
|
509
|
-
const directory = path.join(root, "checks");
|
|
510
|
-
const lock = path.join(directory, ".aggregate-gate.lock");
|
|
511
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
512
|
-
try {
|
|
513
|
-
const descriptor = fs.openSync(lock, "wx");
|
|
514
|
-
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
515
|
-
fs.closeSync(descriptor);
|
|
516
|
-
}
|
|
517
|
-
catch (error) {
|
|
518
|
-
if (!isExistingLock(error))
|
|
519
|
-
throw error;
|
|
520
|
-
const activePid = lockPid(lock);
|
|
521
|
-
if (activePid && processAlive(activePid)) {
|
|
522
|
-
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 });
|
|
523
|
-
}
|
|
524
|
-
fs.rmSync(lock, { force: true });
|
|
525
|
-
return acquireAggregateGateLock(root);
|
|
526
|
-
}
|
|
527
|
-
return () => fs.rmSync(lock, { force: true });
|
|
528
|
-
}
|
|
529
|
-
function isExistingLock(error) {
|
|
530
|
-
return Boolean(error && typeof error === "object" && error.code === "EEXIST");
|
|
531
|
-
}
|
|
532
|
-
function lockPid(lock) {
|
|
533
|
-
try {
|
|
534
|
-
const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
|
|
535
|
-
return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
function processAlive(pid) {
|
|
542
|
-
try {
|
|
543
|
-
process.kill(pid, 0);
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
catch {
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
468
|
/**
|
|
551
469
|
* The immutable Work result is a repair history. The stage report is a current
|
|
552
470
|
* acceptance projection, so a registered receipt from an older check epoch
|
|
@@ -770,6 +688,28 @@ function uniqueBy(values, key) {
|
|
|
770
688
|
return true;
|
|
771
689
|
});
|
|
772
690
|
}
|
|
691
|
+
/** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
|
|
692
|
+
export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
|
|
693
|
+
const index = JSON.parse(run.index_json);
|
|
694
|
+
if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
|
|
695
|
+
return;
|
|
696
|
+
const terminal = next === "code_completed" || next === "code_review_completed";
|
|
697
|
+
if (!terminal && index.next_action === next)
|
|
698
|
+
return;
|
|
699
|
+
withStageSettlement(context, () => {
|
|
700
|
+
if (!terminal) {
|
|
701
|
+
advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
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]);
|
|
705
|
+
if (!work)
|
|
706
|
+
throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
|
|
707
|
+
if (work.status !== "completed")
|
|
708
|
+
finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
|
|
709
|
+
if (index.status !== "done")
|
|
710
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
|
|
711
|
+
});
|
|
712
|
+
}
|
|
773
713
|
function writeReport(root, report) {
|
|
774
714
|
writeStageReport(root, report);
|
|
775
715
|
}
|