@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81
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 +402 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1344 -376
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -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/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -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 +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -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 +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -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 +70 -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/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- 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 +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +575 -293
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +835 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +69 -28
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +701 -164
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,91 +1,133 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { withStageSettlement, prepareFlowRunStageAttachment } 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
18
|
import { writeStageReport } 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";
|
|
20
23
|
const stage = "plan-review";
|
|
21
24
|
const stageDir = vnextStageDirectory(stage);
|
|
22
25
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
23
|
-
const capacityProbeFanoutSize = capacityProbe.fanout_size;
|
|
24
|
-
const capacityProbeHoldSeconds = capacityProbe.probe_hold_seconds;
|
|
25
|
-
const capacityProbeDeadlineSeconds = capacityProbe.cleanup_deadline_seconds;
|
|
26
26
|
export function isVnextPlanReviewRun(context, input) {
|
|
27
27
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
28
28
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
29
29
|
}
|
|
30
|
-
export function
|
|
31
|
-
ensureWorkRegistry(context);
|
|
30
|
+
export function prepareVnextPlanReviewStart(context, input) {
|
|
32
31
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
33
32
|
const project = requireProjectByRoot(context, projectRoot);
|
|
34
33
|
const run = requireRun(context, projectRoot, input.runId);
|
|
35
34
|
const home = requireHome(run);
|
|
36
|
-
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
37
|
-
const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
|
|
38
35
|
const planRoot = path.join(home, "03-plan");
|
|
39
36
|
const root = path.join(home, stageDir);
|
|
40
37
|
const batch = path.join(planRoot, "code-work-batch.json");
|
|
38
|
+
const common = { projectRoot, project, run, home, planRoot, root, batch };
|
|
39
|
+
const priorStage = readIndex(run).stage_runs?.find((entry) => entry.stage === stage);
|
|
40
|
+
if (priorStage?.status === "done") {
|
|
41
|
+
const report = readJson(path.join(root, "stage-report.json"));
|
|
42
|
+
const outcome = report.semantic?.plan_review?.outcome;
|
|
43
|
+
if (outcome === "off" || outcome === "accepted")
|
|
44
|
+
return { ...common, state: "terminal", report, outcome };
|
|
45
|
+
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 });
|
|
46
|
+
}
|
|
47
|
+
const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
|
|
41
48
|
if (!fs.existsSync(path.join(planRoot, "stage-report.json")) || !fs.existsSync(batch))
|
|
42
|
-
throw new AppError("
|
|
49
|
+
throw new AppError("runtime_artifact_missing", "PLAN-REVIEW requires the accepted PLAN report and proposed CODE batch", 1);
|
|
43
50
|
const requested = requestedMode(run);
|
|
44
51
|
const protocolIds = protocolIdsForReview(home);
|
|
45
52
|
assertReviewWorkspace(run.workspace_root, protocolIds);
|
|
46
53
|
const groups = reviewGroups(home, run.workspace_root);
|
|
47
54
|
const effective = resolveMode(requested, groups, home, run.workspace_root);
|
|
48
55
|
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]);
|
|
56
|
+
const batchChecksum = checksum(batch), planChecksum = planSetChecksum(home, run.workspace_root);
|
|
57
|
+
if (input.externalContext)
|
|
58
|
+
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
59
|
+
prepareOutputFile(path.join(root, "stage-prompt.md"));
|
|
60
|
+
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum };
|
|
49
61
|
if (existing?.status === "running") {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
const orchestration = readFanoutDescriptor(root);
|
|
63
|
+
if (!orchestration)
|
|
64
|
+
prepareOutputFile(path.join(root, "fanout.json"));
|
|
65
|
+
return { ...active, state: "resumed", workId: existing.work_id, prompt: fs.readFileSync(path.join(root, "stage-prompt.md"), "utf8"), orchestration };
|
|
53
66
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const report = readJson(path.join(root, "stage-report.json"));
|
|
57
|
-
const outcome = report.semantic?.plan_review?.outcome;
|
|
58
|
-
if (outcome === "off" || outcome === "accepted")
|
|
59
|
-
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 };
|
|
60
|
-
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 });
|
|
61
|
-
}
|
|
62
|
-
fs.mkdirSync(root, { recursive: true });
|
|
63
|
-
const now = context.now();
|
|
64
|
-
const batchChecksum = checksum(batch);
|
|
65
|
-
const planChecksum = planSetChecksum(home, run.workspace_root);
|
|
67
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
68
|
+
const rootWork = rootWorkId(context, project.id, run.id);
|
|
66
69
|
if (effective === "off") {
|
|
67
70
|
const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIds });
|
|
68
71
|
if (failures.length)
|
|
69
|
-
throw new AppError("
|
|
70
|
-
|
|
72
|
+
throw new AppError("runtime_artifact_invalid", "Accepted PLAN artifacts have validation errors", 1, { errors: failures });
|
|
73
|
+
// This is retained runtime data, not a newly supplied --file argument.
|
|
74
|
+
const codeBatch = readJson(batch);
|
|
75
|
+
let codeItems;
|
|
76
|
+
try {
|
|
77
|
+
codeItems = prepareWorkBatch(context, { parentWorkId: rootWork, value: codeBatch });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof AppError && error.exitCode === 2)
|
|
81
|
+
throw new AppError("runtime_artifact_invalid", "Accepted CODE batch cannot be registered", 1, { file: batch, cause_code: error.code });
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
for (const name of ["stage-report.json", "stage-report.md", "stage-report.html"])
|
|
85
|
+
prepareOutputFile(path.join(root, name));
|
|
86
|
+
return { ...active, state: "off", rootWork, codeItems };
|
|
87
|
+
}
|
|
88
|
+
const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
|
|
89
|
+
const revision = currentPlanRevision(home, run.workspace_root);
|
|
90
|
+
const capacity = getStageFanoutCapacity(context, { projectRoot, runId: run.id, stage });
|
|
91
|
+
for (const name of ["work-context.json", "fanout.json"])
|
|
92
|
+
prepareOutputFile(path.join(root, name));
|
|
93
|
+
return { ...active, state: "fresh", rootWork, template, revision, capacity };
|
|
94
|
+
}
|
|
95
|
+
export function startVnextPlanReview(context, input, prepared = prepareVnextPlanReviewStart(context, input)) {
|
|
96
|
+
const { projectRoot, project, run, home, root, planRoot } = prepared;
|
|
97
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
98
|
+
if (prepared.state === "terminal") {
|
|
99
|
+
const { report, outcome } = prepared;
|
|
100
|
+
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 };
|
|
101
|
+
}
|
|
102
|
+
const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
|
|
103
|
+
if (prepared.state === "resumed") {
|
|
104
|
+
const promptPath = path.join(root, "stage-prompt.md");
|
|
105
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
106
|
+
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 } : {}) };
|
|
107
|
+
}
|
|
108
|
+
ensureWorkRegistry(context);
|
|
109
|
+
fs.mkdirSync(root, { recursive: true });
|
|
110
|
+
const now = context.now(), rootWork = prepared.rootWork;
|
|
111
|
+
if (prepared.state === "off") {
|
|
71
112
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
72
113
|
fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
|
|
73
114
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
74
115
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
75
116
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
return withStageSettlement(context, () => {
|
|
118
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems });
|
|
119
|
+
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
120
|
+
writeReport(root, report);
|
|
121
|
+
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
122
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
|
|
123
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
|
|
124
|
+
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
125
|
+
});
|
|
83
126
|
}
|
|
84
|
-
const rootWork = rootWorkId(context, project.id, run.id);
|
|
85
127
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
86
128
|
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]);
|
|
87
129
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
|
|
88
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand) });
|
|
130
|
+
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 });
|
|
89
131
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
90
132
|
fs.writeFileSync(promptPath, prompt);
|
|
91
133
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
@@ -103,7 +145,7 @@ export function startVnextPlanReview(context, input) {
|
|
|
103
145
|
const workSessionId = String(binding.work_session_id);
|
|
104
146
|
const sessionId = String(binding.session_id);
|
|
105
147
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
|
|
106
|
-
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:
|
|
148
|
+
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId }, 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 } });
|
|
107
149
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
108
150
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
109
151
|
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")) } };
|
|
@@ -126,7 +168,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
126
168
|
if (capacity.pending)
|
|
127
169
|
return capacity.response;
|
|
128
170
|
if (capacity.availableSlots < 1)
|
|
129
|
-
throw new AppError("
|
|
171
|
+
throw new AppError("subagent_capacity_unqualified", "PLAN-REVIEW requires one qualified native subagent slot", 1, { run_id: run.id });
|
|
130
172
|
const existing = context.db.all("SELECT work_id, task, status, result, created_at, launch_policy FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
131
173
|
const latest = (group) => existing.find((item) => item.task === group.task);
|
|
132
174
|
const pending = groups.filter((group) => !latest(group));
|
|
@@ -146,7 +188,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
146
188
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
147
189
|
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 })) });
|
|
148
190
|
const waves = plannedReviewWaves(groups, capacity.availableSlots);
|
|
149
|
-
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" };
|
|
191
|
+
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" };
|
|
150
192
|
}
|
|
151
193
|
// Capacity is a RUN fact shared by every fan-out stage.
|
|
152
194
|
const reviewCapacityKey = subagentCapacityKey;
|
|
@@ -162,16 +204,19 @@ export function recordVnextPlanReviewCapacity(context, input) {
|
|
|
162
204
|
const prior = variables.variables[reviewCapacityKey];
|
|
163
205
|
if (typeof prior === "number") {
|
|
164
206
|
if (prior === input.availableSlots)
|
|
165
|
-
return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "
|
|
207
|
+
return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "harness_qualification", reused: true } };
|
|
166
208
|
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
|
|
167
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots,
|
|
168
|
-
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "
|
|
209
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, source: "harness_qualification" });
|
|
210
|
+
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "harness_qualification", refreshed: true } };
|
|
169
211
|
}
|
|
170
212
|
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
|
|
171
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots,
|
|
172
|
-
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "
|
|
213
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, source: "harness_qualification" });
|
|
214
|
+
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
|
|
173
215
|
}
|
|
174
216
|
export function finishVnextPlanReview(context, input) {
|
|
217
|
+
return withStageSettlement(context, () => finishPlanReviewOwned(context, input));
|
|
218
|
+
}
|
|
219
|
+
function finishPlanReviewOwned(context, input) {
|
|
175
220
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
176
221
|
const project = requireProjectByRoot(context, projectRoot);
|
|
177
222
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -183,8 +228,9 @@ export function finishVnextPlanReview(context, input) {
|
|
|
183
228
|
if (!parent || parent.status !== "running")
|
|
184
229
|
throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
|
|
185
230
|
const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
186
|
-
const decision = readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
187
|
-
|
|
231
|
+
const decision = input.decision ?? readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
232
|
+
validateSchema({ schemaName: "plan-review-decision", file: decisionPath, data: decision, projectRoot, runId: run.id, runRoot: home, ddFlowHome: context.ddFlowHome });
|
|
233
|
+
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]);
|
|
188
234
|
const contextFile = readJson(path.join(root, "work-context.json"));
|
|
189
235
|
const groups = contextFile.groups ?? [];
|
|
190
236
|
const reviewedPlanRevision = contextFile.system?.plan_revision;
|
|
@@ -196,8 +242,7 @@ export function finishVnextPlanReview(context, input) {
|
|
|
196
242
|
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 });
|
|
197
243
|
}
|
|
198
244
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
199
|
-
preserveDecisionReceipt(root,
|
|
200
|
-
return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision });
|
|
245
|
+
return withStageSettlement(context, () => { preserveDecisionReceipt(root, decision, input.decisionText); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }); });
|
|
201
246
|
}
|
|
202
247
|
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
203
248
|
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 })) });
|
|
@@ -231,43 +276,47 @@ export function finishVnextPlanReview(context, input) {
|
|
|
231
276
|
else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision || currentPlanChecksum !== contextFile.system?.plan_checksum) {
|
|
232
277
|
throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2);
|
|
233
278
|
}
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors:
|
|
279
|
+
const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
|
|
280
|
+
if (prepared.failures.length)
|
|
281
|
+
throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures });
|
|
237
282
|
if (needsCorrection) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
throw new AppError("stale_code_work_batch", "Applied PLAN correction must regenerate the proposed CODE batch", 2, { batch, initial_batch_checksum: initialBatchChecksum });
|
|
283
|
+
// Regeneration above is authoritative. A valid PLAN correction need not
|
|
284
|
+
// change the derived CODE tasks (for example, a clarified explanation).
|
|
241
285
|
if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
|
|
242
286
|
throw new AppError("validation", "Generated CODE batch must not be listed as an agent-authored correction", 2, { changed_paths: decision.correction.changed_paths });
|
|
243
287
|
}
|
|
244
|
-
preserveDecisionReceipt(root, decisionPath);
|
|
245
288
|
const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
246
|
-
const finalBatchChecksum =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
289
|
+
const finalBatchChecksum = prepared.batchChecksum;
|
|
290
|
+
return withStageSettlement(context, () => {
|
|
291
|
+
prepared.publish();
|
|
292
|
+
preserveDecisionReceipt(root, decision, input.decisionText);
|
|
293
|
+
const registered = registerCode(context, project.id, run.id, batch);
|
|
294
|
+
const now = context.now();
|
|
295
|
+
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") });
|
|
296
|
+
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) });
|
|
297
|
+
applyReviewResults(home, latestChildren, decision);
|
|
298
|
+
writeReport(root, report);
|
|
299
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
300
|
+
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
301
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
|
|
302
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
|
|
303
|
+
return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
304
|
+
});
|
|
258
305
|
}
|
|
259
306
|
function preparedExisting(context, input) {
|
|
260
307
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
261
308
|
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 } : {}) });
|
|
262
|
-
const orchestration =
|
|
309
|
+
const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true });
|
|
263
310
|
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")) } };
|
|
264
311
|
}
|
|
265
312
|
function orchestratorPrompt(context, input) {
|
|
266
|
-
const template
|
|
313
|
+
const { template, revision, capacity } = input;
|
|
267
314
|
const decision = path.join(input.root, "decision.json");
|
|
268
|
-
const revision = currentPlanRevision(input.home, input.run.workspace_root);
|
|
269
315
|
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");
|
|
270
|
-
|
|
316
|
+
const reviewerLaunch = capacity.source === "external_policy"
|
|
317
|
+
? `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.`
|
|
318
|
+
: "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.";
|
|
319
|
+
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, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second 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");
|
|
271
320
|
}
|
|
272
321
|
function reviewGroups(home, workspaceRoot) {
|
|
273
322
|
const root = path.join(home, "03-plan");
|
|
@@ -327,15 +376,14 @@ function plannedReviewWaves(groups, capacity) {
|
|
|
327
376
|
}
|
|
328
377
|
return waves;
|
|
329
378
|
}
|
|
330
|
-
export function
|
|
331
|
-
return { ok: true, run_id: input.runId, stage, outcome: "
|
|
379
|
+
export function capacityQualificationResponse(context, input) {
|
|
380
|
+
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) } };
|
|
332
381
|
}
|
|
333
382
|
function ensureReviewCapacity(context, input) {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return { pending: true, response: capacityProbeResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
|
|
383
|
+
const capacity = getStageFanoutCapacity(context, { ...input, stage });
|
|
384
|
+
if (capacity.available_slots !== null)
|
|
385
|
+
return { pending: false, availableSlots: capacity.available_slots, source: capacity.source };
|
|
386
|
+
return { pending: true, response: capacityQualificationResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
|
|
339
387
|
}
|
|
340
388
|
function protocolIdsForReview(home) {
|
|
341
389
|
const report = readJson(path.join(home, "02-protocolize", "stage-report.json"));
|
|
@@ -385,10 +433,13 @@ catch {
|
|
|
385
433
|
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)
|
|
386
434
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
387
435
|
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
|
|
388
|
-
function readDecision(context, projectRoot, runId, runRoot, file) {
|
|
389
|
-
function preserveDecisionReceipt(root,
|
|
436
|
+
function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
|
|
437
|
+
function preserveDecisionReceipt(root, decision, preparedText) {
|
|
390
438
|
const receipt = path.join(root, "decision.receipt.json");
|
|
391
|
-
|
|
439
|
+
// The CLI has already parsed and validated this decision during prepare.
|
|
440
|
+
// Persist that immutable value rather than re-opening a caller-controlled
|
|
441
|
+
// filename after lifecycle admission.
|
|
442
|
+
const bytes = Buffer.from(preparedText ?? `${JSON.stringify(decision, null, 2)}\n`);
|
|
392
443
|
if (fs.existsSync(receipt)) {
|
|
393
444
|
if (!bytes.equals(fs.readFileSync(receipt)))
|
|
394
445
|
throw new AppError("decision_receipt_conflict", "The immutable PLAN-REVIEW decision receipt already contains a different payload", 2, { receipt });
|
|
@@ -426,8 +477,11 @@ function reviewerEvidenceFailures(context, input) {
|
|
|
426
477
|
continue;
|
|
427
478
|
}
|
|
428
479
|
const actual = result.aspects.map((item) => item.aspect_id);
|
|
429
|
-
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id))
|
|
480
|
+
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)))
|
|
430
481
|
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_coverage_invalid", expected_aspects: group.aspect_ids, actual_aspects: actual });
|
|
482
|
+
const missingEvidence = result.aspects.filter((item) => item.evidence_refs.length === 0).map((item) => item.aspect_id);
|
|
483
|
+
if (missingEvidence.length)
|
|
484
|
+
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_evidence_missing", missing_aspects: missingEvidence });
|
|
431
485
|
for (const ref of result.aspects.flatMap((item) => [...item.evidence_refs, ...item.findings.flatMap((finding) => finding.evidence_refs)])) {
|
|
432
486
|
try {
|
|
433
487
|
assertPortableArtifactRef(ref, { workspaceRoot: input.workspaceRoot, runHome: input.runHome, runId: input.runId });
|
|
@@ -454,15 +508,15 @@ function finishTerminalReview(context, input) {
|
|
|
454
508
|
const now = context.now();
|
|
455
509
|
const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
|
|
456
510
|
// A terminal decision is structured cancellation of the review subtree. It
|
|
457
|
-
// must settle
|
|
458
|
-
//
|
|
511
|
+
// must settle semantic reviewers before its parent Work can settle; otherwise
|
|
512
|
+
// the RUN contains abandoned active Work/Session links.
|
|
459
513
|
settleReviewChildren(context, { projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, reason: `PLAN-REVIEW terminal decision: ${input.decision.summary}`, now });
|
|
460
514
|
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") });
|
|
461
515
|
const rootWork = rootWorkId(context, input.projectId, input.run.id);
|
|
462
516
|
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]);
|
|
463
517
|
closeRunningWorkSession(context, rootWork, childStatus, now);
|
|
464
|
-
const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?
|
|
465
|
-
const report = reportFor({ run: input.run, mode: input.contextFile.system?.effective_mode ?? "standard", requested: input.contextFile.system?.requested_mode ?? "auto", outcome: input.decision.outcome, groups: input.contextFile.groups ?? [], batchChecksum: input.contextFile.system?.batch_checksum ?? checksum(input.batch), planChecksum: input.contextFile.system?.plan_checksum ?? planSetChecksum(requireHome(input.run), input.run.workspace_root), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
|
|
518
|
+
const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [input.projectId, input.run.id, input.parentWorkId]);
|
|
519
|
+
const report = reportFor(context, { run: input.run, mode: input.contextFile.system?.effective_mode ?? "standard", requested: input.contextFile.system?.requested_mode ?? "auto", outcome: input.decision.outcome, groups: input.contextFile.groups ?? [], batchChecksum: input.contextFile.system?.batch_checksum ?? checksum(input.batch), planChecksum: input.contextFile.system?.plan_checksum ?? planSetChecksum(requireHome(input.run), input.run.workspace_root), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
|
|
466
520
|
writeReport(input.root, report);
|
|
467
521
|
refreshRunWorkProjection(context, input.projectId, input.run.id);
|
|
468
522
|
const stageStatus = input.decision.outcome === "cancelled" ? "skipped" : input.decision.outcome === "failed" ? "failed" : "blocked";
|
|
@@ -481,7 +535,7 @@ function settleReviewChildren(context, input) {
|
|
|
481
535
|
function closeRunningWorkSession(context, workId, status, now) {
|
|
482
536
|
context.db.run("UPDATE work_sessions SET status = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [status, now, now, workId]);
|
|
483
537
|
}
|
|
484
|
-
function reportFor(input) {
|
|
538
|
+
function reportFor(context, input) {
|
|
485
539
|
const accepted = input.outcome === "accepted" || input.outcome === "off";
|
|
486
540
|
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}.`;
|
|
487
541
|
const decision = input.decision;
|
|
@@ -490,27 +544,27 @@ function reportFor(input) {
|
|
|
490
544
|
const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
|
|
491
545
|
const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
|
|
492
546
|
const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
|
|
493
|
-
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" } };
|
|
547
|
+
return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
|
|
494
548
|
}
|
|
495
549
|
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 }; }
|
|
496
550
|
function writeReport(root, report) {
|
|
497
551
|
writeStageReport(root, report);
|
|
498
552
|
}
|
|
499
|
-
function codeCommand(context, runId, projectRoot) { return `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json
|
|
553
|
+
function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
|
|
500
554
|
function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
501
|
-
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
|
|
555
|
+
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`); }
|
|
502
556
|
function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
503
557
|
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)
|
|
504
558
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
505
559
|
function requireHome(run) { if (!run.run_root)
|
|
506
560
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
507
561
|
function read(file) { if (!fs.existsSync(file))
|
|
508
|
-
throw new AppError("
|
|
562
|
+
throw new AppError("runtime_artifact_missing", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
|
|
509
563
|
function readJson(file) { try {
|
|
510
564
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
511
565
|
}
|
|
512
566
|
catch {
|
|
513
|
-
throw new AppError("
|
|
567
|
+
throw new AppError("runtime_artifact_invalid", "Expected valid retained JSON artifact", 1, { file });
|
|
514
568
|
} }
|
|
515
569
|
function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
516
570
|
function checksum(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|