@deksden-com/dd-flow-cli 0.9.0-beta.49 → 0.9.0-beta.52
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 +20 -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/managed-processes.js +7 -0
- package/dist/services/run-control.js +5 -1
- package/dist/services/runs.js +16 -0
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-stop.js +5 -1
- package/dist/services/usage.js +4 -3
- package/dist/services/vnext-code-review.js +45 -21
- package/dist/services/vnext-code.js +62 -122
- 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
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { createContext } from "../runtime/context.js";
|
|
5
5
|
import { getResourceDatabase } from "../storage/database.js";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
-
import { finishManagedProcess, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
7
|
+
import { finishManagedProcess, managedLeaseReleased, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
8
8
|
import { requestRuntimeScopeControl, runtimeBudgetStatus, scopeLocalProcessRole, validateRuntimeScopeManifest } from "./runtime-budget.js";
|
|
9
9
|
import { nativeInventoryVersion, recordRunControlIntent, retainedDaemonOperations, runControlStatus } from "./run-control.js";
|
|
10
10
|
import { requestRunControl } from "./run-control-worker.js";
|
|
@@ -125,7 +125,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
|
|
|
125
125
|
}
|
|
126
126
|
export function scopeRunProcessEvidence(record, runs, registryHome) {
|
|
127
127
|
const metadata = JSON.parse(record.metadata_json);
|
|
128
|
-
if (!record.pid || !record.pid_started_at || !record.project_id || !record.run_id || !metadata.dd_flow_home)
|
|
128
|
+
if ((!managedLeaseReleased(record) && (!record.pid || !record.pid_started_at)) || !record.project_id || !record.run_id || !metadata.dd_flow_home)
|
|
129
129
|
return null;
|
|
130
130
|
for (const run of runs) {
|
|
131
131
|
if (run.runtime_home !== metadata.dd_flow_home || run.project_id !== record.project_id || run.run_id !== record.run_id)
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { runHarnessAdapter } from "./harness-adapter.js";
|
|
6
|
-
import { managedProcessBinding, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
6
|
+
import { managedLeaseReleased, managedProcessBinding, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
7
7
|
import { fenceRuntimeBudget, runtimeBudgetStatus, scopeLocalProcessRole } from "./runtime-budget.js";
|
|
8
8
|
import { retainedDaemonOperations } from "./run-control.js";
|
|
9
9
|
import { historicalDaemonSettlement } from "./native-daemon-history.js";
|
|
@@ -62,6 +62,10 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
62
62
|
for (const record of runtimeBudgetStatus(context, scopeId).processes) {
|
|
63
63
|
if (covered.has(record.id))
|
|
64
64
|
continue;
|
|
65
|
+
if (managedLeaseReleased(record)) {
|
|
66
|
+
nodes.push({ process_id: record.id, settled: true });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
65
69
|
const metadata = JSON.parse(record.metadata_json);
|
|
66
70
|
// RUN control owns active controllers/checks. Adopt only its already-finished
|
|
67
71
|
// physical registrations here; death alone is not a completed RUN operation.
|
package/dist/services/usage.js
CHANGED
|
@@ -165,7 +165,7 @@ export function recalculateRunUsage(context, input) {
|
|
|
165
165
|
const usageFinal = final && unsettledSessions.length === 0;
|
|
166
166
|
const persistRunProjection = !input.sessionId && !input.stage;
|
|
167
167
|
if (persistRunProjection) {
|
|
168
|
-
context.db.exec("
|
|
168
|
+
context.db.exec("SAVEPOINT run_usage");
|
|
169
169
|
try {
|
|
170
170
|
context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
|
|
171
171
|
for (const row of rows) {
|
|
@@ -185,10 +185,11 @@ export function recalculateRunUsage(context, input) {
|
|
|
185
185
|
AND status = 'idle'
|
|
186
186
|
AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')`, [observedAt, session.completed_at, input.projectId, input.runId, session.session_id, session.session_id]);
|
|
187
187
|
}
|
|
188
|
-
context.db.exec("
|
|
188
|
+
context.db.exec("RELEASE run_usage");
|
|
189
189
|
}
|
|
190
190
|
catch (error) {
|
|
191
|
-
context.db.exec("ROLLBACK");
|
|
191
|
+
context.db.exec("ROLLBACK TO run_usage");
|
|
192
|
+
context.db.exec("RELEASE run_usage");
|
|
192
193
|
throw error;
|
|
193
194
|
}
|
|
194
195
|
}
|
|
@@ -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,
|
|
@@ -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
|
}
|