@deksden-com/dd-flow-cli 0.9.0-beta.123 → 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 +26 -0
- package/README.md +33 -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/domain/stage-catalog.js +1 -1
- 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/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -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 +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- 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 +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- 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/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- 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 +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- 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 +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- 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/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- 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 +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- 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 +43 -6
- 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
|
@@ -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,8 +10,9 @@ 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
|
-
import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
15
|
+
import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
15
16
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
16
17
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
17
18
|
import { resolveStageTransition } from "./execution-policy.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)
|
|
@@ -994,6 +1017,8 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
|
|
|
994
1017
|
if (matrix) {
|
|
995
1018
|
add("run", matrix.metadata.json.path, matrix.metadata.json.sha256);
|
|
996
1019
|
add("run", matrix.metadata.markdown.path, matrix.metadata.markdown.sha256);
|
|
1020
|
+
for (const source of matrix.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)))
|
|
1021
|
+
add(source.root, source.path, source.sha256);
|
|
997
1022
|
}
|
|
998
1023
|
for (const item of value.reviewer_inputs ?? [])
|
|
999
1024
|
add("workspace", item.path, item.sha256);
|
|
@@ -1066,6 +1091,10 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1066
1091
|
// profile. Check it even when the Work has only raw focused checks, so a
|
|
1067
1092
|
// worker cannot complete after downgrading the frozen project contract.
|
|
1068
1093
|
readCodeCheckProfile(run.workspace_root);
|
|
1094
|
+
// A provider promises aliases even when their execution belongs to a
|
|
1095
|
+
// later aggregate gate. Validate materialization, without running them.
|
|
1096
|
+
for (const check of packet.provides_checks)
|
|
1097
|
+
validateCheckDeclaration(run.workspace_root, check);
|
|
1069
1098
|
coordinationDrift = plannedAreaDrift(packet, result);
|
|
1070
1099
|
const artifactDir = path.relative(requireRunHome(run), path.dirname(link.result_path));
|
|
1071
1100
|
const requiredChecks = packet.repair?.verification_check_refs?.length
|
|
@@ -1245,12 +1274,20 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
|
|
|
1245
1274
|
if (missing.length || unexpected.length || duplicate)
|
|
1246
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 });
|
|
1247
1276
|
}
|
|
1248
|
-
function renderWorkerPrompt(context, work, run, dependencies, commands) {
|
|
1249
|
-
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) : "";
|
|
1250
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)}`;
|
|
1251
1287
|
const matrixStage = kind === "plan-review" || kind === "code-review" ? kind : codePacket(work) ? "code" : null;
|
|
1252
1288
|
const matrix = matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null;
|
|
1253
|
-
|
|
1289
|
+
const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
|
|
1290
|
+
return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
|
|
1254
1291
|
}
|
|
1255
1292
|
function workerLifecycleCommands(context, work, run) {
|
|
1256
1293
|
const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
|
|
@@ -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
|
+
}
|