@deksden-com/dd-flow-cli 0.9.0-beta.124 → 0.9.0-beta.127

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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -11,13 +11,13 @@ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, ensureWorkReg
11
11
  import { validateSchema } from "./schema-validation.js";
12
12
  import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
13
13
  import { assertStageStartHookEvent } from "./hooks.js";
14
- import { prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-plan.js";
14
+ import { planCheckInstruction, prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-plan.js";
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
18
  import { acceptStagePacket, acceptStageReport, prepareStagePacket, prepareStageReport } from "./stage-report-renderer.js";
19
19
  import { assertPortableArtifactRef } from "./portable-refs.js";
20
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
20
+ import { currentStagePrompt, 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";
@@ -25,6 +25,7 @@ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-ins
25
25
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
26
26
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
27
27
  import { reviewDefEvidence } from "./review-deferral.js";
28
+ import { stageSuccessor } from "./vnext-contracts.js";
28
29
  const stage = "plan-review";
29
30
  const stageDir = vnextStageDirectory(stage);
30
31
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
@@ -108,13 +109,13 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
108
109
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
109
110
  if (prepared.state === "terminal") {
110
111
  const { report, outcome } = prepared;
111
- return { ok: true, run_id: run.id, stage, outcome: outcome === "off" ? "review_off" : "accepted", ...(report.semantic?.code ? { code: report.semantic.code } : {}), next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) }, idempotent: true };
112
+ return { ok: true, run_id: run.id, stage, outcome: outcome === "off" ? "review_off" : "accepted", ...(report.semantic?.code ? { code: report.semantic.code } : {}), next: codeSuccessor(context, run.id, projectRoot), idempotent: true };
112
113
  }
113
114
  const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
114
115
  if (prepared.state === "resumed") {
115
116
  resumeVerificationMatrix(context, project.id, run.id, stage);
116
117
  const promptPath = path.join(root, "stage-prompt.md");
117
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
118
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
118
119
  return { ...preparedExisting(context, { projectRoot, projectId: project.id, run, root, workId: prepared.workId, hookEventId: input.hookEventId, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}), requested, effective, groups, batchChecksum, planChecksum, orchestration: prepared.orchestration }), ...(externalContext ? { external_context: externalContext } : {}) };
119
120
  }
120
121
  ensureWorkRegistry(context);
@@ -124,16 +125,16 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
124
125
  if (prepared.state === "off") {
125
126
  const promptPath = path.join(root, "stage-prompt.md");
126
127
  fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
127
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
128
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
128
129
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
129
130
  const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
130
131
  const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
131
132
  commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
132
133
  const reportGit = gitFacts(run.workspace_root);
133
- const nextCommand = codeCommand(context, run.id, projectRoot);
134
+ const next = codeSuccessor(context, run.id, projectRoot);
134
135
  return withStageSettlement(context, () => {
135
136
  const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
136
- const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
137
+ const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, ...(next.command ? { nextCommand: next.command } : {}) });
137
138
  acceptVerificationMatrix(context, prepared.outputMatrix);
138
139
  if (prepared.outputMatrix)
139
140
  Object.assign(report.semantic, { verification_matrix: prepared.outputMatrix.metadata });
@@ -143,7 +144,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
143
144
  completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
144
145
  advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
145
146
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
146
- 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 } };
147
+ 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 };
147
148
  });
148
149
  }
149
150
  const workId = nextWorkId(context, project.id, "plan-review");
@@ -152,7 +153,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
152
153
  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${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, home, "reviewing PLAN coverage, fixtures and proof limits")}\n`;
153
154
  const promptPath = path.join(root, "stage-prompt.md");
154
155
  fs.writeFileSync(promptPath, prompt);
155
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
156
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
156
157
  let binding;
157
158
  try {
158
159
  binding = bindStageCoordinatorWork(context, { workId, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
@@ -172,7 +173,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
172
173
  writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_sources: readJson(prepared.batch).sources.map(source => ({ ...source, content_sha256: planContentHash(readJson(path.join(run.workspace_root, ".memory-bank", "protocol", source.protocol_id, "plan.json"))) })), 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 } });
173
174
  refreshRunWorkProjection(context, project.id, run.id);
174
175
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
175
- return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
176
+ return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
176
177
  }
177
178
  export function dispatchVnextPlanReview(context, input) {
178
179
  ensureWorkRegistry(context);
@@ -326,13 +327,13 @@ function finishPlanReviewOwned(context, input) {
326
327
  const codeParentId = rootWorkId(context, project.id, run.id);
327
328
  const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
328
329
  const reportGit = gitFacts(run.workspace_root);
329
- const nextCommand = codeCommand(context, run.id, projectRoot);
330
+ const next = codeSuccessor(context, run.id, projectRoot);
330
331
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionPath });
331
332
  return withStageSettlement(context, () => {
332
333
  const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
333
334
  const now = context.now();
334
335
  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") });
335
- 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 });
336
+ 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, ...(next.command ? { nextCommand: next.command } : {}) });
336
337
  Object.assign(report.semantic, { def_evidence: defEvidence });
337
338
  applyReviewResults(home, latestChildren, decision);
338
339
  if (matrix)
@@ -346,21 +347,23 @@ function finishPlanReviewOwned(context, input) {
346
347
  refreshRunWorkProjection(context, project.id, run.id);
347
348
  advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
348
349
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
349
- return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: nextCommand } };
350
+ return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next };
350
351
  });
351
352
  }
352
353
  function preparedExisting(context, input) {
353
354
  const promptPath = path.join(input.root, "stage-prompt.md");
354
355
  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 } : {}) });
355
356
  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) });
356
- 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")) } };
357
+ return { ok: true, resumed: true, run_id: input.run.id, stage, outcome: "review_required", work_id: input.workId, id: binding.work_session_id, ...currentStagePrompt(context, promptPath), 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")) } };
357
358
  }
358
359
  function orchestratorPrompt(context, input) {
359
360
  const { revision, capacity } = input;
360
361
  const decision = path.join(input.root, "decision.json");
361
362
  const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
362
363
  const acceptanceOwnership = "For every deferred acceptance or document update, identify the named CODE Work owner, its causal checks, and when it can truthfully close the obligation. Aggregate checks run only after implementation Work completes: assign a CODE owner to pending documents and use CODE needs_checks → verification_required → repair after those receipts. A promise that MERGE or an unnamed readiness owner will finish acceptance is a material PLAN finding; bootstrap workspace readiness is not proof of the new feature.";
363
- const template = [acceptanceOwnership, input.template].join("\n\n");
364
+ const template = [acceptanceOwnership, planCheckInstruction,
365
+ `The accepted check profile is ${path.join(input.run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json")}. A new check required by a correction is planned, not available: name its provider Work and order consumers after that Work.`,
366
+ input.template].join("\n\n");
364
367
  const reviewerLaunch = capacity.source === "external_policy"
365
368
  ? `After dispatch, return at the Work-graph boundary. The shared runtime launches the queued reviewer Works as separate external Sessions, at most ${capacity.available_slots} in parallel under the RUN's frozen profiles. Do not launch native children, create provider roots, run a capacity probe, or record this external limit as native capacity. Reviewers are read-only leaf workers. Continue the semantic decision only after their Work receipts settle; do not substitute a missing result or relaunch a settled reviewer.`
366
369
  : "After dispatch, stop at the Work-graph boundary. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract; do not choose a provider tool from this stage prompt or substitute a shell command. It launches at most the qualified capacity at once and starts unchanged queued Works only after the current wave settles. A launch rejected before it starts is not review evidence: do not create a replacement. Each reviewer must be a genuinely fresh harness child Session; the lifecycle adapter binds that observed Session, so do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits.";
@@ -636,10 +639,11 @@ function reportFor(context, input) {
636
639
  const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
637
640
  const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
638
641
  const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
639
- 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" } };
642
+ const nextCommand = accepted ? input.nextCommand : undefined;
643
+ 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, ...(nextCommand ? { next_command: nextCommand } : {}) }, 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" } };
640
644
  }
641
645
  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 }; }
642
- function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
646
+ function codeSuccessor(context, runId, projectRoot) { return stageSuccessor(context, { projectRoot, runId, stage: "code", command: `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json` }); }
643
647
  function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
644
648
  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`); }
645
649
  function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }
@@ -1,3 +1,4 @@
1
+ export const planCheckInstruction = "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.";
1
2
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
3
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
3
4
  import crypto from "node:crypto";
@@ -18,10 +19,11 @@ import { readVnextSpecifyResult } from "./vnext-specify.js";
18
19
  import { nextWorkId } from "./ids.js";
19
20
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
20
21
  import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
21
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
23
  import { subagentCapacityKey } from "./vnext-fanout.js";
23
24
  import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
24
25
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, verificationMatrixInstruction } from "./verification-matrix.js";
26
+ import { stageSuccessor } from "./vnext-contracts.js";
25
27
  export function isVnextPlanRun(context, input) {
26
28
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
27
29
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
@@ -107,7 +109,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
107
109
  ? [`This RUN must reach MERGE. Project policy already supplies the mandatory merge gate${policyMergeAliases.length === 1 ? "" : "s"}: ${policyMergeAliases.join(", ")}. Do not duplicate them in semantic checks[]. Add another merge check only when the task genuinely needs additional evidence.`]
108
110
  : ["This RUN must reach MERGE and project policy supplies no merge gate. Select at least one real top-level checks[] entry with run_at: merge. It may use an existing project alias or a planned alias materialised by a named P* provider Work. This is a planning obligation: do not defer it to CODE-REVIEW or MERGE."]), "The CLI validates the effective merge gate but never invents one or migrates an incompatible project policy.", "</merge_gate_contract>", ""]
109
111
  : [];
110
- const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each check must terminate and return an exit status. Never declare a bare dev/start/serve/watch service command as a check; use a finite test that manages its own service lifecycle (for example, a browser test with a webServer fixture).", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
112
+ const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", planCheckInstruction, "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each check must terminate and return an exit status. Never declare a bare dev/start/serve/watch service command as a check; use a finite test that manages its own service lifecycle (for example, a browser test with a webServer fixture).", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
111
113
  const artifactMaterialization = { status: "materialized", completeness: "partially_filled", plan_paths: planPaths, aspect_map_paths: mapPaths, cli_owned_plan_fields: ["schema_id", "plan_id", "protocol_id", "revision", "source_refs"], cli_owned_aspect_map_fields: ["schema_id", "protocol_id", "plan_id", "plan_revision", "catalog_ref", "aspects[].aspect_id"], validation_commands: validationCommands };
112
114
  const promptPath = path.join(root, "stage-prompt.md");
113
115
  // This explicit final rule supersedes historical pack wording: a flow gate
@@ -117,7 +119,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
117
119
  ? `PLAN-REVIEW mode is ${planReviewMode}: assign at least one genuinely applicable aspect to a review group. Small feature size alone does not make every aspect not applicable; PLAN finish rejects an empty review set.`
118
120
  : `PLAN-REVIEW mode is ${planReviewMode}: include review groups only for genuinely applicable aspects; an empty set is allowed.`;
119
121
  fs.writeFileSync(promptPath, `${prompt}\n<review_mode_rule>${reviewModeRule}</review_mode_rule>\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`);
120
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
122
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
121
123
  if (prepared.matrix)
122
124
  fs.appendFileSync(promptPath, verificationMatrixInstruction(prepared.matrix.metadata, home, "designing the source PLAN and its tests"));
123
125
  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));
@@ -129,7 +131,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
129
131
  const sessionId = String(binding.session_id);
130
132
  refreshRunWorkProjection(context, project.id, run.id);
131
133
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage: "plan", work_id: planWorkId, id: workSessionId, session_id: sessionId });
132
- return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), prompt_path: promptPath, next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
134
+ return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, ...currentStagePrompt(context, promptPath), next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
133
135
  }
134
136
  export function prepareVnextPlanFinish(context, input) {
135
137
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -171,9 +173,9 @@ function finishVnextPlanOwned(context, input) {
171
173
  prepared.publish();
172
174
  const now = context.now();
173
175
  const batchChecksum = checksum(batch);
174
- const reviewCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`);
176
+ const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan-review", command: `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json` });
175
177
  const startedAt = stageStartedAt(home, now);
176
- 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" } };
178
+ 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 ? { next_command: next.command } : {}) }, 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" } };
177
179
  Object.assign(report.semantic, {
178
180
  plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
179
181
  aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
@@ -197,7 +199,7 @@ function finishVnextPlanOwned(context, input) {
197
199
  refreshRunWorkProjection(context, project.id, run.id);
198
200
  advanceFlowRun(context, { settlementStage: "plan", projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
199
201
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
200
- 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 } };
202
+ return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", ...(next.command ? { next_command: next.command } : {}), next };
201
203
  });
202
204
  }
203
205
  export function validateVnextPlanArtifacts(context, input) {
@@ -1,4 +1,5 @@
1
- import { managedLifecycleCommand } from "./lifecycle-invocations.js";
1
+ import { isControllerManagedInvocation, managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { stageSuccessor } from "./vnext-contracts.js";
2
3
  import crypto from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import os from "node:os";
@@ -19,7 +20,7 @@ import { loadVnextWorkspacePolicy, requireVnextWorkspaceRoute, vnextRunBranch }
19
20
  import { readVnextSpecifyResult } from "./vnext-specify.js";
20
21
  import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
21
22
  import { acceptStageReport, prepareStageReport, publishStageReport } from "./stage-report-renderer.js";
22
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
23
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext, renderRunOperationalContext } from "./stage-context.js";
23
24
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
24
25
  import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
25
26
  import { withProjectionLock } from "../storage/projection-lock.js";
@@ -81,11 +82,16 @@ export function prepareVnextProtocolize(context, input, prepared = prepareVnextP
81
82
  const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId: work.work_id, projectRoot });
82
83
  const prompt = renderPrompt({ projectRoot, runId: run.id, workId: work.work_id, specifyPath, obligations, resultPath, handoff: input.handoff, catalog: projectCatalog, policy, template, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), finishCommand: managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --result-file ${JSON.stringify(resultPath)} --json`) });
83
84
  const workspaceContract = ["<workspace_contract>", `- stage artifact directory: ${stageRoot}`, `- provisioned project workspace for later stages: ${policy.worktree_path ?? projectRoot}`, `- this PROTOCOLIZE Session remains in its launch directory: ${projectRoot}`, `Write only the named result file in the stage artifact directory; do not write project files, another RUN, another native home, branches, worktrees or Git commits. Do not change the Session directory to the newly provisioned worktree.`, "The CLI performs all protocol document materialization after the result is accepted.", "</workspace_contract>"].join("\n");
84
- const promptWithWorkspaceContract = prompt.replace("</work_context>", `</work_context>\n\n${workspaceContract}`);
85
+ const profile = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE id = ? AND project_id = ?", [run.id, project.id]).index_json).execution_profile;
86
+ const operational = profile?.operational_contract ? renderRunOperationalContext(requireRunHome(run), profile.operational_contract.sha256, profile.operational_contract.path) : "";
87
+ const promptWithWorkspaceContract = `${operational}\n\n${prompt.replace("</work_context>", `</work_context>\n\n${workspaceContract}`)}`;
85
88
  fs.writeFileSync(promptPath, promptWithWorkspaceContract);
86
89
  // Packet publication does not itself bind a Session or open the Stage.
87
90
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_prepared", work_id: work.work_id, stage, handoff_mode: input.handoff.stage_handoff.effective });
88
- const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: promptWithWorkspaceContract } : { kind: "await_new_session", stage, work_id: work.work_id, prompt_path: promptPath, resume_command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`) };
91
+ // The controller has already selected and bound this Stage's Session. The
92
+ // frozen handoff remains evidence, not authority to open another Session.
93
+ const currentSession = input.handoff.stage_handoff.effective === "same_session" || isControllerManagedInvocation(context, projectRoot, run.id);
94
+ const directive = currentSession ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: promptWithWorkspaceContract } : { kind: "await_new_session", stage, work_id: work.work_id, prompt_path: promptPath, resume_command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`) };
89
95
  return { directive, prompt_path: promptPath };
90
96
  }
91
97
  export function prepareVnextProtocolizeStart(context, input) {
@@ -149,7 +155,7 @@ export function startVnextProtocolize(context, input, prepared = prepareVnextPro
149
155
  if (packet)
150
156
  prepareVnextProtocolize(context, packetInput, packet);
151
157
  const frozenContext = prepared.frozenContext ?? readProtocolizeContext(contextPath);
152
- const externalContext = applyExternalStageContext({ stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
158
+ const externalContext = applyExternalStageContext({ context, stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
153
159
  const binding = bindStageCoordinatorWork(context, { workId: work.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(requireRunHome(run), "02-protocolize", "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
154
160
  // Same-session handoff deliberately reuses the root Work/Session that
155
161
  // SPECIFY opened. That is not evidence that PROTOCOLIZE is already attached.
@@ -157,11 +163,11 @@ export function startVnextProtocolize(context, input, prepared = prepareVnextPro
157
163
  if (!stageAttached)
158
164
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: "02-protocolize", status: "running", dataSchemaId: "dd-flow/vnext-protocolize-result@3" });
159
165
  if (mode === "same_session" && activeWorkSession?.session_id === sessionId) {
160
- return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: activeWorkSession.id, prompt_path: promptPath, git_policy: frozenContext.workspace?.git_policy ?? null, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}) };
166
+ return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: activeWorkSession.id, git_policy: frozenContext.workspace?.git_policy ?? null, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}) };
161
167
  }
162
168
  const workSessionId = String(binding.work_session_id);
163
169
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", work_id: work.work_id, id: workSessionId, stage, session_id: sessionId });
164
- return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: workSessionId, prompt_path: promptPath, git_policy: frozenContext.workspace?.git_policy ?? null, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}) };
170
+ return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: workSessionId, git_policy: frozenContext.workspace?.git_policy ?? null, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}) };
165
171
  }
166
172
  function specifyHandoff(file, workId) {
167
173
  try {
@@ -237,8 +243,8 @@ export function finishVnextProtocolize(context, input) {
237
243
  if (currentIndex.verdict !== "protocolized")
238
244
  advanceFlowRun(context, { settlementStage: "protocolize", projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
239
245
  refreshRunWorkProjection(context, project.id, run.id);
240
- const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`);
241
- return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", next_command: nextCommand, next: { kind: "start_stage", stage: "plan", command: nextCommand, cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode }, artifacts: { result: resultFile, report_json: reportJson, report_markdown: reportMarkdown, report_html: reportHtml, protocols: protocolIds.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "summary.md")) } };
246
+ const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan", command: `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode } });
247
+ return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", ...(next.command ? { next_command: next.command } : {}), next, artifacts: { result: resultFile, report_json: reportJson, report_markdown: reportMarkdown, report_html: reportHtml, protocols: protocolIds.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "summary.md")) } };
242
248
  }
243
249
  function prepareProtocolizeIntent(context, run, resultFile, result, obligations) {
244
250
  const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
@@ -1,10 +1,11 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { stageSuccessor } from "./vnext-contracts.js";
2
3
  import crypto from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { spawnSync } from "node:child_process";
6
7
  import { AppError } from "../shared/errors.js";
7
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
8
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
8
9
  import { ensureVnextWorkStorage } from "../storage/database.js";
9
10
  import { projectHome, resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
10
11
  import { preflightMemoryPermissions } from "./memory-permissions.js";
@@ -114,7 +115,7 @@ export function launchVnextSpecify(context, input, prepared = prepareVnextSpecif
114
115
  const pauseCommand = stagePauseCommand(context, { runId: run.id, stage: stageId, workId, projectRoot });
115
116
  const prompt = renderPrompt({ workId, runId: run.id, projectRoot, stageRoot, resultPath, discussion, template, workContext, grounding, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), finishCommand: managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --project-root ${JSON.stringify(projectRoot)} --stage specify --result-file ${JSON.stringify(path.join(stageRoot, "specify-result.json"))} --outcome specified --json`) });
116
117
  fs.writeFileSync(promptPath, prompt);
117
- const externalContext = applyExternalStageContext({ stageRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
118
+ const externalContext = applyExternalStageContext({ context, stageRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
118
119
  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)
119
120
  VALUES (?, ?, ?, NULL, ?, ?, NULL, '[]', 'running', NULL, ?, ?, ?, NULL)`, [workId, run.project_id, run.id, "Complete the requested MB-SDLC flow through its configured stop target.", "reuse_allowed", now, now, now]);
120
121
  const binding = bindRunningWorkSession(context, { workId, hookEventId: input.hookEventId, promptPath, resultPath });
@@ -142,10 +143,9 @@ export function launchVnextSpecify(context, input, prepared = prepareVnextSpecif
142
143
  run_id: run.id,
143
144
  work_id: workId,
144
145
  work_session_id: workSessionId,
145
- prompt_path: promptPath,
146
146
  result_path: resultPath,
147
147
  work_context_path: contextPath,
148
- worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"),
148
+ ...currentStagePrompt(context, promptPath),
149
149
  ...(externalContext ? { external_context: externalContext } : {})
150
150
  };
151
151
  }
@@ -227,14 +227,14 @@ export function submitVnextSpecify(context, input) {
227
227
  const nextWork = requireWork(context, work.work_id);
228
228
  advanceFlowRun(context, { settlementStage: "specify", projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
229
229
  refreshRunWorkProjection(context, run.project_id, run.id);
230
- const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`);
230
+ const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "protocolize", command: `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { handoff_mode: handoff.stage_handoff.effective } });
231
231
  return {
232
232
  ok: true,
233
233
  schema_id: "dd-flow/vnext-work-submit@1",
234
234
  outcome,
235
235
  run_id: run.id,
236
236
  work: workProjection(nextWork, latestWorkSession(context, work.work_id)),
237
- next: { kind: "start_stage", stage: "protocolize", command: nextCommand, handoff_mode: handoff.stage_handoff.effective },
237
+ next,
238
238
  artifacts: { result_json: resultFile, result_markdown: path.join(stageRoot, "specify.md"), report_json: reportPath, report_markdown: markdownPath, report_html: htmlPath }
239
239
  };
240
240
  }
@@ -10,6 +10,7 @@ import { resolveWorkReference } from "../storage/work-references.js";
10
10
  import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageStartHookEvent, claimWorkLifecycleHookEvent, claimWorkStartHookEvent, hookSessionIdentity, workStartMatchKey } from "./hooks.js";
11
11
  import { validateSchema } from "./schema-validation.js";
12
12
  import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
13
+ import { prepareAgentOutputFile } from "../storage/agent-output.js";
13
14
  import { refreshRunSessionProjection } from "./run-projection.js";
14
15
  import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
15
16
  import { nextWorkId, nextWorkIds } from "./ids.js";
@@ -23,7 +24,7 @@ import { activateRecoveryWork, assertRecoveryHookCurrent, assertRunMutationAllow
23
24
  import { runControlDirective } from "./run-control.js";
24
25
  import { prepareError, prepareJsonFile } from "../cli/input-preparation.js";
25
26
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
26
- import { acceptedDecisionPrecedence } from "./stage-context.js";
27
+ import { acceptedDecisionPrecedence, loadExternalStageContext, renderRetainedStageContext, renderRunOperationalContext, runContextRelocations } from "./stage-context.js";
27
28
  import { reviewEnvironmentAllowed, reviewExecutionCopyHash, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
28
29
  import { latestRunVerificationMatrix, verificationMatrixInstruction, verificationMatrixContract } from "./verification-matrix.js";
29
30
  const workColumns = "work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, created_at, started_at, updated_at, completed_at";
@@ -296,6 +297,7 @@ export function materializePendingWorkStarts(context, projectId, runId) {
296
297
  throw new AppError("work_start_receipt_invalid", "Committed Work start receipt does not match its retained bytes", 1, { work_id: row.work_id });
297
298
  if (row.start_publication_status && context.db.writeTransaction(() => context.db.run("UPDATE work_sessions SET start_publication_status = 'publishing' WHERE id = ? AND status = 'running' AND prompt_path = ? AND start_receipt_json = ? AND start_publication_status IN ('pending','publishing')", [row.id, row.prompt_path, row.start_receipt_json]).changes) !== 1)
298
299
  continue;
300
+ prepareAgentOutputFile(path.join(runHome, "works", row.work_id, "result-input.json"));
299
301
  publishStartReceiptFile(row.prompt_path, receipt.prompt, String(receipt.prompt_sha256));
300
302
  publishStartReceiptFile(path.join(path.dirname(row.prompt_path), "context.json"), receipt.context, String(receipt.context_sha256));
301
303
  if (row.start_publication_status && context.db.writeTransaction(() => context.db.run("UPDATE work_sessions SET start_publication_status = 'published' WHERE id = ? AND status = 'running' AND prompt_path = ? AND start_receipt_json = ? AND start_publication_status = 'publishing'", [row.id, row.prompt_path, row.start_receipt_json]).changes) !== 1) {
@@ -537,6 +539,17 @@ export function bindStageCoordinatorWork(context, input) {
537
539
  const run = requireRun(context, work.project_id, work.run_id);
538
540
  const snapshot = JSON.parse((context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id])?.index_json) ?? "{}");
539
541
  const profile = snapshot.execution_profile;
542
+ if (profile && input.contextSha256) {
543
+ const file = path.join(path.dirname(input.promptPath), "stage-context.json");
544
+ if (!fs.existsSync(file) || sha256(fs.readFileSync(file, "utf8")) !== input.contextSha256)
545
+ throw new AppError("context_checksum_mismatch", "Retained Stage context changed before coordinator binding", 2);
546
+ const item = { path: path.relative(requireRunHome(run), file), sha256: input.contextSha256 };
547
+ const prior = profile.stage_context_inputs?.[input.stage];
548
+ if (prior && JSON.stringify(prior) !== JSON.stringify(item))
549
+ throw new AppError("stage_context_replacement", "Stage already has a different accepted context", 2);
550
+ profile.stage_context_inputs = { ...profile.stage_context_inputs, [input.stage]: item };
551
+ context.db.run("UPDATE runs SET index_json = ? WHERE id = ? AND project_id = ?", [JSON.stringify(snapshot), run.id, work.project_id]);
552
+ }
540
553
  const previous = context.db.get("SELECT ws.session_id, s.current_stage FROM work_sessions ws JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? AND ws.status = 'running' ORDER BY ws.created_at DESC LIMIT 1", [work.project_id, work.work_id]);
541
554
  let sessionMode = profile?.settings.stage_session_mode ?? "same_session";
542
555
  if (profile?.settings.execution) {
@@ -589,7 +602,8 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
589
602
  const retryPending = fs.existsSync(path.join(directory, ".retry.json"));
590
603
  const finishMarker = `DD_FLOW_FINISH_${crypto.randomUUID()}`;
591
604
  const failMarker = `DD_FLOW_FAIL_${crypto.randomUUID()}`;
592
- let promptTemplate = renderWorkerPrompt(context, work, workerRun, preparedDependencies, { finish: finishMarker, fail: failMarker });
605
+ const workspaceView = preparedCopy ? { work_id: work.work_id, from: run.workspace_root, to: reviewInputPath(requireRunHome(run), preparedCopy.copy.root), source_sha256: preparedCopy.copy.source_sha256, copy_sha256: preparedCopy.copy.sha256 } : undefined;
606
+ let promptTemplate = renderWorkerPrompt(context, work, workerRun, preparedDependencies, { finish: finishMarker, fail: failMarker }, workspaceView);
593
607
  if (preparedRecovery)
594
608
  promptTemplate = `${recoveryPrompt(preparedRecovery)}\n\n${promptTemplate}`;
595
609
  let binding;
@@ -951,6 +965,15 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
951
965
  throw new AppError("review_input_invalid", "Review input has conflicting accepted checksums", 1, { path: relativePath });
952
966
  expected.set(key, sha);
953
967
  };
968
+ const frozenProfile = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json).execution_profile;
969
+ const operational = frozenProfile?.operational_contract;
970
+ if (operational)
971
+ add("run", operational.path, operational.sha256);
972
+ if (operational?.policy_source)
973
+ add("run", operational.policy_source.path, operational.policy_source.sha256);
974
+ const stageContexts = frozenProfile?.stage_context_inputs ?? {};
975
+ for (const item of Object.values(stageContexts))
976
+ add("run", item.path, item.sha256);
954
977
  const stageReport = (name, relativePath) => {
955
978
  const sha = stage(name)?.data_sha256;
956
979
  if (!sha)
@@ -1251,9 +1274,16 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
1251
1274
  if (missing.length || unexpected.length || duplicate)
1252
1275
  throw new AppError("review_repair_incomplete", "Review repair must include exactly one resolution for each assigned finding reference", 2, { work_id: workId, missing, unexpected, duplicate });
1253
1276
  }
1254
- function renderWorkerPrompt(context, work, run, dependencies, commands) {
1255
- const body = renderWorkerPromptBody(context, work, run, dependencies, commands);
1277
+ function renderWorkerPrompt(context, work, run, dependencies, commands, workspaceView) {
1278
+ const profile = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json).execution_profile;
1279
+ const operational = profile?.operational_contract ? renderRunOperationalContext(requireRunHome(run), profile.operational_contract.sha256, profile.operational_contract.path) : "";
1256
1280
  const kind = parsePayload(work)?.kind;
1281
+ const contextStage = codePacket(work) ? "code" : typeof kind === "string" ? kind : null;
1282
+ const contextInput = contextStage ? profile?.stage_context_inputs?.[contextStage] : null;
1283
+ const contextFile = contextInput ? path.join(requireRunHome(run), contextInput.path) : null;
1284
+ const relocationReceipts = runContextRelocations(context, work.project_id, work.run_id);
1285
+ const retainedContext = contextFile ? renderRetainedStageContext({ loaded: loadExternalStageContext({ file: contextFile, expectedSha256: contextInput.sha256, stage: contextStage }), receipts: relocationReceipts, ...(workspaceView ? { workspaceView } : {}) }) : "";
1286
+ const body = `${operational}\n\n${retainedContext}${renderWorkerPromptBody(context, work, run, dependencies, commands)}`;
1257
1287
  const matrixStage = kind === "plan-review" || kind === "code-review" ? kind : codePacket(work) ? "code" : null;
1258
1288
  const matrix = matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null;
1259
1289
  const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { assertRunMutationAllowed } from "./run-recovery.js";
6
6
  import { codeExecutionEnvironment, reconcileWorkspaceOwnership, runCheck, workspaceOperationId } from "./code-checks.js";
7
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
7
+ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
8
8
  /** CODE and MERGE prepare their own checkout through the same managed executor.
9
9
  * Bootstrap is a project operation, not an alias classified by the engine as a test. */
10
10
  export async function runWorkspaceBootstrap(context, input) {
@@ -56,7 +56,7 @@ export async function runWorkspaceBootstrap(context, input) {
56
56
  finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: "failed", reason: "dispatch_fenced_before_spawn" });
57
57
  throw error;
58
58
  }
59
- const completed = await runCheck(input.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_CHECK_COMPLETION_FILE: completionPath }, stdout, stderr, elapsed => input.progress?.(`workspace bootstrap still running (${elapsed}s); logs: ${directory}; next report in 15s`), pid => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token }));
59
+ const completed = await runCheck(input.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_CHECK_COMPLETION_FILE: completionPath }, stdout, stderr, elapsed => input.progress?.(`workspace bootstrap still running (${elapsed}s); logs: ${directory}; next report in 15s`), pid => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }));
60
60
  const current = managedProcessStatus(context).find(item => item.id === managed.id);
61
61
  if (processTreeIsAlive(current))
62
62
  throw new AppError("bootstrap_tree_not_settled", "Bootstrap shell exited but still owns live processes; resources were retained for inspection", 2, { process_id: managed.id, stdout_path: stdoutPath, stderr_path: stderrPath });
@@ -71,6 +71,17 @@ export async function runWorkspaceBootstrap(context, input) {
71
71
  }
72
72
  function result(id, stdout, stderr, completion, exitCode, error) {
73
73
  // Full logs remain on disk; keep the returned prompt bounded.
74
- const excerpt = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8").slice(-64_000) : "";
74
+ const excerpt = (file) => {
75
+ if (!fs.existsSync(file))
76
+ return "";
77
+ const fd = fs.openSync(file, "r");
78
+ try {
79
+ const size = fs.fstatSync(fd).size, bytes = Buffer.alloc(Math.min(size, 64_000));
80
+ return bytes.subarray(0, fs.readSync(fd, bytes, 0, bytes.length, Math.max(0, size - bytes.length))).toString("utf8");
81
+ }
82
+ finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ };
75
86
  return { exit_code: exitCode, stdout: excerpt(stdout), stderr: excerpt(stderr), error, process_id: id, stdout_path: stdout, stderr_path: stderr, completion_path: completion };
76
87
  }
@@ -1,3 +1,4 @@
1
+ import { errorRecord as nativeErrorRecord } from "../harness-runtime/lib/operation-errors.mjs";
1
2
  export class AppError extends Error {
2
3
  code;
3
4
  exitCode;
@@ -11,14 +12,18 @@ export class AppError extends Error {
11
12
  }
12
13
  /** Error fields must cross JSON boundaries as data: Error.message is not enumerable. */
13
14
  export function errorRecord(error, depth = 0) {
14
- const value = error;
15
- const details = value?.details && typeof value.details === "object" && !Array.isArray(value.details) ? value.details : {};
15
+ const value = nativeErrorRecord(error, depth);
16
+ const details = value.details && typeof value.details === "object" && !Array.isArray(value.details) ? value.details : {};
17
+ let typed = false;
18
+ try {
19
+ typed = typeof error?.code === "string";
20
+ }
21
+ catch { /* Diagnostic access cannot replace the primary failure. */ }
16
22
  return {
17
- code: typeof value?.code === "string" ? value.code : "unexpected",
18
- message: typeof value?.message === "string" ? value.message : String(error),
19
- exitCode: typeof value?.exitCode === "number" ? value.exitCode : 1,
23
+ ...value,
24
+ code: typed ? value.code : "unexpected",
25
+ exitCode: value.exitCode ?? 1,
20
26
  details,
21
- ...(value?.cause && depth < 3 ? { cause: errorRecord(value.cause, depth + 1) } : {})
22
27
  };
23
28
  }
24
29
  export function isAppError(error) {
@@ -0,0 +1,26 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ /** Execution only: prepare an agent's write destination without creating or replacing its payload. */
5
+ export function prepareAgentOutputFile(file) {
6
+ const absolute = path.resolve(file);
7
+ try {
8
+ const parent = path.dirname(absolute);
9
+ fs.mkdirSync(parent, { recursive: true });
10
+ fs.accessSync(parent, fs.constants.W_OK | fs.constants.X_OK);
11
+ // lstat also rejects dangling links: an advertised output must not silently
12
+ // redirect a write to another authority or overwrite a retained input.
13
+ const existing = fs.lstatSync(absolute, { throwIfNoEntry: false });
14
+ if (existing) {
15
+ if (!existing.isFile())
16
+ throw new Error("Output must be a regular file, not a directory or link");
17
+ fs.accessSync(absolute, fs.constants.W_OK);
18
+ }
19
+ return absolute;
20
+ }
21
+ catch (error) {
22
+ throw new AppError("agent_output_unavailable", "Agent output destination is not writable", 1, {
23
+ file: absolute, cause: error instanceof Error ? error.message : String(error)
24
+ });
25
+ }
26
+ }