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

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 (41) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +1 -1
  5. package/dist/domain/stage-catalog.js +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  7. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  8. package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +66 -16
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +39 -10
  12. package/dist/harness-runtime/lib/dd-codex.mjs +11 -25
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
  15. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
  16. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
  17. package/dist/harness-runtime/lib/dd-zcode.mjs +9 -10
  18. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  19. package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
  20. package/dist/harness-runtime/lib/managed-daemon.mjs +100 -32
  21. package/dist/schemas/flow-run.schema.json +1 -0
  22. package/dist/schemas/plan-review-decision.schema.json +3 -3
  23. package/dist/services/code-checks.js +14 -18
  24. package/dist/services/codex-capacity.js +149 -48
  25. package/dist/services/controller-fanout.js +1 -0
  26. package/dist/services/external-work-launch.js +10 -3
  27. package/dist/services/harness-adapter.js +21 -15
  28. package/dist/services/managed-processes.js +14 -6
  29. package/dist/services/merge-server.js +10 -4
  30. package/dist/services/review-deferral.js +23 -0
  31. package/dist/services/run-controller-adapter.js +8 -5
  32. package/dist/services/run-controller-recovery.js +17 -0
  33. package/dist/services/run-controller.js +42 -14
  34. package/dist/services/run-recovery-runtime.js +49 -3
  35. package/dist/services/runs.js +1 -1
  36. package/dist/services/verification-matrix.js +30 -1
  37. package/dist/services/vnext-code-review.js +16 -4
  38. package/dist/services/vnext-merge.js +20 -2
  39. package/dist/services/vnext-plan-review.js +81 -18
  40. package/dist/services/work-registry.js +9 -2
  41. package/package.json +1 -1
@@ -24,6 +24,7 @@ import { stageGraphScope } from "./stage-work-graph.js";
24
24
  import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
25
25
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
26
26
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
27
+ import { reviewDefEvidence } from "./review-deferral.js";
27
28
  const stage = "plan-review";
28
29
  const stageDir = vnextStageDirectory(stage);
29
30
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
@@ -65,7 +66,7 @@ export function prepareVnextPlanReviewStart(context, input) {
65
66
  if (input.externalContext)
66
67
  prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
67
68
  prepareOutputFile(path.join(root, "stage-prompt.md"));
68
- const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
69
+ const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" });
69
70
  const matrix = existing?.status === "running" ? latestRunVerificationMatrix(context, project.id, run.id, stage, "input") : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
70
71
  const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix, reviewerInputs };
71
72
  if (existing?.status === "running") {
@@ -117,7 +118,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
117
118
  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 } : {}) };
118
119
  }
119
120
  ensureWorkRegistry(context);
120
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" }, true);
121
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" }, true);
121
122
  fs.mkdirSync(root, { recursive: true });
122
123
  const now = context.now(), rootWork = prepared.rootWork;
123
124
  if (prepared.state === "off") {
@@ -125,7 +126,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
125
126
  fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
126
127
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
127
128
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
128
- const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
129
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
129
130
  const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
130
131
  commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
131
132
  const reportGit = gitFacts(run.workspace_root);
@@ -161,14 +162,14 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
161
162
  fs.rmSync(promptPath, { force: true });
162
163
  throw error;
163
164
  }
164
- const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
165
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
165
166
  const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
166
167
  commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
167
168
  markReviewPending(home, groups);
168
169
  const workSessionId = String(binding.work_session_id);
169
170
  const sessionId = String(binding.session_id);
170
171
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
171
- 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_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 } });
172
+ 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 } });
172
173
  refreshRunWorkProjection(context, project.id, run.id);
173
174
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
174
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")) } };
@@ -260,7 +261,6 @@ function finishPlanReviewOwned(context, input) {
260
261
  if (!Number.isInteger(reviewedPlanRevision) || reviewedPlanRevision < 1)
261
262
  throw new AppError("runtime_missing", "PLAN-REVIEW starting revision is unavailable", 1);
262
263
  const planRevision = currentPlanRevision(home, run.workspace_root);
263
- const currentPlanChecksum = planSetChecksum(home, run.workspace_root);
264
264
  if (decision.outcome === "blocked") {
265
265
  throw new AppError("stage_pause_required", "A user or decision blocker must pause the current PLAN-REVIEW Work with `dd-flow stage pause`; do not finish the stage as blocked", 2, { run_id: run.id, stage, work_id: parent.work_id });
266
266
  }
@@ -287,7 +287,6 @@ function finishPlanReviewOwned(context, input) {
287
287
  // plan uncertainty prevents it from approving its aspect. That is evidence
288
288
  // for the coordinator to classify and fix in this one review pass, not an
289
289
  // infrastructure failure or a reason to start another review cycle.
290
- const needsCorrection = latestChildren.some((child) => ["needs_changes", "blocked"].includes(reviewerVerdict(child.result)));
291
290
  if (latestChildren.some((child) => child.status !== "completed"))
292
291
  throw new AppError("blocked", "PLAN-REVIEW cannot accept incomplete reviewer evidence", 2);
293
292
  const canonicalFindings = canonicalReviewerFindings(latestChildren);
@@ -298,19 +297,23 @@ function finishPlanReviewOwned(context, input) {
298
297
  const unknownDecisions = decisionFindingIds.filter((id) => !allFindingIds.includes(id));
299
298
  if (new Set(allFindingIds).size !== allFindingIds.length || decidedFindingIds.size !== decisionFindingIds.length || unknownDecisions.length || materialFindingIds.some((id) => !decidedFindingIds.has(id)))
300
299
  throw new AppError("review_evidence_invalid", "Reviewer findings and coordinator decisions must use unique known canonical references, with every material finding decided", 2, { material_finding_ids: materialFindingIds, decided_finding_ids: decisionFindingIds, unknown_decisions: unknownDecisions });
301
- if (needsCorrection) {
302
- if (decision.correction.status !== "applied")
303
- throw new AppError("validation", "Reviewer findings require an in-place correction receipt", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
304
- if (decision.correction.previous_plan_revision !== reviewedPlanRevision || planRevision <= reviewedPlanRevision || decision.correction.changed_paths.length === 0)
305
- throw new AppError("validation", "Applied PLAN correction must advance revision and list changed paths", 2, { reviewed_plan_revision: reviewedPlanRevision, final_plan_revision: planRevision, phase: "prepare", effect: "no_effect", recoverable: true });
306
- }
307
- else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision || currentPlanChecksum !== contextFile.system?.plan_checksum) {
308
- throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
309
- }
300
+ const defEvidence = validatePlanFindingDecisions(decision.finding_decisions, allFindingIds, run.workspace_root);
301
+ const acceptedMatrix = latestRunVerificationMatrix(context, project.id, run.id, "plan", "output");
302
+ const planSources = acceptedMatrix ? acceptedMatrix.matrix.sources.filter(source => source.role.startsWith("plan:")).map(source => {
303
+ const packet = acceptedMatrix.packets.find(packet => packet.file === path.join(source.root === "run" ? home : run.workspace_root, source.path) && packet.sha256 === source.sha256);
304
+ if (!packet)
305
+ throw new AppError("review_input_contract_missing", "Accepted PLAN source lacks its immutable publication bytes", 1);
306
+ const value = JSON.parse(packet.bytes.toString("utf8"));
307
+ return { plan_id: String(value.plan_id), protocol_id: String(value.protocol_id), revision: Number(value.revision), sha256: source.sha256, content_sha256: planContentHash(value) };
308
+ }) : contextFile.system?.plan_sources ?? [];
309
+ if (checksum(path.join(planRoot, "stage-report.json")) !== readIndex(run).stage_runs?.find(entry => entry.stage === "plan")?.data_sha256)
310
+ throw new AppError("read_only_input_changed", "Accepted PLAN report changed during review", 2);
311
+ const acceptedPlan = readJson(path.join(planRoot, "stage-report.json"));
312
+ validatePlanCorrection({ decision, reviewedPlanRevision: reviewedPlanRevision, sources: planSources, maps: acceptedPlan.semantic.aspect_map_checksums, workspaceRoot: run.workspace_root, home });
310
313
  const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
311
314
  if (prepared.failures.length)
312
315
  throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures, phase: "prepare", effect: "no_effect", recoverable: true });
313
- if (needsCorrection) {
316
+ if (decision.correction.status === "applied") {
314
317
  // Regeneration above is authoritative. A valid PLAN correction need not
315
318
  // change the derived CODE tasks (for example, a clarified explanation).
316
319
  if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
@@ -330,6 +333,7 @@ function finishPlanReviewOwned(context, input) {
330
333
  const now = context.now();
331
334
  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") });
332
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
+ Object.assign(report.semantic, { def_evidence: defEvidence });
333
337
  applyReviewResults(home, latestChildren, decision);
334
338
  if (matrix)
335
339
  Object.assign(report.semantic, { verification_matrix: matrix.metadata });
@@ -360,7 +364,7 @@ function orchestratorPrompt(context, input) {
360
364
  const reviewerLaunch = capacity.source === "external_policy"
361
365
  ? `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.`
362
366
  : "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.";
363
- return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input 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", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. For an accepted correction, increment PLAN revision and update only plan.json and the relevant aspect map. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@3", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why." }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
367
+ return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input 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", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. Accepted fixes need an actual authored delta. Rejected/duplicate findings need no artificial correction. Coordinator-only correction is allowed. Increment only the changed protocol PLAN's own revision; an aspect-map-only correction needs no fake PLAN revision. changed_paths must list exactly the changed workspace-relative PLAN paths and RUN/<aspect-map-path>. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@4", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why.", duplicate_of: "A distinct canonical finding_ref only for duplicate", def_id: "DEF-0001 only for deferred_as_DEF" }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
364
368
  }
365
369
  function reviewGroups(home, workspaceRoot) {
366
370
  const root = path.join(home, "03-plan");
@@ -438,6 +442,65 @@ function markReviewPending(_home, _groups) { }
438
442
  /** Review evidence is immutable child Work output plus the stage decision. */
439
443
  function applyReviewResults(_home, _children, _decision) { }
440
444
  function canonicalReviewerFindings(children) { return children.flatMap((child) => (reviewerResult(child.result)?.aspects ?? []).flatMap((aspect) => aspect.findings.map((finding) => ({ finding_ref: `${child.work_id}/${finding.finding_id}`, finding })))); }
445
+ export function validatePlanFindingDecisions(items, knownRefs, workspaceRoot) {
446
+ const known = new Set(knownRefs), decisions = new Map(items.map(item => [item.finding_ref, item]));
447
+ if (decisions.size !== items.length || items.some(item => !item.finding_ref || !known.has(item.finding_ref)))
448
+ throw new AppError("review_evidence_invalid", "PLAN decisions require unique known finding references", 2);
449
+ if (items.some(item => item.decision === "requires_user"))
450
+ throw new AppError("stage_pause_required", "Resolve the user question with stage pause/resume before accepting PLAN-REVIEW", 2);
451
+ for (const item of items.filter(item => item.decision === "duplicate")) {
452
+ let cursor = item;
453
+ const seen = new Set();
454
+ while (cursor?.decision === "duplicate") {
455
+ if (!cursor.finding_ref || seen.has(cursor.finding_ref) || !cursor.duplicate_of || !known.has(cursor.duplicate_of) || cursor.duplicate_of === cursor.finding_ref)
456
+ throw new AppError("review_evidence_invalid", "PLAN duplicates must form an acyclic chain of distinct known findings", 2);
457
+ seen.add(cursor.finding_ref);
458
+ cursor = decisions.get(cursor.duplicate_of);
459
+ }
460
+ if (!cursor)
461
+ throw new AppError("review_evidence_invalid", "A PLAN duplicate must end at a final decided finding", 2);
462
+ }
463
+ return items.filter(item => item.decision === "deferred_as_DEF").map(item => reviewDefEvidence(workspaceRoot, item.def_id));
464
+ }
465
+ function planContentHash(plan) {
466
+ const content = { ...plan };
467
+ delete content.revision;
468
+ return crypto.createHash("sha256").update(JSON.stringify(content, (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value)).digest("hex");
469
+ }
470
+ export function validatePlanCorrection(input) {
471
+ const changed = [];
472
+ if (!input.sources.length || !input.maps?.length)
473
+ throw new AppError("review_input_contract_missing", "PLAN correction needs its accepted per-protocol sources", 1);
474
+ const invalid = (message) => { throw new AppError("validation", message, 2, { phase: "prepare", effect: "no_effect", recoverable: true }); };
475
+ for (const source of input.sources) {
476
+ const relative = `.memory-bank/protocol/${source.protocol_id}/plan.json`;
477
+ const file = assertPortableArtifactRef(relative, { workspaceRoot: input.workspaceRoot, runHome: input.home, runId: path.basename(input.home) });
478
+ const current = readJson(file);
479
+ if (current.plan_id !== source.plan_id || current.protocol_id !== source.protocol_id)
480
+ invalid("PLAN correction cannot change protocol identity");
481
+ if (checksum(file) !== source.sha256) {
482
+ if (!Number.isSafeInteger(current.revision) || Number(current.revision) <= source.revision || planContentHash(current) === source.content_sha256)
483
+ invalid("A changed PLAN must advance its own revision and contain a real authored change");
484
+ changed.push(relative);
485
+ }
486
+ }
487
+ for (const map of input.maps) {
488
+ const file = assertPortableArtifactRef(`run://${path.basename(input.home)}/${map.path}`, { workspaceRoot: input.workspaceRoot, runHome: input.home, runId: path.basename(input.home) });
489
+ if (checksum(file) !== map.sha256)
490
+ changed.push(`RUN/${map.path}`);
491
+ }
492
+ const correction = input.decision.correction;
493
+ const actual = [...new Set(changed)].sort();
494
+ const named = correction.changed_paths.map(ref => ref.startsWith(`run://${path.basename(input.home)}/`) ? `RUN/${ref.slice(`run://${path.basename(input.home)}/`.length)}` : ref).sort();
495
+ if (new Set(named).size !== named.length || JSON.stringify(actual) !== JSON.stringify(named))
496
+ invalid("Correction changed_paths must exactly name the changed PLAN/aspect-map artifacts");
497
+ if (actual.length) {
498
+ if (correction.status !== "applied" || correction.previous_plan_revision !== input.reviewedPlanRevision)
499
+ invalid("An authored correction requires an applied receipt bound to the reviewed revision");
500
+ }
501
+ else if (correction.status !== "not_required" || input.decision.finding_decisions.some(item => item.decision === "accepted_fix"))
502
+ invalid("An accepted fix needs a real authored correction; unchanged rejected findings need not_required");
503
+ }
441
504
  function resolveMode(requested, groups, home, projectRoot) {
442
505
  if (requested !== "auto")
443
506
  return requested;
@@ -11,7 +11,7 @@ import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageSt
11
11
  import { validateSchema } from "./schema-validation.js";
12
12
  import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
13
13
  import { refreshRunSessionProjection } from "./run-projection.js";
14
- import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
14
+ import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
15
15
  import { nextWorkId, nextWorkIds } from "./ids.js";
16
16
  import { appendFlowRunTimelineEvent } from "./runs.js";
17
17
  import { resolveStageTransition } from "./execution-policy.js";
@@ -994,6 +994,8 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
994
994
  if (matrix) {
995
995
  add("run", matrix.metadata.json.path, matrix.metadata.json.sha256);
996
996
  add("run", matrix.metadata.markdown.path, matrix.metadata.markdown.sha256);
997
+ for (const source of matrix.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)))
998
+ add(source.root, source.path, source.sha256);
997
999
  }
998
1000
  for (const item of value.reviewer_inputs ?? [])
999
1001
  add("workspace", item.path, item.sha256);
@@ -1066,6 +1068,10 @@ async function settle(context, id, status, result, progress, retry) {
1066
1068
  // profile. Check it even when the Work has only raw focused checks, so a
1067
1069
  // worker cannot complete after downgrading the frozen project contract.
1068
1070
  readCodeCheckProfile(run.workspace_root);
1071
+ // A provider promises aliases even when their execution belongs to a
1072
+ // later aggregate gate. Validate materialization, without running them.
1073
+ for (const check of packet.provides_checks)
1074
+ validateCheckDeclaration(run.workspace_root, check);
1069
1075
  coordinationDrift = plannedAreaDrift(packet, result);
1070
1076
  const artifactDir = path.relative(requireRunHome(run), path.dirname(link.result_path));
1071
1077
  const requiredChecks = packet.repair?.verification_check_refs?.length
@@ -1250,7 +1256,8 @@ function renderWorkerPrompt(context, work, run, dependencies, commands) {
1250
1256
  const kind = parsePayload(work)?.kind;
1251
1257
  const matrixStage = kind === "plan-review" || kind === "code-review" ? kind : codePacket(work) ? "code" : null;
1252
1258
  const matrix = matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null;
1253
- return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
1259
+ const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
1260
+ return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
1254
1261
  }
1255
1262
  function workerLifecycleCommands(context, work, run) {
1256
1263
  const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.123",
3
+ "version": "0.9.0-beta.124",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",