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