@deksden-com/dd-flow-cli 0.9.0-beta.103 → 0.9.0-beta.105

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/run-cli.js +32 -11
  4. package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
  6. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
  7. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
  8. package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
  9. package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
  10. package/dist/protocol/local-files.js +18 -9
  11. package/dist/runtime/context.js +2 -2
  12. package/dist/services/cleanup.js +81 -32
  13. package/dist/services/code-checks.js +18 -19
  14. package/dist/services/continuation-outcome.js +1 -1
  15. package/dist/services/controller-fanout.js +3 -3
  16. package/dist/services/eval-snapshots.js +69 -41
  17. package/dist/services/execution-policy.js +12 -1
  18. package/dist/services/hooks.js +150 -42
  19. package/dist/services/lanes.js +23 -18
  20. package/dist/services/lifecycle-command.js +11 -2
  21. package/dist/services/lifecycle-invocations.js +291 -54
  22. package/dist/services/managed-processes.js +35 -23
  23. package/dist/services/merge-queue.js +71 -67
  24. package/dist/services/merge-server.js +3 -11
  25. package/dist/services/migrations.js +28 -12
  26. package/dist/services/plan-runtime.js +8 -4
  27. package/dist/services/projects.js +3 -11
  28. package/dist/services/protocols.js +98 -68
  29. package/dist/services/recovery-snapshot-database.js +43 -8
  30. package/dist/services/run-control-worker.js +15 -32
  31. package/dist/services/run-control.js +3 -17
  32. package/dist/services/run-controller-capture.js +58 -27
  33. package/dist/services/run-controller-process.js +21 -30
  34. package/dist/services/run-controller-recovery.js +36 -37
  35. package/dist/services/run-controller.js +81 -86
  36. package/dist/services/run-fork.js +10 -6
  37. package/dist/services/run-projection.js +19 -64
  38. package/dist/services/run-recovery-runtime.js +24 -14
  39. package/dist/services/run-recovery.js +56 -54
  40. package/dist/services/runs.js +218 -160
  41. package/dist/services/runtime-budget.js +19 -45
  42. package/dist/services/runtime-scope-capture.js +8 -6
  43. package/dist/services/runtime-scope-control.js +49 -39
  44. package/dist/services/runtime-scope-resume.js +134 -122
  45. package/dist/services/runtime-scope-worker.js +48 -49
  46. package/dist/services/scope-writer-cleanup.js +26 -0
  47. package/dist/services/stage-context.js +6 -1
  48. package/dist/services/stage-lifecycle.js +1 -1
  49. package/dist/services/stage-pause.js +13 -29
  50. package/dist/services/stage-report-renderer.js +116 -6
  51. package/dist/services/usage.js +2 -9
  52. package/dist/services/vnext-code-review.js +10 -7
  53. package/dist/services/vnext-code.js +9 -17
  54. package/dist/services/vnext-merge.js +64 -21
  55. package/dist/services/vnext-plan-review.js +43 -30
  56. package/dist/services/vnext-plan.js +28 -24
  57. package/dist/services/vnext-protocolize.js +183 -20
  58. package/dist/services/vnext-specify.js +27 -19
  59. package/dist/services/work-registry.js +229 -150
  60. package/dist/storage/database.js +286 -56
  61. package/dist/storage/foreign-key-transaction.js +39 -0
  62. package/dist/storage/immutable-input.js +45 -0
  63. package/dist/storage/import-guard.js +102 -0
  64. package/dist/storage/native-cleanup.js +52 -0
  65. package/dist/storage/projection-lock.js +73 -0
  66. package/dist/storage/writer-contract.js +3 -3
  67. package/dist/storage/writer-migration.js +24 -41
  68. package/package.json +1 -1
  69. package/tools/repair-paused-run-status.mjs +48 -23
@@ -1,6 +1,6 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
2
  import crypto from "node:crypto";
3
- import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
3
+ import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { AppError } from "../shared/errors.js";
@@ -15,12 +15,13 @@ import { prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-p
15
15
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
16
16
  import { nextWorkId } from "./ids.js";
17
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
18
- import { writeStageReport } from "./stage-report-renderer.js";
18
+ import { acceptStagePacket, acceptStageReport, prepareStagePacket, prepareStageReport } from "./stage-report-renderer.js";
19
19
  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
23
  import { stageGraphScope } from "./stage-work-graph.js";
24
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
24
25
  const stage = "plan-review";
25
26
  const stageDir = vnextStageDirectory(stage);
26
27
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
@@ -115,20 +116,24 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
115
116
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
116
117
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
117
118
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
119
+ const reportGit = gitFacts(run.workspace_root);
120
+ const nextCommand = codeCommand(context, run.id, projectRoot);
118
121
  return withStageSettlement(context, () => {
119
122
  const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
120
- const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
121
- writeReport(root, report);
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" });
123
+ const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
124
+ const preparedReport = prepareStageReport(report);
125
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
126
+ const completionInput = { 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", dataSha256: preparedReport.sha256, git: reportGit };
127
+ completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
123
128
  advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
124
129
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
125
- return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
130
+ return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: nextCommand } };
126
131
  });
127
132
  }
128
133
  const workId = nextWorkId(context, project.id, "plan-review");
129
134
  context.db.run("INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)", [workId, project.id, run.id, rootWork, reviewTask, now, now, now]);
130
135
  const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
131
- const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity });
136
+ const prompt = `${orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity })}\n${stageLifecycleInstruction}\n`;
132
137
  const promptPath = path.join(root, "stage-prompt.md");
133
138
  fs.writeFileSync(promptPath, prompt);
134
139
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
@@ -215,7 +220,7 @@ export function recordVnextPlanReviewCapacity(context, input) {
215
220
  return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
216
221
  }
217
222
  export function finishVnextPlanReview(context, input) {
218
- return withStageSettlement(context, () => finishPlanReviewOwned(context, input));
223
+ return finishPlanReviewOwned(context, input);
219
224
  }
220
225
  function finishPlanReviewOwned(context, input) {
221
226
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -245,7 +250,15 @@ function finishPlanReviewOwned(context, input) {
245
250
  if (children.some((child) => !["completed", "failed", "cancelled"].includes(child.status)))
246
251
  throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
247
252
  if (["failed", "cancelled"].includes(decision.outcome)) {
248
- return withStageSettlement(context, () => { preserveDecisionReceipt(root, decision, input.decisionText); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, contextFile, decision }); });
253
+ const decisionReceipt = prepareDecisionReceipt(root, decision, input.decisionText);
254
+ const now = context.now();
255
+ const reportGit = gitFacts(run.workspace_root);
256
+ const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: decision.outcome, groups: contextFile.groups ?? [], batchChecksum: contextFile.system?.batch_checksum ?? checksum(batch), planChecksum: contextFile.system?.plan_checksum ?? planSetChecksum(home, run.workspace_root), code: {}, now, projectRoot, decision, children: latestReviewChildren(children), flow: flowCommand(context), git: reportGit });
257
+ const preparedReport = prepareStageReport(report);
258
+ const stageStatus = decision.outcome === "cancelled" ? "skipped" : "failed";
259
+ const completionInput = { projectRoot, runId: run.id, stage, status: stageStatus, data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
260
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
261
+ return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, parentWorkId: parent.work_id, decision, now, decisionReceipt, preparedReport, completionInput, preparedCompletion }));
249
262
  }
250
263
  const latestChildren = latestReviewChildren(children);
251
264
  const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision, workspaceRoot: run.workspace_root, runHome: home, runId: run.id });
@@ -288,20 +301,27 @@ function finishPlanReviewOwned(context, input) {
288
301
  }
289
302
  const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
290
303
  const finalBatchChecksum = prepared.batchChecksum;
304
+ prepared.publish();
305
+ const decisionReceipt = prepareDecisionReceipt(root, decision, input.decisionText);
306
+ const codeParentId = rootWorkId(context, project.id, run.id);
307
+ const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
308
+ const reportGit = gitFacts(run.workspace_root);
309
+ const nextCommand = codeCommand(context, run.id, projectRoot);
291
310
  return withStageSettlement(context, () => {
292
- prepared.publish();
293
- preserveDecisionReceipt(root, decision, input.decisionText);
294
- const registered = registerCode(context, project.id, run.id, batch);
311
+ const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
295
312
  const now = context.now();
296
313
  settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
297
- const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context) });
314
+ const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context), git: reportGit, nextCommand });
298
315
  applyReviewResults(home, latestChildren, decision);
299
- writeReport(root, report);
316
+ const preparedReport = prepareStageReport(report);
317
+ acceptStagePacket(context, { projectId: project.id, runId: run.id, stage, prepared: decisionReceipt });
318
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
319
+ const completionInput = { 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", dataSha256: preparedReport.sha256, git: reportGit };
320
+ completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
300
321
  refreshRunWorkProjection(context, project.id, run.id);
301
- 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" });
302
322
  advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
303
323
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
304
- return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
324
+ return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: nextCommand } };
305
325
  });
306
326
  }
307
327
  function preparedExisting(context, input) {
@@ -433,9 +453,8 @@ catch {
433
453
  } }
434
454
  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)
435
455
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
436
- function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch, stage: "code" }); }
437
456
  function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
438
- function preserveDecisionReceipt(root, decision, preparedText) {
457
+ function prepareDecisionReceipt(root, decision, preparedText) {
439
458
  const receipt = path.join(root, "decision.receipt.json");
440
459
  // The CLI has already parsed and validated this decision during prepare.
441
460
  // Persist that immutable value rather than re-opening a caller-controlled
@@ -444,9 +463,8 @@ function preserveDecisionReceipt(root, decision, preparedText) {
444
463
  if (fs.existsSync(receipt)) {
445
464
  if (!bytes.equals(fs.readFileSync(receipt)))
446
465
  throw new AppError("decision_receipt_conflict", "The immutable PLAN-REVIEW decision receipt already contains a different payload", 2, { receipt });
447
- return;
448
466
  }
449
- fs.writeFileSync(receipt, bytes);
467
+ return prepareStagePacket(receipt, bytes);
450
468
  }
451
469
  function reviewerResult(result) { try {
452
470
  const value = JSON.parse(result ?? "");
@@ -506,18 +524,16 @@ function settleReviewParent(context, input) {
506
524
  context.db.run("UPDATE work_sessions SET status = ?, result_path = COALESCE(?, result_path), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, input.resultPath ?? null, now, now, workSession.id]);
507
525
  }
508
526
  function finishTerminalReview(context, input) {
509
- const now = context.now();
527
+ const now = input.now;
510
528
  const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
511
529
  settleReviewParent(context, { projectRoot: input.projectRoot, projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, result: input.decision.summary, reason: `plan_review_${input.decision.outcome}`, resultPath: path.join(input.root, "stage-report.json") });
512
530
  const rootWork = rootWorkId(context, input.projectId, input.run.id);
513
531
  context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [childStatus, input.decision.summary, now, now, rootWork]);
514
532
  closeRunningWorkSession(context, rootWork, childStatus, now);
515
- const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [input.projectId, input.run.id, input.parentWorkId]);
516
- const report = reportFor(context, { run: input.run, mode: input.contextFile.system?.effective_mode ?? "standard", requested: input.contextFile.system?.requested_mode ?? "auto", outcome: input.decision.outcome, groups: input.contextFile.groups ?? [], batchChecksum: input.contextFile.system?.batch_checksum ?? checksum(input.batch), planChecksum: input.contextFile.system?.plan_checksum ?? planSetChecksum(requireHome(input.run), input.run.workspace_root), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
517
- writeReport(input.root, report);
533
+ acceptStagePacket(context, { projectId: input.projectId, runId: input.run.id, stage, prepared: input.decisionReceipt });
534
+ acceptStageReport(context, { projectId: input.projectId, runId: input.run.id, stage, root: input.root, prepared: input.preparedReport });
535
+ completeFlowRunStage(context, input.completionInput, input.preparedCompletion);
518
536
  refreshRunWorkProjection(context, input.projectId, input.run.id);
519
- const stageStatus = input.decision.outcome === "cancelled" ? "skipped" : input.decision.outcome === "failed" ? "failed" : "blocked";
520
- completeFlowRunStage(context, { projectRoot: input.projectRoot, runId: input.run.id, stage, status: stageStatus, data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
521
537
  completeFlowRun(context, { projectRoot: input.projectRoot, runId: input.run.id, status: input.decision.outcome === "cancelled" ? "cancelled" : input.decision.outcome === "failed" ? "failed" : "blocked", verdict: `plan_review_${input.decision.outcome}`, nextAction: "resolve_plan_review_blocker" });
522
538
  appendFlowRunTimelineEvent(context, input.projectId, input.run.id, { type: "plan_review_terminal", outcome: input.decision.outcome, work_id: input.parentWorkId });
523
539
  return { ok: true, run_id: input.run.id, stage, outcome: input.decision.outcome, next_action: "resolve_plan_review_blocker" };
@@ -534,12 +550,9 @@ function reportFor(context, input) {
534
550
  const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
535
551
  const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
536
552
  const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
537
- return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
553
+ return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: input.git ?? gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: input.nextCommand ?? managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
538
554
  }
539
555
  function stageTiming(run, now) { const started = readIndex(run).stage_runs?.find((item) => item.stage === stage && typeof item.started_at === "string"); const startedAt = started?.started_at ?? now; const milliseconds = Date.parse(now) - Date.parse(startedAt); return { started_at: startedAt, wall_clock_ms: Number.isFinite(milliseconds) && milliseconds >= 0 ? milliseconds : null }; }
540
- function writeReport(root, report) {
541
- writeStageReport(root, report);
542
- }
543
556
  function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
544
557
  function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
545
558
  function finishCommand(context, runId, projectRoot, decision) { return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json`); }
@@ -1,7 +1,7 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
2
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
3
3
  import crypto from "node:crypto";
4
- import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
4
+ import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { AppError } from "../shared/errors.js";
@@ -17,9 +17,10 @@ import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
17
17
  import { readVnextSpecifyResult } from "./vnext-specify.js";
18
18
  import { nextWorkId } from "./ids.js";
19
19
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
20
- import { writeStageReport } from "./stage-report-renderer.js";
20
+ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
21
21
  import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
22
  import { subagentCapacityKey } from "./vnext-fanout.js";
23
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
23
24
  export function isVnextPlanRun(context, input) {
24
25
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
25
26
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
@@ -70,19 +71,13 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
70
71
  if (!context.db.get("SELECT 1 FROM work_sessions WHERE work_id = ? LIMIT 1", [rootWork.work_id])) {
71
72
  bindRunningWorkSession(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, promptPath: path.join(root, "fixture-root.prompt.md") });
72
73
  }
73
- context.db.beginWriteTransaction();
74
- let planWorkId;
75
- try {
74
+ const planWorkId = context.db.writeTransaction(() => {
76
75
  if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
77
76
  throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
78
- planWorkId = nextWorkId(context, project.id, "plan");
77
+ const planWorkId = nextWorkId(context, project.id, "plan");
79
78
  context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)`, [planWorkId, project.id, run.id, rootWork.work_id, planTask, now, now, now]);
80
- context.db.exec("COMMIT");
81
- }
82
- catch (error) {
83
- context.db.exec("ROLLBACK");
84
- throw error;
85
- }
79
+ return planWorkId;
80
+ });
86
81
  // A Desktop task may start above the materialized repository. Lifecycle
87
82
  // prompts therefore hand agents write targets as absolute paths: relative
88
83
  // `.memory-bank/...` paths would otherwise silently land in the parent cwd.
@@ -112,7 +107,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
112
107
  // This explicit final rule supersedes historical pack wording: a flow gate
113
108
  // has only executable autonomous evidence. Human or external confirmation
114
109
  // is neither a check nor a permitted completion condition.
115
- fs.writeFileSync(promptPath, `${prompt}\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n`);
110
+ fs.writeFileSync(promptPath, `${prompt}\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n${stageLifecycleInstruction}\n`);
116
111
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
117
112
  fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
118
113
  const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
@@ -135,7 +130,7 @@ export function prepareVnextPlanFinish(context, input) {
135
130
  return prepared;
136
131
  }
137
132
  export function finishVnextPlan(context, input) {
138
- return withStageSettlement(context, () => finishVnextPlanOwned(context, input));
133
+ return finishVnextPlanOwned(context, input);
139
134
  }
140
135
  function finishVnextPlanOwned(context, input) {
141
136
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -163,21 +158,30 @@ function finishVnextPlanOwned(context, input) {
163
158
  prepared.publish();
164
159
  const now = context.now();
165
160
  const batchChecksum = checksum(batch);
166
- context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
167
- context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
168
- refreshRunWorkProjection(context, project.id, run.id);
169
161
  const reviewCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`);
170
- const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage: "plan", generated_at: now, verdict: "done", semantic: { result: `Accepted ${protocols.length} executable PLAN artifact${protocols.length === 1 ? "" : "s"}.`, acceptance: protocols, changed_files: [...planFiles.map((file) => path.relative(projectRoot, file)), ...mapFiles.map((file) => runRef(run.id, home, file)), runRef(run.id, home, batch)], checks: ["protocol-plan schema", "aspect-map schema", "cross-artifact references", "generated CODE batch"], evidence: [runRef(run.id, home, path.join(root, "stage-report.json"))], next_action: "start_plan_review", plans: planFiles.map((file) => path.relative(projectRoot, file)), aspect_maps: mapFiles.map((file) => runRef(run.id, home, file)), code_work_batch: runRef(run.id, home, batch), batch_checksum: batchChecksum }, mechanical: { started_at: stageStartedAt(home, now), finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(stageStartedAt(home, now))), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, next_command: reviewCommand }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
162
+ const startedAt = stageStartedAt(home, now);
163
+ const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage: "plan", generated_at: now, verdict: "done", semantic: { result: `Accepted ${protocols.length} executable PLAN artifact${protocols.length === 1 ? "" : "s"}.`, acceptance: protocols, changed_files: [...planFiles.map((file) => path.relative(projectRoot, file)), ...mapFiles.map((file) => runRef(run.id, home, file)), runRef(run.id, home, batch)], checks: ["protocol-plan schema", "aspect-map schema", "cross-artifact references", "generated CODE batch"], evidence: [runRef(run.id, home, path.join(root, "stage-report.json"))], next_action: "start_plan_review", plans: planFiles.map((file) => path.relative(projectRoot, file)), aspect_maps: mapFiles.map((file) => runRef(run.id, home, file)), code_work_batch: runRef(run.id, home, batch), batch_checksum: batchChecksum }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, next_command: reviewCommand }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
171
164
  Object.assign(report.semantic, {
172
165
  plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
173
166
  aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
174
167
  });
175
- const reportJson = writeStageReport(root, report).json;
176
- validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
177
- completeFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
178
- advanceFlowRun(context, { settlementStage: "plan", projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
179
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
180
- return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", next_command: reviewCommand, next: { kind: "start_stage", stage: "plan-review", command: reviewCommand } };
168
+ const preparedReport = prepareStageReport(report);
169
+ validateSchema({ schemaName: "stage-report", file: path.join(root, "stage-report.json"), data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
170
+ const completionInput = { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
171
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
172
+ return withStageSettlement(context, () => {
173
+ const current = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id]);
174
+ if (current?.status !== "running")
175
+ throw new AppError("invalid_work_state", "PLAN Work changed during finish preparation", 1);
176
+ context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
177
+ context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
178
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage: "plan", root, prepared: preparedReport });
179
+ completeFlowRunStage(context, completionInput, preparedCompletion);
180
+ refreshRunWorkProjection(context, project.id, run.id);
181
+ advanceFlowRun(context, { settlementStage: "plan", projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
182
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
183
+ return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", next_command: reviewCommand, next: { kind: "start_stage", stage: "plan-review", command: reviewCommand } };
184
+ });
181
185
  }
182
186
  export function validateVnextPlanArtifacts(context, input) {
183
187
  // Validation never repairs or republishes accepted artifacts. PLAN and