@deksden-com/dd-flow-cli 0.9.0-beta.88 → 0.9.0-beta.89
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 +6 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/command-inputs.js +13 -10
- package/dist/cli/help.js +2 -0
- package/dist/cli/hook-ingress.js +2 -2
- package/dist/cli/run-cli.js +27 -12
- package/dist/services/codex-hook-delivery.js +3 -3
- package/dist/services/command-context.js +80 -0
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +4 -2
- package/dist/services/hooks.js +21 -15
- package/dist/services/lifecycle-command.js +27 -0
- package/dist/services/lifecycle-invocations.js +65 -31
- package/dist/services/recovery-snapshot-database.js +1 -1
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-controller.js +4 -1
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/stage-pause.js +7 -21
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/vnext-code-review.js +13 -11
- package/dist/services/vnext-code.js +39 -21
- package/dist/services/vnext-fanout.js +27 -6
- package/dist/services/vnext-merge.js +13 -10
- package/dist/services/vnext-plan-review.js +6 -5
- package/dist/services/work-registry.js +18 -9
- package/dist/storage/database.js +15 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
|
|
2
3
|
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
@@ -8,7 +9,8 @@ import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckD
|
|
|
8
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
10
|
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
11
|
import { validateSchema } from "./schema-validation.js";
|
|
11
|
-
import {
|
|
12
|
+
import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
|
|
13
|
+
import { stageCycle, stageGraphScope } from "./stage-work-graph.js";
|
|
12
14
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
15
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
16
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
@@ -91,8 +93,8 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
91
93
|
if (prepared.resumed) {
|
|
92
94
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
93
95
|
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
94
|
-
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
95
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command:
|
|
96
|
+
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
97
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
96
98
|
}
|
|
97
99
|
fs.mkdirSync(root, { recursive: true });
|
|
98
100
|
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
@@ -119,7 +121,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
119
121
|
dataSchemaId: "dd-flow/stage-report@1"
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
124
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
123
125
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
124
126
|
return {
|
|
125
127
|
ok: true,
|
|
@@ -133,7 +135,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
133
135
|
graph,
|
|
134
136
|
orchestration,
|
|
135
137
|
readiness,
|
|
136
|
-
next: { refresh_graph_command:
|
|
138
|
+
next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) }
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
export async function finishVnextCode(context, input) {
|
|
@@ -206,14 +208,14 @@ export async function finishVnextCode(context, input) {
|
|
|
206
208
|
});
|
|
207
209
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
208
210
|
if (failed.length) {
|
|
211
|
+
const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
|
|
209
212
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
210
213
|
failures: failed,
|
|
211
214
|
outcome: "repair_required",
|
|
212
215
|
retry_after_workspace_change: true,
|
|
213
216
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
retry_command: finishCommand(context, run.id, projectRoot, input.verificationFile, failed[0].id)
|
|
217
|
+
continuation,
|
|
218
|
+
instruction: "Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage."
|
|
217
219
|
});
|
|
218
220
|
}
|
|
219
221
|
const finalReceipts = receipts;
|
|
@@ -256,8 +258,8 @@ export async function finishVnextCode(context, input) {
|
|
|
256
258
|
wall_clock_ms: timing.wall_clock_ms,
|
|
257
259
|
git: gitFacts(run.workspace_root),
|
|
258
260
|
graph: codeWorkGraph(context, project.id, run.id),
|
|
259
|
-
session_stats_command:
|
|
260
|
-
usage_stats_command:
|
|
261
|
+
session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]),
|
|
262
|
+
usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"])
|
|
261
263
|
},
|
|
262
264
|
artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
|
|
263
265
|
validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
|
|
@@ -307,16 +309,30 @@ export async function finishVnextCode(context, input) {
|
|
|
307
309
|
outcome: "accepted",
|
|
308
310
|
report_path: path.join(root, "stage-report.json"),
|
|
309
311
|
next_action: next,
|
|
310
|
-
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context,
|
|
312
|
+
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"])) } } : next === "start_merge" ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {})
|
|
311
313
|
};
|
|
312
314
|
}
|
|
315
|
+
export function registerAggregateCodeRepair(context, input) {
|
|
316
|
+
const project = requireProjectByRoot(context, input.projectRoot);
|
|
317
|
+
const origins = codeWorks(context, project.id, input.runId).filter(work => !packet(work)?.repair && work.status === "completed");
|
|
318
|
+
const failedDeclarations = new Set(input.failures.flatMap(receipt => [receipt.declaration_id, ...receipt.check_refs]));
|
|
319
|
+
const objective = ["Investigate and repair all retained aggregate check failures while preserving accepted requirements and non-goals. If evidence proves an external blocker or a contradiction, report it; do not invent a product change.", ...input.failures.map(receipt => `Receipt: ${receipt.receipt_path}\nstdout: ${receipt.stdout_path}\nstderr: ${receipt.stderr_path}`)].join("\n\n");
|
|
320
|
+
try {
|
|
321
|
+
return ensureAggregateRepair(context, { projectId: project.id, runId: input.runId, stage: input.stage, failures: input.failures, origins: origins.map(work => work.work_id), ...(input.generation !== undefined ? { generation: input.generation } : {}) }, () => String(addVnextCodeRepair(context, { registrationOwned: true, projectRoot: input.projectRoot, runId: input.runId, stage: input.stage, ...(input.stage === "code-review" ? { reviewCycle: stageCycle(context, project.id, input.runId, input.stage) } : {}), checkReceiptId: input.failures[0].id, originWorkIds: origins.map(work => work.work_id), objective, reviewChecks: input.checks.filter(check => failedDeclarations.has(check.id) || failedDeclarations.has(check.canonical_ref ?? "")) }).repair_work_id));
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (!(error instanceof AppError))
|
|
325
|
+
throw error;
|
|
326
|
+
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, stage: input.stage, run_id: input.runId, failures: input.failures, cause: { code: input.stage === "code" ? "code_gate_failed" : "code_review_gate_failed", message: "Aggregate checks failed; repair registration was rejected" } });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
313
329
|
export function prepareVnextCodeRepair(context, input) {
|
|
314
330
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
315
331
|
const project = requireProjectByRoot(context, projectRoot);
|
|
316
332
|
const run = requireRun(context, project.id, input.runId);
|
|
317
333
|
const home = requireHome(run);
|
|
318
334
|
if (input.reviewCycle) {
|
|
319
|
-
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
|
|
335
|
+
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle && (input.checkReceiptId ? packet(work)?.repair?.check_receipt_id === input.checkReceiptId : Boolean(packet(work)?.repair?.review_findings?.length)));
|
|
320
336
|
if (previous)
|
|
321
337
|
return { kind: "replay", project, run, previous };
|
|
322
338
|
}
|
|
@@ -434,8 +450,11 @@ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRe
|
|
|
434
450
|
if (prepared.kind === "replay")
|
|
435
451
|
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
436
452
|
const { parentWorkId, items, key, receipt } = prepared;
|
|
437
|
-
const
|
|
438
|
-
const id =
|
|
453
|
+
const create = () => String(addWorkBatch(context, { parentWorkId, prepared: items, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"), ...(input.reviewCycle ? { cycle: input.reviewCycle } : {}) }).work_ids[key]);
|
|
454
|
+
const id = input.registrationOwned ? create() : ensureRepairIntent(context, {
|
|
455
|
+
projectId: project.id, runId: run.id, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"),
|
|
456
|
+
causes: input.checkReceiptId ? { receipt: input.checkReceiptId, origins: [...input.originWorkIds].sort() } : input.reviewCycle ? { review_cycle: input.reviewCycle, findings: input.reviewFindings } : { verification: input.verification, unresolved: input.semanticUnresolved, origins: [...input.originWorkIds].sort() }
|
|
457
|
+
}, create).work_id;
|
|
439
458
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
|
|
440
459
|
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
441
460
|
type: "code_repair_created",
|
|
@@ -448,7 +467,7 @@ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRe
|
|
|
448
467
|
}
|
|
449
468
|
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
450
469
|
export function selectRepairChecks(input) {
|
|
451
|
-
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
470
|
+
const candidates = input.failedCheck ? [input.failedCheck, ...(input.reviewChecks ?? [])] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
452
471
|
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
453
472
|
return uniqueBy(checks, (check) => check.id);
|
|
454
473
|
}
|
|
@@ -499,11 +518,11 @@ function coordinatorPrompt(context, input) {
|
|
|
499
518
|
"</code_graph>",
|
|
500
519
|
"",
|
|
501
520
|
"<execution_commands>",
|
|
502
|
-
"Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " +
|
|
503
|
-
"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.
|
|
504
|
-
`A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to
|
|
521
|
+
"Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]),
|
|
522
|
+
"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. A repair_required continuation already registered the repair: end this Turn. Do not invoke that Work's start_command from the coordinator Session, inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
|
|
523
|
+
`A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to ${path.join(requireHome(input.run), "works", input.rootWork.work_id, "block-summary.md")}, then record it with a separate command: ${runtimeCommand(context, ["stage", "block", input.run.id, "--stage", "code", "--work", input.rootWork.work_id, "--kind", "<engine|harness|environment>", "--code", "<stable-code>", "--summary-file", `@run/works/${input.rootWork.work_id}/block-summary.md`, "--retryable", "--project-root", input.projectRoot, "--json"])}. Choose kind and stable-code from evidence; this example is not an executable continuation. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
505
524
|
`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"))}`,
|
|
506
|
-
"
|
|
525
|
+
"A code_gate_failed response with continuation.kind=repair_required has already registered one repair Work with all failed receipts. End this Turn so the controller can dispatch it in a fresh child. Do not recreate the repair, copy origin IDs, write a repair task file or execute the child start command yourself. After repair, update semantic verification and finish again.",
|
|
507
526
|
"</execution_commands>",
|
|
508
527
|
"",
|
|
509
528
|
"<verification_contract>",
|
|
@@ -689,8 +708,7 @@ function requireHome(run) {
|
|
|
689
708
|
return run.run_root;
|
|
690
709
|
}
|
|
691
710
|
export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
|
|
692
|
-
|
|
693
|
-
return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`);
|
|
711
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code", "--verification-file", verificationFile, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
694
712
|
}
|
|
695
713
|
function readVerification(context, input) {
|
|
696
714
|
return prepareJsonSchema(input.file, "CODE verification", {
|
|
@@ -9,6 +9,7 @@ import { listWorks, failWork, cancelWork } from "./work-registry.js";
|
|
|
9
9
|
import { storageSessionId } from "./session-identity.js";
|
|
10
10
|
import { reconcileUnfinishedChecks } from "./code-checks.js";
|
|
11
11
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
12
|
+
import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
|
|
12
13
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
13
14
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
14
15
|
export function getStageFanoutCapacity(context, input) {
|
|
@@ -40,7 +41,7 @@ export function fanoutState(input) {
|
|
|
40
41
|
}
|
|
41
42
|
export function fanoutFile(stageRoot) { return path.join(stageRoot, "fanout.json"); }
|
|
42
43
|
export function writeFanoutDescriptor(stageRoot, descriptor) {
|
|
43
|
-
const value = { schema_id: "dd-flow/work-fanout@1", kind: "work_fanout", ...descriptor };
|
|
44
|
+
const value = { schema_id: descriptor.scope ? "dd-flow/work-fanout@2" : "dd-flow/work-fanout@1", kind: "work_fanout", ...descriptor };
|
|
44
45
|
fs.writeFileSync(fanoutFile(stageRoot), `${JSON.stringify(value, null, 2)}\n`);
|
|
45
46
|
return value;
|
|
46
47
|
}
|
|
@@ -49,7 +50,7 @@ export function readFanoutDescriptor(stageRoot) {
|
|
|
49
50
|
if (!fs.existsSync(file))
|
|
50
51
|
return null;
|
|
51
52
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
52
|
-
if (
|
|
53
|
+
if (!["dd-flow/work-fanout@1", "dd-flow/work-fanout@2"].includes(String(value.schema_id)) || value.kind !== "work_fanout" || typeof value.stage !== "string" || typeof value.parent_work_id !== "string" || !["none", "plan-review"].includes(String(value.dispatch)) || typeof value.capacity_required !== "boolean" || value.schema_id === "dd-flow/work-fanout@2" && (!value.scope || [value.scope.project_id, value.scope.run_id, value.scope.attempt, value.scope.cycle].some(item => typeof item !== "string"))) {
|
|
53
54
|
throw new AppError("runtime_missing", "Stage fan-out descriptor is invalid", 1, { file });
|
|
54
55
|
}
|
|
55
56
|
return value;
|
|
@@ -64,14 +65,30 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
64
65
|
if (!run.run_root)
|
|
65
66
|
throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
|
|
66
67
|
const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
|
|
67
|
-
|
|
68
|
+
let descriptor = readFanoutDescriptor(stageRoot);
|
|
69
|
+
// Registration is authoritative even if projection publication was interrupted,
|
|
70
|
+
// or an otherwise review-off stage acquired an aggregate repair graph.
|
|
71
|
+
if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
|
|
72
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
73
|
+
const parents = context.db.all("SELECT w.work_id, w.parent_work_id FROM repair_intents i JOIN works w ON w.work_id = i.work_id WHERE i.project_id = ? AND i.run_id = ? AND i.stage = ?", [project.id, input.runId, input.stage]).filter(work => members.has(work.work_id));
|
|
74
|
+
const roots = [...new Set(parents.map(work => work.parent_work_id))];
|
|
75
|
+
if (roots.length > 1)
|
|
76
|
+
throw new AppError("stage_graph_conflict", "Registered repairs disagree on their stage coordinator", 1);
|
|
77
|
+
if (roots.length === 1)
|
|
78
|
+
descriptor = { schema_id: "dd-flow/work-fanout@2", scope: stageGraphScope(context, project.id, input.runId, input.stage), kind: "work_fanout", stage: input.stage, parent_work_id: roots[0], dispatch: "none", capacity_required: true };
|
|
79
|
+
}
|
|
68
80
|
if (!descriptor)
|
|
69
81
|
return { ok: true, run_id: input.runId, stage: input.stage, orchestration: null };
|
|
70
82
|
if (descriptor.stage !== input.stage)
|
|
71
83
|
throw new AppError("runtime_missing", "Stage fan-out descriptor belongs to another stage", 1, { requested_stage: input.stage, descriptor_stage: descriptor.stage });
|
|
72
84
|
const capacity = getStageFanoutCapacity(context, { projectRoot, runId: input.runId, stage: input.stage });
|
|
73
|
-
const
|
|
74
|
-
const
|
|
85
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
86
|
+
const currentScope = stageGraphScope(context, project.id, input.runId, input.stage);
|
|
87
|
+
const compatible = descriptor.scope ? Object.entries(currentScope).every(([key, value]) => descriptor.scope[key] === value) : members.size > 0;
|
|
88
|
+
if (!compatible)
|
|
89
|
+
return { ok: true, run_id: input.runId, stage: input.stage, orchestration: { ...descriptor, state: "compatibility_required", compatibility: { code: "stage_graph_compatibility_required", message: "Legacy or stale stage graph has no authoritative current membership; use the pinned engine", expected_scope: currentScope } } };
|
|
90
|
+
const all = listWorks(context, { parentWorkId: descriptor.parent_work_id, includeResults: false, workIds: members });
|
|
91
|
+
const ready = { works: all.works.filter(work => work.ready) };
|
|
75
92
|
const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
|
|
76
93
|
const hasWork = all.works.length > 0;
|
|
77
94
|
const capacityKnown = capacity.available_slots !== null;
|
|
@@ -86,6 +103,7 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
86
103
|
// authoritative prompt. An empty graph is therefore not a success.
|
|
87
104
|
state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
|
|
88
105
|
capacity,
|
|
106
|
+
graph_revision: all.works.map(work => ({ work_id: work.work_id, status: work.status, depends_on: work.depends_on })),
|
|
89
107
|
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
|
|
90
108
|
}
|
|
91
109
|
};
|
|
@@ -99,7 +117,10 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
99
117
|
const status = getVnextFanoutStatus(context, input);
|
|
100
118
|
if (!status.orchestration)
|
|
101
119
|
return { ok: true, issues: [], reconciled: [] };
|
|
120
|
+
if (status.orchestration.state === "compatibility_required")
|
|
121
|
+
throw new AppError("stage_graph_compatibility_required", "Cannot reconcile a legacy or stale graph", 1);
|
|
102
122
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
123
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
103
124
|
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
104
125
|
const issues = [], reconciled = [];
|
|
105
126
|
for (const child of value.children) {
|
|
@@ -107,7 +128,7 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
107
128
|
continue;
|
|
108
129
|
const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
|
|
109
130
|
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.status, w.status AS work_status, w.parent_work_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? AND s.parent_session_id = ? ORDER BY ws.created_at`, [project.id, input.runId, sessionId, parent]);
|
|
110
|
-
const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id);
|
|
131
|
+
const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id && members.has(link.work_id));
|
|
111
132
|
if (!active.length) {
|
|
112
133
|
if (!links.length)
|
|
113
134
|
issues.push({ code: "child_launch_association_missing", session_id: child.session_id, outcome: child.status });
|
|
@@ -11,7 +11,8 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
11
11
|
import { nextMergeRequestId } from "./ids.js";
|
|
12
12
|
import { readProjectConfig } from "./config.js";
|
|
13
13
|
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareFlowRunStageAttachment, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
|
|
14
|
-
import {
|
|
14
|
+
import { stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
15
|
+
import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime-command.js";
|
|
15
16
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
16
17
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
17
18
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
@@ -102,7 +103,7 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
102
103
|
const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') ORDER BY created_at DESC LIMIT 1", [project.id, run.id]);
|
|
103
104
|
if (raced)
|
|
104
105
|
return requestView(context, raced);
|
|
105
|
-
const child = createChildWork(context, { parentWorkId: root.work_id, slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
|
|
106
|
+
const child = createChildWork(context, { parentWorkId: root.work_id, stage: "merge", slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
|
|
106
107
|
const requestId = nextMergeRequestId(context);
|
|
107
108
|
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, replacementOf(run), now, now]);
|
|
108
109
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
|
|
@@ -290,7 +291,7 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
290
291
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
291
292
|
if (failed.length) {
|
|
292
293
|
context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
|
|
293
|
-
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot),
|
|
294
|
+
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } });
|
|
294
295
|
}
|
|
295
296
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
296
297
|
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
@@ -403,7 +404,7 @@ export async function repairVnextMerge(context, input) {
|
|
|
403
404
|
if (request.project_id === project.id && request.status === "superseded" && errorCode(request) === "merge_source_repair") {
|
|
404
405
|
const saved = JSON.parse(request.last_error_json);
|
|
405
406
|
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
406
|
-
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context,
|
|
407
|
+
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
|
|
407
408
|
}
|
|
408
409
|
if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
|
|
409
410
|
throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
|
|
@@ -422,14 +423,14 @@ export async function repairVnextMerge(context, input) {
|
|
|
422
423
|
await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
|
|
423
424
|
assertRunMutationAllowed(context, project.id, request.run_id, generation);
|
|
424
425
|
const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
|
|
425
|
-
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, checkReceiptId: failed[0], originWorkIds, reviewCycle: `MRR-${request.merge_request_id}`, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
426
|
+
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, stage: "code", checkReceiptId: failed[0], originWorkIds, reviewCycle: `MRR-${request.merge_request_id}`, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
426
427
|
const now = context.now();
|
|
427
428
|
context.db.run("UPDATE merge_requests SET status = 'superseded', checkpoint = 'source_repair_required', last_error_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_source_repair", replacement_pending: true, failed_receipt_ids: failed, repair_work_id: repair.repair_work_id }), now, now, request.merge_request_id]);
|
|
428
429
|
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
429
430
|
appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
|
|
430
|
-
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context,
|
|
431
|
+
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
|
|
431
432
|
}
|
|
432
|
-
function mergePrompt(context, input, checks, settings) { const pause = stagePauseCommand(context, { runId: input.run.id, stage: "merge", workId: input.request.executor_work_id, projectRoot: input.projectRoot }); const repair = repairCommand(context, input.request, input.projectRoot); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and
|
|
433
|
+
function mergePrompt(context, input, checks, settings) { const pause = stagePauseCommand(context, { runId: input.run.id, stage: "merge", workId: input.request.executor_work_id, projectRoot: input.projectRoot }); const repair = repairCommand(context, input.request, input.projectRoot); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and write the returned recovery JSON file and use continuation.command_after_input. The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work by writing the question file and then running the separate standalone command below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
|
|
433
434
|
function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
|
|
434
435
|
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
435
436
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
@@ -588,6 +589,8 @@ function findRootWork(context, projectId, runId) { const work = context.db.get("
|
|
|
588
589
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
|
|
589
590
|
function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
|
|
590
591
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
591
|
-
function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context,
|
|
592
|
-
function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context,
|
|
593
|
-
export function finishCommand(context, runId, request, projectRoot, retryCheckId) {
|
|
592
|
+
function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context, runtimeCommand(context, ["merge", "apply", request.merge_request_id, "--work", request.executor_work_id, "--project-root", projectRoot, "--json", "--progress-jsonl"])); }
|
|
593
|
+
function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context, runtimeCommand(context, ["merge", "repair", request.merge_request_id, "--project-root", projectRoot, "--json"])); }
|
|
594
|
+
export function finishCommand(context, runId, request, projectRoot, retryCheckId) {
|
|
595
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "merge", "--request", request.merge_request_id, "--work", request.executor_work_id, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
596
|
+
}
|
|
@@ -20,6 +20,7 @@ import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
|
20
20
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
22
|
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
23
24
|
const stage = "plan-review";
|
|
24
25
|
const stageDir = vnextStageDirectory(stage);
|
|
25
26
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -115,7 +116,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
115
116
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
116
117
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
117
118
|
return withStageSettlement(context, () => {
|
|
118
|
-
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems });
|
|
119
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
|
|
119
120
|
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
120
121
|
writeReport(root, report);
|
|
121
122
|
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" });
|
|
@@ -144,7 +145,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
144
145
|
markReviewPending(home, groups);
|
|
145
146
|
const workSessionId = String(binding.work_session_id);
|
|
146
147
|
const sessionId = String(binding.session_id);
|
|
147
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
|
|
148
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
148
149
|
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
149
150
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
150
151
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
@@ -176,7 +177,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
176
177
|
const file = path.join(root, ".dispatch.json");
|
|
177
178
|
writeJson(file, { works: pending.map((group) => ({ key: group.key, task: group.task, depends_on: group.depends_on, launch_policy: "fresh_agent_required", result_schema: "dd-flow/plan-review-result@1", payload: { kind: "plan-review", read_only: true, group: { key: group.key, aspect_ids: group.aspect_ids } } })) });
|
|
178
179
|
try {
|
|
179
|
-
addWorkBatch(context, { parentWorkId: parent.work_id, file });
|
|
180
|
+
addWorkBatch(context, { parentWorkId: parent.work_id, file, stage });
|
|
180
181
|
}
|
|
181
182
|
finally {
|
|
182
183
|
fs.rmSync(file, { force: true });
|
|
@@ -306,7 +307,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
306
307
|
function preparedExisting(context, input) {
|
|
307
308
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
308
309
|
const binding = bindStageCoordinatorWork(context, { workId: input.workId, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(input.root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
309
|
-
const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true });
|
|
310
|
+
const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, input.run.project_id, input.run.id, stage) });
|
|
310
311
|
return { ok: true, resumed: true, run_id: input.run.id, stage, outcome: "review_required", work_id: input.workId, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), orchestration, plan_review: { requested_mode: input.requested, effective_mode: input.effective, policy_source: policySource(input.run), groups: input.groups }, next: { dispatch_command: dispatchCommand(context, input.run.id, input.projectRoot), finish_command: finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "decision.json")) } };
|
|
311
312
|
}
|
|
312
313
|
function orchestratorPrompt(context, input) {
|
|
@@ -432,7 +433,7 @@ catch {
|
|
|
432
433
|
} }
|
|
433
434
|
function rootWorkId(context, projectId, runId) { const root = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!root)
|
|
434
435
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
435
|
-
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
|
|
436
|
+
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch, stage: "code" }); }
|
|
436
437
|
function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
|
|
437
438
|
function preserveDecisionReceipt(root, decision, preparedText) {
|
|
438
439
|
const receipt = path.join(root, "decision.receipt.json");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { bindStageWork, currentStageWorkIds, stageWorkIds } from "./stage-work-graph.js";
|
|
3
|
+
import { runtimeCommand } from "./runtime-command.js";
|
|
2
4
|
import crypto from "node:crypto";
|
|
3
5
|
import fs from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
|
-
import { quote } from "shell-quote";
|
|
6
7
|
import { AppError } from "../shared/errors.js";
|
|
7
8
|
import { entityReferenceVariants, shortEntityReference } from "../shared/entity-references.js";
|
|
8
9
|
import { resolveWorkReference } from "../storage/work-references.js";
|
|
@@ -104,6 +105,8 @@ export function createChildWork(context, input) {
|
|
|
104
105
|
const id = nextWorkId(context, parent.project_id, input.slug);
|
|
105
106
|
const now = context.now();
|
|
106
107
|
context.db.run(`INSERT INTO works (${workColumns}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '[]', 'created', NULL, ?, NULL, ?, NULL)`, [id, parent.project_id, parent.run_id, parent.work_id, input.task, input.launchPolicy, input.resultSchema ?? null, input.payload ? JSON.stringify(input.payload) : null, now, now]);
|
|
108
|
+
if (input.stage)
|
|
109
|
+
bindStageWork(context, { projectId: parent.project_id, runId: parent.run_id, workId: id, stage: input.stage });
|
|
107
110
|
refreshRunWorkProjection(context, parent.project_id, parent.run_id);
|
|
108
111
|
return requireWork(context, id);
|
|
109
112
|
});
|
|
@@ -181,8 +184,14 @@ export function addWorkBatch(context, input) {
|
|
|
181
184
|
for (const item of proposed)
|
|
182
185
|
context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, started_at, created_at, updated_at, completed_at)
|
|
183
186
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NULL, NULL, ?, ?, NULL)`, [item.id, parent.project_id, parent.run_id, item.parentId, item.task, item.launch_policy ?? "reuse_allowed", item.result_schema ?? null, item.payload ? JSON.stringify(item.payload) : null, JSON.stringify(item.dependencies), now, now]);
|
|
184
|
-
|
|
187
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [parent.project_id, parent.run_id]);
|
|
188
|
+
const stages = run ? (JSON.parse(run.index_json).stage_runs ?? []) : [];
|
|
189
|
+
const ownerStage = input.stage ?? [...stages].reverse().find(stage => stage.status === "running")?.stage;
|
|
190
|
+
for (const item of proposed) {
|
|
191
|
+
if (ownerStage)
|
|
192
|
+
bindStageWork(context, { projectId: parent.project_id, runId: parent.run_id, workId: item.id, stage: ownerStage, ...(input.cycle ? { cycle: input.cycle } : {}) });
|
|
185
193
|
appendFlowRunTimelineEvent(context, parent.project_id, parent.run_id, { type: "work_materialized", work_id: item.id, parent_work_id: item.parentId, depends_on: item.dependencies, launch_policy: item.launch_policy ?? "reuse_allowed" });
|
|
194
|
+
}
|
|
186
195
|
refreshRunWorkProjection(context, parent.project_id, parent.run_id);
|
|
187
196
|
return { ok: true, work_ids: Object.fromEntries(ids) };
|
|
188
197
|
});
|
|
@@ -205,8 +214,8 @@ export function listWorks(context, input) {
|
|
|
205
214
|
where.push("status = ?");
|
|
206
215
|
params.push(input.status);
|
|
207
216
|
}
|
|
208
|
-
const rows = context.db.all(`SELECT ${workColumns} FROM works WHERE ${where.join(" AND ")} ORDER BY created_at, work_id`, params);
|
|
209
|
-
const ready = (work) => isReady(context, work);
|
|
217
|
+
const rows = context.db.all(`SELECT ${workColumns} FROM works WHERE ${where.join(" AND ")} ORDER BY created_at, work_id`, params).filter(work => !input.workIds || input.workIds.has(work.work_id));
|
|
218
|
+
const ready = (work) => (input.workIds || currentStageWorkIds(context, work.project_id, work.run_id))?.has(work.work_id) !== false && isReady(context, work);
|
|
210
219
|
const filtered = input.ready ? rows.filter(ready) : rows;
|
|
211
220
|
return { works: filtered.slice(0, input.limit ?? filtered.length).map((work) => {
|
|
212
221
|
const isReadyNow = ready(work);
|
|
@@ -382,7 +391,7 @@ export function shortWorkId(id) { return shortEntityReference(id); }
|
|
|
382
391
|
* PATH is not inherited; using the shared lifecycle command keeps them on the
|
|
383
392
|
* same captured runtime as their parent stage.
|
|
384
393
|
*/
|
|
385
|
-
export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); const recovery = recoveryGuard(context, work.project_id, work.run_id); return managedLifecycleCommand(context,
|
|
394
|
+
export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); const recovery = recoveryGuard(context, work.project_id, work.run_id); return managedLifecycleCommand(context, runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"])); }
|
|
386
395
|
export function assertWorkLaunchReady(context, id) {
|
|
387
396
|
ensureWorkRegistry(context);
|
|
388
397
|
const work = requireWork(context, id);
|
|
@@ -391,7 +400,7 @@ export function assertWorkLaunchReady(context, id) {
|
|
|
391
400
|
throw new AppError("invalid_work_state", "Work is not created", 2, { work_id: id, status: work.status });
|
|
392
401
|
if (!isReady(context, work)) {
|
|
393
402
|
const blockers = readinessBlockers(context, work);
|
|
394
|
-
const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
|
|
403
|
+
const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => currentStageWorkIds(context, candidate.project_id, candidate.run_id)?.has(candidate.work_id) !== false && isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
|
|
395
404
|
throw new AppError("work_not_ready", "Work cannot start until dependencies complete and overlapping planned coordination areas are free", 2, { work_id: id, blockers, ready });
|
|
396
405
|
}
|
|
397
406
|
return work;
|
|
@@ -976,8 +985,8 @@ function renderWorkerPrompt(context, work, run, dependencies) {
|
|
|
976
985
|
const command = flowCommand(context);
|
|
977
986
|
const packet = codePacket(work);
|
|
978
987
|
const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
|
|
979
|
-
const finishWorkCommand = managedLifecycleCommand(context,
|
|
980
|
-
const failWorkCommand = managedLifecycleCommand(context,
|
|
988
|
+
const finishWorkCommand = managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"]));
|
|
989
|
+
const failWorkCommand = managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]));
|
|
981
990
|
const mergeWork = parsePayload(work)?.kind === "merge";
|
|
982
991
|
const writeBoundary = mergeWork
|
|
983
992
|
? ["MERGE does not own product-code repair. Use only the stage packet's merge apply command; resolve only paths that Git reports as unmerged. Do not edit integration code, tests, documentation or configuration to make a gate pass.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result."]
|
|
@@ -1163,7 +1172,7 @@ export function refreshRunWorkProjection(context, projectId, runId) { refreshRun
|
|
|
1163
1172
|
if (fs.existsSync(obsolete))
|
|
1164
1173
|
fs.rmSync(obsolete);
|
|
1165
1174
|
} }
|
|
1166
|
-
export function codeWorkGraph(context, projectId, runId) { const works = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id`, [projectId, runId]).filter((work) => Boolean(codePacket(work))); const ready = works.filter((work) => isReady(context, work)); return { total: works.length, created: works.filter((work) => work.status === "created").length, running: works.filter((work) => work.status === "running").length, completed: works.filter((work) => work.status === "completed").length, failed: works.filter((work) => work.status === "failed").length, ready: ready.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: workStartCommand(context, work) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
|
|
1175
|
+
export function codeWorkGraph(context, projectId, runId) { const index = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId])?.index_json ?? "{}"); const active = index.stage_runs?.some(item => item.stage === "code-review" && item.status === "running") ? "code-review" : "code"; const members = index.stage_runs?.length ? stageWorkIds(context, projectId, runId, active) : null; const works = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id`, [projectId, runId]).filter((work) => Boolean(codePacket(work)) && (!members || members.has(work.work_id))); const ready = works.filter((work) => isReady(context, work)); return { total: works.length, created: works.filter((work) => work.status === "created").length, running: works.filter((work) => work.status === "running").length, completed: works.filter((work) => work.status === "completed").length, failed: works.filter((work) => work.status === "failed").length, ready: ready.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: workStartCommand(context, work) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
|
|
1167
1176
|
function parsePayload(work) { if (!work.payload_json)
|
|
1168
1177
|
return null; try {
|
|
1169
1178
|
const value = JSON.parse(work.payload_json);
|
package/dist/storage/database.js
CHANGED
|
@@ -5,6 +5,18 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { ensureDir } from "./paths.js";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter } from "./writer-contract.js";
|
|
8
|
+
const workContinuationStorageSql = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS stage_work_memberships (
|
|
10
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL, work_id TEXT NOT NULL REFERENCES works(work_id),
|
|
11
|
+
stage TEXT NOT NULL, attempt TEXT NOT NULL, cycle TEXT NOT NULL DEFAULT '',
|
|
12
|
+
PRIMARY KEY(project_id, run_id, work_id, stage, attempt, cycle)
|
|
13
|
+
);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS repair_intents (
|
|
15
|
+
identity TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
16
|
+
stage TEXT NOT NULL, attempt TEXT NOT NULL, payload_hash TEXT NOT NULL,
|
|
17
|
+
payload_json TEXT NOT NULL, work_id TEXT NOT NULL REFERENCES works(work_id), created_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
`;
|
|
8
20
|
const require = createRequire(import.meta.url);
|
|
9
21
|
const { DatabaseSync } = require("node:sqlite");
|
|
10
22
|
const resourceDatabases = new Map();
|
|
@@ -210,6 +222,7 @@ export function ensureVnextWorkStorage(db) {
|
|
|
210
222
|
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
211
223
|
);
|
|
212
224
|
CREATE INDEX IF NOT EXISTS idx_check_receipts_run ON check_receipts(project_id, run_id, started_at);
|
|
225
|
+
${workContinuationStorageSql}
|
|
213
226
|
`);
|
|
214
227
|
// A committed Work start is authoritative even if publishing its prompt
|
|
215
228
|
// files is interrupted after commit. Keep the exact bytes for repair.
|
|
@@ -949,6 +962,8 @@ function migrate(db) {
|
|
|
949
962
|
CREATE INDEX IF NOT EXISTS idx_check_receipts_run
|
|
950
963
|
ON check_receipts(project_id, run_id, started_at);
|
|
951
964
|
|
|
965
|
+
${workContinuationStorageSql}
|
|
966
|
+
|
|
952
967
|
CREATE TABLE IF NOT EXISTS merge_requests (
|
|
953
968
|
merge_request_id TEXT PRIMARY KEY,
|
|
954
969
|
project_id TEXT NOT NULL,
|