@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.
- package/CHANGELOG.md +10 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +1 -1
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +66 -16
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +39 -10
- package/dist/harness-runtime/lib/dd-codex.mjs +11 -25
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +9 -10
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -32
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +149 -48
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/external-work-launch.js +10 -3
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +10 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +8 -5
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller.js +42 -14
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +16 -4
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +81 -18
- package/dist/services/work-registry.js +9 -2
- 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@
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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");
|