@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.110
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 +520 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +388 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -1,22 +1,27 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
|
|
2
4
|
import fs from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { AppError } from "../shared/errors.js";
|
|
5
7
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
7
9
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunVariables, gitFacts, setRuntimeRunVariable } from "./runs.js";
|
|
8
|
-
import { addWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, workStartCommand } from "./work-registry.js";
|
|
10
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, workStartCommand } from "./work-registry.js";
|
|
9
11
|
import { validateSchema } from "./schema-validation.js";
|
|
10
12
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
11
13
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
|
-
import { validateVnextPlanArtifacts } from "./vnext-plan.js";
|
|
14
|
+
import { prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-plan.js";
|
|
13
15
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
16
|
import { nextWorkId } from "./ids.js";
|
|
15
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
16
|
-
import {
|
|
18
|
+
import { acceptStagePacket, acceptStageReport, prepareStagePacket, prepareStageReport } from "./stage-report-renderer.js";
|
|
17
19
|
import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
18
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
|
-
import {
|
|
20
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
|
+
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
|
+
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
20
25
|
const stage = "plan-review";
|
|
21
26
|
const stageDir = vnextStageDirectory(stage);
|
|
22
27
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -24,65 +29,111 @@ export function isVnextPlanReviewRun(context, input) {
|
|
|
24
29
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
25
30
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
26
31
|
}
|
|
27
|
-
export function
|
|
28
|
-
ensureWorkRegistry(context);
|
|
32
|
+
export function prepareVnextPlanReviewStart(context, input) {
|
|
29
33
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
30
34
|
const project = requireProjectByRoot(context, projectRoot);
|
|
31
35
|
const run = requireRun(context, projectRoot, input.runId);
|
|
32
36
|
const home = requireHome(run);
|
|
33
|
-
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
34
|
-
const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
|
|
35
37
|
const planRoot = path.join(home, "03-plan");
|
|
36
38
|
const root = path.join(home, stageDir);
|
|
37
39
|
const batch = path.join(planRoot, "code-work-batch.json");
|
|
40
|
+
const common = { projectRoot, project, run, home, planRoot, root, batch };
|
|
41
|
+
const priorStage = readIndex(run).stage_runs?.find((entry) => entry.stage === stage);
|
|
42
|
+
if (priorStage?.status === "done") {
|
|
43
|
+
const report = readJson(path.join(root, "stage-report.json"));
|
|
44
|
+
const outcome = report.semantic?.plan_review?.outcome;
|
|
45
|
+
if (outcome === "off" || outcome === "accepted")
|
|
46
|
+
return { ...common, state: "terminal", report, outcome };
|
|
47
|
+
throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
|
|
48
|
+
}
|
|
49
|
+
const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
|
|
38
50
|
if (!fs.existsSync(path.join(planRoot, "stage-report.json")) || !fs.existsSync(batch))
|
|
39
|
-
throw new AppError("
|
|
51
|
+
throw new AppError("runtime_artifact_missing", "PLAN-REVIEW requires the accepted PLAN report and proposed CODE batch", 1);
|
|
40
52
|
const requested = requestedMode(run);
|
|
41
53
|
const protocolIds = protocolIdsForReview(home);
|
|
42
54
|
assertReviewWorkspace(run.workspace_root, protocolIds);
|
|
43
55
|
const groups = reviewGroups(home, run.workspace_root);
|
|
44
56
|
const effective = resolveMode(requested, groups, home, run.workspace_root);
|
|
45
57
|
const existing = context.db.get("SELECT work_id, status FROM works WHERE project_id = ? AND run_id = ? AND task = ? ORDER BY created_at DESC LIMIT 1", [project.id, run.id, reviewTask]);
|
|
58
|
+
const batchChecksum = checksum(batch), planChecksum = planSetChecksum(home, run.workspace_root);
|
|
59
|
+
if (input.externalContext)
|
|
60
|
+
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
61
|
+
prepareOutputFile(path.join(root, "stage-prompt.md"));
|
|
62
|
+
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum };
|
|
46
63
|
if (existing?.status === "running") {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
const orchestration = readFanoutDescriptor(root);
|
|
65
|
+
if (!orchestration)
|
|
66
|
+
prepareOutputFile(path.join(root, "fanout.json"));
|
|
67
|
+
return { ...active, state: "resumed", workId: existing.work_id, prompt: fs.readFileSync(path.join(root, "stage-prompt.md"), "utf8"), orchestration };
|
|
50
68
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const report = readJson(path.join(root, "stage-report.json"));
|
|
54
|
-
const outcome = report.semantic?.plan_review?.outcome;
|
|
55
|
-
if (outcome === "off" || outcome === "accepted")
|
|
56
|
-
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 };
|
|
57
|
-
throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
|
|
58
|
-
}
|
|
59
|
-
fs.mkdirSync(root, { recursive: true });
|
|
60
|
-
const now = context.now();
|
|
61
|
-
const batchChecksum = checksum(batch);
|
|
62
|
-
const planChecksum = planSetChecksum(home, run.workspace_root);
|
|
69
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
70
|
+
const rootWork = rootWorkId(context, project.id, run.id);
|
|
63
71
|
if (effective === "off") {
|
|
64
72
|
const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIds });
|
|
65
73
|
if (failures.length)
|
|
66
|
-
throw new AppError("
|
|
67
|
-
|
|
74
|
+
throw new AppError("runtime_artifact_invalid", "Accepted PLAN artifacts have validation errors", 1, { errors: failures });
|
|
75
|
+
// This is retained runtime data, not a newly supplied --file argument.
|
|
76
|
+
const codeBatch = readJson(batch);
|
|
77
|
+
let codeItems;
|
|
78
|
+
try {
|
|
79
|
+
codeItems = prepareWorkBatch(context, { parentWorkId: rootWork, value: codeBatch });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof AppError && error.exitCode === 2)
|
|
83
|
+
throw new AppError("runtime_artifact_invalid", "Accepted CODE batch cannot be registered", 1, { file: batch, cause_code: error.code });
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
for (const name of ["stage-report.json", "stage-report.md", "stage-report.html"])
|
|
87
|
+
prepareOutputFile(path.join(root, name));
|
|
88
|
+
return { ...active, state: "off", rootWork, codeItems };
|
|
89
|
+
}
|
|
90
|
+
const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
|
|
91
|
+
const revision = currentPlanRevision(home, run.workspace_root);
|
|
92
|
+
const capacity = getStageFanoutCapacity(context, { projectRoot, runId: run.id, stage });
|
|
93
|
+
for (const name of ["work-context.json", "fanout.json"])
|
|
94
|
+
prepareOutputFile(path.join(root, name));
|
|
95
|
+
return { ...active, state: "fresh", rootWork, template, revision, capacity };
|
|
96
|
+
}
|
|
97
|
+
export function startVnextPlanReview(context, input, prepared = prepareVnextPlanReviewStart(context, input)) {
|
|
98
|
+
const { projectRoot, project, run, home, root, planRoot } = prepared;
|
|
99
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
100
|
+
if (prepared.state === "terminal") {
|
|
101
|
+
const { report, outcome } = prepared;
|
|
102
|
+
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 };
|
|
103
|
+
}
|
|
104
|
+
const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
|
|
105
|
+
if (prepared.state === "resumed") {
|
|
106
|
+
const promptPath = path.join(root, "stage-prompt.md");
|
|
107
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
108
|
+
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 } : {}) };
|
|
109
|
+
}
|
|
110
|
+
ensureWorkRegistry(context);
|
|
111
|
+
fs.mkdirSync(root, { recursive: true });
|
|
112
|
+
const now = context.now(), rootWork = prepared.rootWork;
|
|
113
|
+
if (prepared.state === "off") {
|
|
68
114
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
69
115
|
fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
|
|
70
116
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
71
117
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
72
118
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
const reportGit = gitFacts(run.workspace_root);
|
|
120
|
+
const nextCommand = codeCommand(context, run.id, projectRoot);
|
|
121
|
+
return withStageSettlement(context, () => {
|
|
122
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
|
|
123
|
+
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
|
|
124
|
+
const preparedReport = prepareStageReport(report);
|
|
125
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
126
|
+
const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
127
|
+
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
|
|
128
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
|
|
129
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
|
|
130
|
+
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: nextCommand } };
|
|
131
|
+
});
|
|
80
132
|
}
|
|
81
|
-
const rootWork = rootWorkId(context, project.id, run.id);
|
|
82
133
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
83
134
|
context.db.run("INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)", [workId, project.id, run.id, rootWork, reviewTask, now, now, now]);
|
|
84
135
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
|
|
85
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand) })
|
|
136
|
+
const prompt = `${orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity })}\n${stageLifecycleInstruction}\n`;
|
|
86
137
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
87
138
|
fs.writeFileSync(promptPath, prompt);
|
|
88
139
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
@@ -99,8 +150,8 @@ export function startVnextPlanReview(context, input) {
|
|
|
99
150
|
markReviewPending(home, groups);
|
|
100
151
|
const workSessionId = String(binding.work_session_id);
|
|
101
152
|
const sessionId = String(binding.session_id);
|
|
102
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
|
|
103
|
-
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision:
|
|
153
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
154
|
+
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", 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 } });
|
|
104
155
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
105
156
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
106
157
|
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")) } };
|
|
@@ -131,7 +182,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
131
182
|
const file = path.join(root, ".dispatch.json");
|
|
132
183
|
writeJson(file, { works: pending.map((group) => ({ key: group.key, task: group.task, depends_on: group.depends_on, launch_policy: "fresh_agent_required", result_schema: "dd-flow/plan-review-result@1", payload: { kind: "plan-review", read_only: true, group: { key: group.key, aspect_ids: group.aspect_ids } } })) });
|
|
133
184
|
try {
|
|
134
|
-
addWorkBatch(context, { parentWorkId: parent.work_id, file });
|
|
185
|
+
addWorkBatch(context, { parentWorkId: parent.work_id, file, stage });
|
|
135
186
|
}
|
|
136
187
|
finally {
|
|
137
188
|
fs.rmSync(file, { force: true });
|
|
@@ -143,7 +194,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
143
194
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
144
195
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_dispatched", work_id: parent.work_id, groups: reviews.map((item) => ({ group: item.group, work_id: item.work_id })) });
|
|
145
196
|
const waves = plannedReviewWaves(groups, capacity.availableSlots);
|
|
146
|
-
return { ok: true, run_id: run.id, stage, capacity: { available_slots: capacity.availableSlots }, routing: { group_count: groups.length, planned_waves: waves.length, waves, preference: "Use one wave when semantic compatibility permits; hard dependencies create later waves." }, review_works: reviews, next_action: "launch_ready_reviews" };
|
|
197
|
+
return { ok: true, run_id: run.id, stage, capacity: { available_slots: capacity.availableSlots, source: capacity.source }, routing: { group_count: groups.length, planned_waves: waves.length, waves, preference: "Use one wave when semantic compatibility permits; hard dependencies create later waves." }, review_works: reviews, next_action: "launch_ready_reviews" };
|
|
147
198
|
}
|
|
148
199
|
// Capacity is a RUN fact shared by every fan-out stage.
|
|
149
200
|
const reviewCapacityKey = subagentCapacityKey;
|
|
@@ -169,6 +220,9 @@ export function recordVnextPlanReviewCapacity(context, input) {
|
|
|
169
220
|
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
|
|
170
221
|
}
|
|
171
222
|
export function finishVnextPlanReview(context, input) {
|
|
223
|
+
return finishPlanReviewOwned(context, input);
|
|
224
|
+
}
|
|
225
|
+
function finishPlanReviewOwned(context, input) {
|
|
172
226
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
173
227
|
const project = requireProjectByRoot(context, projectRoot);
|
|
174
228
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -180,7 +234,8 @@ export function finishVnextPlanReview(context, input) {
|
|
|
180
234
|
if (!parent || parent.status !== "running")
|
|
181
235
|
throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
|
|
182
236
|
const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
183
|
-
const decision = readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
237
|
+
const decision = input.decision ?? readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
238
|
+
validateSchema({ schemaName: "plan-review-decision", file: decisionPath, data: decision, projectRoot, runId: run.id, runRoot: home, ddFlowHome: context.ddFlowHome });
|
|
184
239
|
const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
185
240
|
const contextFile = readJson(path.join(root, "work-context.json"));
|
|
186
241
|
const groups = contextFile.groups ?? [];
|
|
@@ -192,18 +247,25 @@ export function finishVnextPlanReview(context, input) {
|
|
|
192
247
|
if (decision.outcome === "blocked") {
|
|
193
248
|
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 });
|
|
194
249
|
}
|
|
250
|
+
if (children.some((child) => !["completed", "failed", "cancelled"].includes(child.status)))
|
|
251
|
+
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
195
252
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
196
|
-
|
|
197
|
-
|
|
253
|
+
const decisionReceipt = prepareDecisionReceipt(root, decision, input.decisionText);
|
|
254
|
+
const now = context.now();
|
|
255
|
+
const reportGit = gitFacts(run.workspace_root);
|
|
256
|
+
const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: decision.outcome, groups: contextFile.groups ?? [], batchChecksum: contextFile.system?.batch_checksum ?? checksum(batch), planChecksum: contextFile.system?.plan_checksum ?? planSetChecksum(home, run.workspace_root), code: {}, now, projectRoot, decision, children: latestReviewChildren(children), flow: flowCommand(context), git: reportGit });
|
|
257
|
+
const preparedReport = prepareStageReport(report);
|
|
258
|
+
const stageStatus = decision.outcome === "cancelled" ? "skipped" : "failed";
|
|
259
|
+
const completionInput = { projectRoot, runId: run.id, stage, status: stageStatus, data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
260
|
+
const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
|
|
261
|
+
return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, parentWorkId: parent.work_id, decision, now, decisionReceipt, preparedReport, completionInput, preparedCompletion }));
|
|
198
262
|
}
|
|
199
|
-
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
200
|
-
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
201
263
|
const latestChildren = latestReviewChildren(children);
|
|
202
264
|
const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision, workspaceRoot: run.workspace_root, runHome: home, runId: run.id });
|
|
203
265
|
if (evidenceFailures.length)
|
|
204
266
|
throw new AppError("review_evidence_invalid", "PLAN-REVIEW reviewer evidence is incomplete, stale, or not isolated", 2, { errors: evidenceFailures });
|
|
205
267
|
if (decision.outcome !== "accepted")
|
|
206
|
-
throw new AppError("validation", "Unsupported PLAN-REVIEW decision outcome", 2, { outcome: decision.outcome });
|
|
268
|
+
throw new AppError("validation", "Unsupported PLAN-REVIEW decision outcome", 2, { outcome: decision.outcome, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
207
269
|
// A completed reviewer can legitimately report `blocked` when an upstream
|
|
208
270
|
// plan uncertainty prevents it from approving its aspect. That is evidence
|
|
209
271
|
// for the coordinator to classify and fix in this one review pass, not an
|
|
@@ -221,50 +283,61 @@ export function finishVnextPlanReview(context, input) {
|
|
|
221
283
|
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 });
|
|
222
284
|
if (needsCorrection) {
|
|
223
285
|
if (decision.correction.status !== "applied")
|
|
224
|
-
throw new AppError("validation", "Reviewer findings require an in-place correction receipt", 2);
|
|
286
|
+
throw new AppError("validation", "Reviewer findings require an in-place correction receipt", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
|
|
225
287
|
if (decision.correction.previous_plan_revision !== reviewedPlanRevision || planRevision <= reviewedPlanRevision || decision.correction.changed_paths.length === 0)
|
|
226
|
-
throw new AppError("validation", "Applied PLAN correction must advance revision and list changed paths", 2, { reviewed_plan_revision: reviewedPlanRevision, final_plan_revision: planRevision });
|
|
288
|
+
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 });
|
|
227
289
|
}
|
|
228
290
|
else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision || currentPlanChecksum !== contextFile.system?.plan_checksum) {
|
|
229
|
-
throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2);
|
|
291
|
+
throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
|
|
230
292
|
}
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors:
|
|
293
|
+
const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
|
|
294
|
+
if (prepared.failures.length)
|
|
295
|
+
throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
234
296
|
if (needsCorrection) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
throw new AppError("stale_code_work_batch", "Applied PLAN correction must regenerate the proposed CODE batch", 2, { batch, initial_batch_checksum: initialBatchChecksum });
|
|
297
|
+
// Regeneration above is authoritative. A valid PLAN correction need not
|
|
298
|
+
// change the derived CODE tasks (for example, a clarified explanation).
|
|
238
299
|
if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
|
|
239
|
-
throw new AppError("validation", "Generated CODE batch must not be listed as an agent-authored correction", 2, { changed_paths: decision.correction.changed_paths });
|
|
300
|
+
throw new AppError("validation", "Generated CODE batch must not be listed as an agent-authored correction", 2, { changed_paths: decision.correction.changed_paths, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
240
301
|
}
|
|
241
|
-
preserveDecisionReceipt(root, decisionPath);
|
|
242
302
|
const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
243
|
-
const finalBatchChecksum =
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
303
|
+
const finalBatchChecksum = prepared.batchChecksum;
|
|
304
|
+
prepared.publish();
|
|
305
|
+
const decisionReceipt = prepareDecisionReceipt(root, decision, input.decisionText);
|
|
306
|
+
const codeParentId = rootWorkId(context, project.id, run.id);
|
|
307
|
+
const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
|
|
308
|
+
const reportGit = gitFacts(run.workspace_root);
|
|
309
|
+
const nextCommand = codeCommand(context, run.id, projectRoot);
|
|
310
|
+
return withStageSettlement(context, () => {
|
|
311
|
+
const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
|
|
312
|
+
const now = context.now();
|
|
313
|
+
settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
|
|
314
|
+
const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context), git: reportGit, nextCommand });
|
|
315
|
+
applyReviewResults(home, latestChildren, decision);
|
|
316
|
+
const preparedReport = prepareStageReport(report);
|
|
317
|
+
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage, prepared: decisionReceipt });
|
|
318
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
319
|
+
const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
320
|
+
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
|
|
321
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
322
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
|
|
323
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
|
|
324
|
+
return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: nextCommand } };
|
|
325
|
+
});
|
|
255
326
|
}
|
|
256
327
|
function preparedExisting(context, input) {
|
|
257
328
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
258
329
|
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 } : {}) });
|
|
259
|
-
const orchestration =
|
|
330
|
+
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) });
|
|
260
331
|
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")) } };
|
|
261
332
|
}
|
|
262
333
|
function orchestratorPrompt(context, input) {
|
|
263
|
-
const template
|
|
334
|
+
const { template, revision, capacity } = input;
|
|
264
335
|
const decision = path.join(input.root, "decision.json");
|
|
265
|
-
const revision = currentPlanRevision(input.home, input.run.workspace_root);
|
|
266
336
|
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");
|
|
267
|
-
|
|
337
|
+
const reviewerLaunch = capacity.source === "external_policy"
|
|
338
|
+
? `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.`
|
|
339
|
+
: "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.";
|
|
340
|
+
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");
|
|
268
341
|
}
|
|
269
342
|
function reviewGroups(home, workspaceRoot) {
|
|
270
343
|
const root = path.join(home, "03-plan");
|
|
@@ -285,7 +358,7 @@ function reviewGroups(home, workspaceRoot) {
|
|
|
285
358
|
if (dependencyGroup && dependencyGroup !== key)
|
|
286
359
|
dependencies.add(dependencyGroup);
|
|
287
360
|
}
|
|
288
|
-
groups.push({ key, protocol_id: protocolId, aspect_ids: group.aspect_ids, depends_on: [...dependencies].sort(), task: `Review PLAN group ${key}: ${group.aspect_ids.join(", ")}. Read run://${path.basename(home)}/03-plan/${protocolId}/aspect-map.json and the
|
|
361
|
+
groups.push({ key, protocol_id: protocolId, aspect_ids: group.aspect_ids, depends_on: [...dependencies].sort(), task: `Review PLAN group ${key}: ${group.aspect_ids.join(", ")}. Read run://${path.basename(home)}/03-plan/${protocolId}/aspect-map.json and .memory-bank/protocol/${protocolId}/plan.json under workspace_root. Cite the PLAN using that workspace-relative path, not a run:// ref. Try to falsify ambiguous behavior and missing verification. For every selected check that starts fixture/reset, service, or client processes, verify that they share the intended data/configuration world and that any runtime entrypoint requiring change is explicit in a Work task and verification and ordered before its consumer. planned_write_areas are coordination hints, not ownership. Do not edit plan or product files. Return one aspect entry for every assigned aspect, with exact evidence references and concise findings. Finish with the JSON contract supplied in your work prompt.` });
|
|
289
362
|
}
|
|
290
363
|
}
|
|
291
364
|
return groups;
|
|
@@ -328,10 +401,9 @@ export function capacityQualificationResponse(context, input) {
|
|
|
328
401
|
return { ok: true, run_id: input.runId, stage, outcome: "qualified_capacity_required", next_action: "qualify_harness_profile_then_record_capacity", next: { record_command: capacityRecordCommand(context, input.runId, input.projectRoot, "<qualified-native-child-count>"), retry_command: dispatchCommand(context, input.runId, input.projectRoot) } };
|
|
329
402
|
}
|
|
330
403
|
function ensureReviewCapacity(context, input) {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return { pending: false, availableSlots: known };
|
|
404
|
+
const capacity = getStageFanoutCapacity(context, { ...input, stage });
|
|
405
|
+
if (capacity.available_slots !== null)
|
|
406
|
+
return { pending: false, availableSlots: capacity.available_slots, source: capacity.source };
|
|
335
407
|
return { pending: true, response: capacityQualificationResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
|
|
336
408
|
}
|
|
337
409
|
function protocolIdsForReview(home) {
|
|
@@ -381,17 +453,18 @@ catch {
|
|
|
381
453
|
} }
|
|
382
454
|
function rootWorkId(context, projectId, runId) { const root = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!root)
|
|
383
455
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
384
|
-
function
|
|
385
|
-
function
|
|
386
|
-
function preserveDecisionReceipt(root, source) {
|
|
456
|
+
function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
|
|
457
|
+
function prepareDecisionReceipt(root, decision, preparedText) {
|
|
387
458
|
const receipt = path.join(root, "decision.receipt.json");
|
|
388
|
-
|
|
459
|
+
// The CLI has already parsed and validated this decision during prepare.
|
|
460
|
+
// Persist that immutable value rather than re-opening a caller-controlled
|
|
461
|
+
// filename after lifecycle admission.
|
|
462
|
+
const bytes = Buffer.from(preparedText ?? `${JSON.stringify(decision, null, 2)}\n`);
|
|
389
463
|
if (fs.existsSync(receipt)) {
|
|
390
464
|
if (!bytes.equals(fs.readFileSync(receipt)))
|
|
391
465
|
throw new AppError("decision_receipt_conflict", "The immutable PLAN-REVIEW decision receipt already contains a different payload", 2, { receipt });
|
|
392
|
-
return;
|
|
393
466
|
}
|
|
394
|
-
|
|
467
|
+
return prepareStagePacket(receipt, bytes);
|
|
395
468
|
}
|
|
396
469
|
function reviewerResult(result) { try {
|
|
397
470
|
const value = JSON.parse(result ?? "");
|
|
@@ -423,8 +496,11 @@ function reviewerEvidenceFailures(context, input) {
|
|
|
423
496
|
continue;
|
|
424
497
|
}
|
|
425
498
|
const actual = result.aspects.map((item) => item.aspect_id);
|
|
426
|
-
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id))
|
|
499
|
+
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)))
|
|
427
500
|
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_coverage_invalid", expected_aspects: group.aspect_ids, actual_aspects: actual });
|
|
501
|
+
const missingEvidence = result.aspects.filter((item) => item.evidence_refs.length === 0).map((item) => item.aspect_id);
|
|
502
|
+
if (missingEvidence.length)
|
|
503
|
+
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_evidence_missing", missing_aspects: missingEvidence });
|
|
428
504
|
for (const ref of result.aspects.flatMap((item) => [...item.evidence_refs, ...item.findings.flatMap((finding) => finding.evidence_refs)])) {
|
|
429
505
|
try {
|
|
430
506
|
assertPortableArtifactRef(ref, { workspaceRoot: input.workspaceRoot, runHome: input.runHome, runId: input.runId });
|
|
@@ -448,37 +524,24 @@ function settleReviewParent(context, input) {
|
|
|
448
524
|
context.db.run("UPDATE work_sessions SET status = ?, result_path = COALESCE(?, result_path), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, input.resultPath ?? null, now, now, workSession.id]);
|
|
449
525
|
}
|
|
450
526
|
function finishTerminalReview(context, input) {
|
|
451
|
-
const now =
|
|
527
|
+
const now = input.now;
|
|
452
528
|
const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
|
|
453
|
-
// A terminal decision is structured cancellation of the review subtree. It
|
|
454
|
-
// must settle semantic reviewers before its parent Work can settle; otherwise
|
|
455
|
-
// the RUN contains abandoned active Work/Session links.
|
|
456
|
-
settleReviewChildren(context, { projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, reason: `PLAN-REVIEW terminal decision: ${input.decision.summary}`, now });
|
|
457
529
|
settleReviewParent(context, { projectRoot: input.projectRoot, projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, result: input.decision.summary, reason: `plan_review_${input.decision.outcome}`, resultPath: path.join(input.root, "stage-report.json") });
|
|
458
530
|
const rootWork = rootWorkId(context, input.projectId, input.run.id);
|
|
459
531
|
context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [childStatus, input.decision.summary, now, now, rootWork]);
|
|
460
532
|
closeRunningWorkSession(context, rootWork, childStatus, now);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
533
|
+
acceptStagePacket(context, { projectId: input.projectId, runId: input.run.id, stage, prepared: input.decisionReceipt });
|
|
534
|
+
acceptStageReport(context, { projectId: input.projectId, runId: input.run.id, stage, root: input.root, prepared: input.preparedReport });
|
|
535
|
+
completeFlowRunStage(context, input.completionInput, input.preparedCompletion);
|
|
464
536
|
refreshRunWorkProjection(context, input.projectId, input.run.id);
|
|
465
|
-
const stageStatus = input.decision.outcome === "cancelled" ? "skipped" : input.decision.outcome === "failed" ? "failed" : "blocked";
|
|
466
|
-
completeFlowRunStage(context, { projectRoot: input.projectRoot, runId: input.run.id, stage, status: stageStatus, data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
467
537
|
completeFlowRun(context, { projectRoot: input.projectRoot, runId: input.run.id, status: input.decision.outcome === "cancelled" ? "cancelled" : input.decision.outcome === "failed" ? "failed" : "blocked", verdict: `plan_review_${input.decision.outcome}`, nextAction: "resolve_plan_review_blocker" });
|
|
468
538
|
appendFlowRunTimelineEvent(context, input.projectId, input.run.id, { type: "plan_review_terminal", outcome: input.decision.outcome, work_id: input.parentWorkId });
|
|
469
539
|
return { ok: true, run_id: input.run.id, stage, outcome: input.decision.outcome, next_action: "resolve_plan_review_blocker" };
|
|
470
540
|
}
|
|
471
|
-
function settleReviewChildren(context, input) {
|
|
472
|
-
const active = context.db.all("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND status IN ('created', 'running')", [input.projectId, input.runId, input.parentWorkId]);
|
|
473
|
-
for (const child of active) {
|
|
474
|
-
context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [input.status, input.reason, input.now, input.now, child.work_id]);
|
|
475
|
-
closeRunningWorkSession(context, child.work_id, input.status, input.now);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
541
|
function closeRunningWorkSession(context, workId, status, now) {
|
|
479
542
|
context.db.run("UPDATE work_sessions SET status = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [status, now, now, workId]);
|
|
480
543
|
}
|
|
481
|
-
function reportFor(input) {
|
|
544
|
+
function reportFor(context, input) {
|
|
482
545
|
const accepted = input.outcome === "accepted" || input.outcome === "off";
|
|
483
546
|
const result = input.outcome === "off" ? "Independent PLAN review was explicitly or automatically skipped." : accepted ? "Independent PLAN review accepted the plan." : `PLAN-REVIEW stopped with outcome: ${input.outcome}.`;
|
|
484
547
|
const decision = input.decision;
|
|
@@ -487,27 +550,24 @@ function reportFor(input) {
|
|
|
487
550
|
const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
|
|
488
551
|
const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
|
|
489
552
|
const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
|
|
490
|
-
return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json` } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
|
|
553
|
+
return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: input.git ?? gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: input.nextCommand ?? managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
|
|
491
554
|
}
|
|
492
555
|
function stageTiming(run, now) { const started = readIndex(run).stage_runs?.find((item) => item.stage === stage && typeof item.started_at === "string"); const startedAt = started?.started_at ?? now; const milliseconds = Date.parse(now) - Date.parse(startedAt); return { started_at: startedAt, wall_clock_ms: Number.isFinite(milliseconds) && milliseconds >= 0 ? milliseconds : null }; }
|
|
493
|
-
function
|
|
494
|
-
writeStageReport(root, report);
|
|
495
|
-
}
|
|
496
|
-
function codeCommand(context, runId, projectRoot) { return `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
556
|
+
function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
|
|
497
557
|
function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
498
|
-
function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json
|
|
558
|
+
function finishCommand(context, runId, projectRoot, decision) { return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json`); }
|
|
499
559
|
function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
500
560
|
function requireRun(context, projectRoot, runId) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]); if (!run)
|
|
501
561
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
502
562
|
function requireHome(run) { if (!run.run_root)
|
|
503
563
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
504
564
|
function read(file) { if (!fs.existsSync(file))
|
|
505
|
-
throw new AppError("
|
|
565
|
+
throw new AppError("runtime_artifact_missing", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
|
|
506
566
|
function readJson(file) { try {
|
|
507
567
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
508
568
|
}
|
|
509
569
|
catch {
|
|
510
|
-
throw new AppError("
|
|
570
|
+
throw new AppError("runtime_artifact_invalid", "Expected valid retained JSON artifact", 1, { file });
|
|
511
571
|
} }
|
|
512
572
|
function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
513
573
|
function checksum(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|