@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.75
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 +404 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +224 -216
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +605 -157
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { withStageSettlement } from "./runs.js";
|
|
2
4
|
import fs from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { AppError } from "../shared/errors.js";
|
|
5
7
|
import { effectiveCheckDeclarations, readCodeCheckProfile, validateCheckDeclaration, validateCheckPlacement, validateCodeCheckCommands } from "./code-checks.js";
|
|
6
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
-
import { resolveProjectRoot } from "../storage/paths.js";
|
|
9
|
+
import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
|
|
8
10
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
9
11
|
import { validateSchema } from "./schema-validation.js";
|
|
10
12
|
import { bindRunningWorkSession, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, validateWorkBatchFile } from "./work-registry.js";
|
|
@@ -50,7 +52,7 @@ export function startVnextPlan(context, input) {
|
|
|
50
52
|
if (!context.db.get("SELECT 1 FROM work_sessions WHERE work_id = ? LIMIT 1", [rootWork.work_id])) {
|
|
51
53
|
bindRunningWorkSession(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, promptPath: path.join(root, "fixture-root.prompt.md") });
|
|
52
54
|
}
|
|
53
|
-
context.db.
|
|
55
|
+
context.db.beginWriteTransaction();
|
|
54
56
|
let planWorkId;
|
|
55
57
|
try {
|
|
56
58
|
planWorkId = nextWorkId(context, project.id, "plan");
|
|
@@ -70,20 +72,20 @@ export function startVnextPlan(context, input) {
|
|
|
70
72
|
const identities = protocols.map((protocolId) => planIdentity(home, run.id, protocolId, owned.get(protocolId) ?? []));
|
|
71
73
|
planPaths.forEach((file, index) => ensurePlanSkeleton(file, protocols[index], identities[index]));
|
|
72
74
|
mapPaths.forEach((file, index) => ensureAspectMapSkeleton(file, protocols[index], identities[index], run.workspace_root));
|
|
73
|
-
const finishCommand = `${flowCommand(context)} stage finish ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json
|
|
75
|
+
const finishCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
74
76
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage: "plan", workId: planWorkId, projectRoot });
|
|
75
77
|
const pauseCommandTemplate = stagePauseCommandTemplate(pauseCommand);
|
|
76
78
|
const validationCommands = protocols.flatMap((_, index) => [
|
|
77
|
-
`${flowCommand(context)} schema validate --schema vnext-protocol-plan --file ${JSON.stringify(planPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --json`,
|
|
78
|
-
`${flowCommand(context)} schema validate --schema plan-aspect-map --file ${JSON.stringify(mapPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --json`
|
|
79
|
+
`${flowCommand(context)} schema validate --schema vnext-protocol-plan --file ${JSON.stringify(planPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --run ${run.id} --json`,
|
|
80
|
+
`${flowCommand(context)} schema validate --schema plan-aspect-map --file ${JSON.stringify(mapPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --run ${run.id} --json`
|
|
79
81
|
]);
|
|
80
82
|
const runVariables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
81
83
|
const measuredCapacity = runVariables.variables[subagentCapacityKey];
|
|
82
84
|
const mergeRequired = runEndsAtMerge(context, projectRoot, run.id);
|
|
83
85
|
const capacityContext = typeof measuredCapacity === "number" && Number.isInteger(measuredCapacity) && measuredCapacity >= 0
|
|
84
|
-
? `- The
|
|
85
|
-
: "- Reviewer capacity is not
|
|
86
|
-
const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch
|
|
86
|
+
? `- The qualified reviewer capacity is ${measuredCapacity}. This is a runtime fact for later PLAN-REVIEW dispatch; do not repeat qualification or invent a different value.`
|
|
87
|
+
: "- Reviewer capacity is not qualified yet. PLAN must not qualify or launch reviewers; an external harness controller supplies it before fan-out.";
|
|
88
|
+
const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch uses externally qualified capacity to schedule these semantic groups into waves; do not invent a capacity value here.";
|
|
87
89
|
const checkProfile = path.join(run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
88
90
|
const policyMergeAliases = codeCheckProfile?.mandatory_by_gate.merge ?? [];
|
|
89
91
|
const mergeContract = mergeRequired
|
|
@@ -91,10 +93,13 @@ export function startVnextPlan(context, input) {
|
|
|
91
93
|
? [`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.`]
|
|
92
94
|
: ["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>", ""]
|
|
93
95
|
: [];
|
|
94
|
-
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(gitFacts(run.workspace_root))}`, 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>", "", ...(fs.existsSync(checkProfile) ? ["<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.", fs.readFileSync(checkProfile, "utf8").trim(), "</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.", "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.", "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
|
|
96
|
+
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(gitFacts(run.workspace_root))}`, 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>", "", ...(fs.existsSync(checkProfile) ? ["<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.", fs.readFileSync(checkProfile, "utf8").trim(), "</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.", "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.", "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 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, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", 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");
|
|
95
97
|
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 };
|
|
96
98
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
97
|
-
|
|
99
|
+
// This explicit final rule supersedes historical pack wording: a flow gate
|
|
100
|
+
// has only executable autonomous evidence. Human or external confirmation
|
|
101
|
+
// is neither a check nor a permitted completion condition.
|
|
102
|
+
fs.writeFileSync(promptPath, `${prompt}\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`);
|
|
98
103
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
99
104
|
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));
|
|
100
105
|
const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
@@ -106,6 +111,9 @@ export function startVnextPlan(context, input) {
|
|
|
106
111
|
return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), prompt_path: promptPath, next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
|
|
107
112
|
}
|
|
108
113
|
export function finishVnextPlan(context, input) {
|
|
114
|
+
return withStageSettlement(context, () => finishVnextPlanOwned(context, input));
|
|
115
|
+
}
|
|
116
|
+
function finishVnextPlanOwned(context, input) {
|
|
109
117
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
110
118
|
const project = requireProjectByRoot(context, projectRoot);
|
|
111
119
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -133,16 +141,23 @@ export function finishVnextPlan(context, input) {
|
|
|
133
141
|
throw new AppError("trusted_session_binding_required", "PLAN finish requires its bound Agent WorkSession", 1, { work_id: work.work_id });
|
|
134
142
|
context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
|
|
135
143
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
136
|
-
const reviewCommand = `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json
|
|
144
|
+
const reviewCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
137
145
|
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: stageStartedAt(home, now), finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(stageStartedAt(home, now))), 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: reviewCommand }, 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" } };
|
|
138
146
|
const reportJson = writeStageReport(root, report).json;
|
|
139
|
-
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
147
|
+
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
140
148
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
141
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
|
|
149
|
+
advanceFlowRun(context, { settlementStage: "plan", projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
|
|
142
150
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
|
|
143
151
|
return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", next_command: reviewCommand, next: { kind: "start_stage", stage: "plan-review", command: reviewCommand } };
|
|
144
152
|
}
|
|
145
153
|
export function validateVnextPlanArtifacts(context, input) {
|
|
154
|
+
const prepared = prepareVnextPlanArtifacts(context, input);
|
|
155
|
+
if (!prepared.failures.length && input.publishBatch !== false)
|
|
156
|
+
prepared.publish();
|
|
157
|
+
return prepared.failures;
|
|
158
|
+
}
|
|
159
|
+
/** Validate in scratch files; publish only after the caller accepts its inputs. */
|
|
160
|
+
export function prepareVnextPlanArtifacts(context, input) {
|
|
146
161
|
const workspaceRoot = input.workspaceRoot ?? input.projectRoot;
|
|
147
162
|
const planFiles = input.protocols.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "plan.json"));
|
|
148
163
|
const mapFiles = input.protocols.map((id) => path.join(input.home, "03-plan", id, "aspect-map.json"));
|
|
@@ -155,11 +170,12 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
155
170
|
: []) : []) ?? [])
|
|
156
171
|
: undefined;
|
|
157
172
|
const failures = [];
|
|
173
|
+
const publications = new Map();
|
|
158
174
|
const plans = [];
|
|
159
175
|
for (const [index, file] of planFiles.entries()) {
|
|
160
176
|
const protocolId = input.protocols[index];
|
|
161
177
|
try {
|
|
162
|
-
validateSchema({ schemaName: "vnext-protocol-plan", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
|
|
178
|
+
validateSchema({ schemaName: "vnext-protocol-plan", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
163
179
|
const value = readPlan(file);
|
|
164
180
|
assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
|
|
165
181
|
validatePlanSemantics(file, new Set(ownership.get(protocolId) ?? []), obligations);
|
|
@@ -178,14 +194,22 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
for (const file of mapFiles) {
|
|
197
|
+
const temporary = `${file}.tmp-${crypto.randomUUID()}`;
|
|
181
198
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
const bytes = normalizedAspectMapRefs(file, workspaceRoot, input.home, input.runId);
|
|
200
|
+
if (input.publishBatch === false && bytes !== fs.readFileSync(file, "utf8"))
|
|
201
|
+
throw new AppError("validation", "Accepted aspect-map requires normalization; regenerate it in PLAN", 2, { file });
|
|
202
|
+
fs.writeFileSync(temporary, bytes);
|
|
203
|
+
validateSchema({ schemaName: "plan-aspect-map", file: temporary, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
204
|
+
validateAspectMap(temporary, input.protocols, workspaceRoot);
|
|
205
|
+
publications.set(file, bytes);
|
|
185
206
|
}
|
|
186
207
|
catch (error) {
|
|
187
208
|
failures.push(validationFailure(file, error));
|
|
188
209
|
}
|
|
210
|
+
finally {
|
|
211
|
+
fs.rmSync(temporary, { force: true });
|
|
212
|
+
}
|
|
189
213
|
}
|
|
190
214
|
if (!failures.length) {
|
|
191
215
|
try {
|
|
@@ -202,10 +226,10 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
202
226
|
const projection = projectCodeWorkBatch({ home: input.home, workspaceRoot, runId: input.runId, plans, protocols: input.protocols, ...(frozenDocumentBaselines ? { frozenDocumentBaselines } : {}) });
|
|
203
227
|
validateProjectedPaths(projection, workspaceRoot, input.home, input.runId);
|
|
204
228
|
fs.writeFileSync(temporaryBatch, `${JSON.stringify(projection, null, 2)}\n`);
|
|
205
|
-
validateSchema({ schemaName: "code-work-batch", file: temporaryBatch, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
|
|
229
|
+
validateSchema({ schemaName: "code-work-batch", file: temporaryBatch, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
206
230
|
validateWorkBatchFile(temporaryBatch);
|
|
207
231
|
if (input.publishBatch !== false) {
|
|
208
|
-
fs.
|
|
232
|
+
publications.set(batch, fs.readFileSync(temporaryBatch, "utf8"));
|
|
209
233
|
}
|
|
210
234
|
else if (!fs.existsSync(batch) || checksum(temporaryBatch) !== checksum(batch)) {
|
|
211
235
|
throw new AppError("stale_code_work_batch", "CODE batch no longer matches the accepted semantic PLAN", 2, { batch });
|
|
@@ -218,7 +242,29 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
218
242
|
fs.rmSync(temporaryBatch, { force: true });
|
|
219
243
|
}
|
|
220
244
|
}
|
|
221
|
-
return failures
|
|
245
|
+
return { failures, batchChecksum: publications.has(batch) ? crypto.createHash("sha256").update(publications.get(batch)).digest("hex") : null,
|
|
246
|
+
publish: () => {
|
|
247
|
+
if (failures.length)
|
|
248
|
+
throw new AppError("validation", "Cannot publish invalid PLAN artifacts", 2, { errors: failures });
|
|
249
|
+
const published = [];
|
|
250
|
+
try {
|
|
251
|
+
for (const [file, bytes] of publications) {
|
|
252
|
+
const temporary = `${file}.tmp-${crypto.randomUUID()}`;
|
|
253
|
+
try {
|
|
254
|
+
fs.writeFileSync(temporary, bytes);
|
|
255
|
+
fs.renameSync(temporary, file);
|
|
256
|
+
published.push(file);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
fs.rmSync(temporary, { force: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (cause) {
|
|
264
|
+
throw new AppError("plan_publication_failed", cause instanceof Error ? cause.message : String(cause), 1, { effect: "unknown", published_files: published, business_commit: false });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
222
268
|
}
|
|
223
269
|
/** CODE entry validates the same PLAN closure without changing an accepted batch. */
|
|
224
270
|
export function validateVnextCodeHandoff(context, input) {
|
|
@@ -231,10 +277,10 @@ export function validateVnextCodeHandoff(context, input) {
|
|
|
231
277
|
const planTask = "Produce accepted plan.json and aspect-map.json artifacts.";
|
|
232
278
|
function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@6", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], planned_write_areas: ["apps/api/src/"], stop_conditions: ["Stop if accepted scope conflicts with current truth."] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
|
|
233
279
|
function aspectMapExample(protocolId) { return { $schema: "plan-aspect-map.schema.json", schema_id: "dd-flow/plan-aspect-map@3", protocol_id: protocolId, plan_id: "PLAN-001", plan_revision: 1, catalog_ref: { path: ".memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects" }, routing: { initial_state: "orchestrator_local", selected_route: "local_compact", reason: "One genuinely small semantic unit.", groups: [] }, review_groups: [], aspects: [{ aspect_id: "example_aspect", applicability: "not_applicable", reason: "Only an example; use the supplied real catalog.", planned_artifact_refs: [] }] }; }
|
|
234
|
-
function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root,
|
|
280
|
+
function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
|
|
235
281
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
236
|
-
function requireHome(run) { if (!run.
|
|
237
|
-
throw new AppError("runtime_missing", "RUN
|
|
282
|
+
function requireHome(run) { if (!run.run_root)
|
|
283
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
238
284
|
function protocolIds(home) {
|
|
239
285
|
const report = JSON.parse(fs.readFileSync(path.join(home, "02-protocolize", "stage-report.json"), "utf8"));
|
|
240
286
|
const ids = report.semantic?.acceptance;
|
|
@@ -367,13 +413,16 @@ function assertPlanIdentity(value, identity, file) {
|
|
|
367
413
|
if (JSON.stringify(value.source_refs) !== JSON.stringify(identity.source_refs))
|
|
368
414
|
throw new AppError("validation", "PLAN must preserve CLI-owned source_refs", 2, { file });
|
|
369
415
|
}
|
|
370
|
-
function
|
|
371
|
-
|
|
416
|
+
function normalizedAspectMapRefs(file, projectRoot, runHome, runId) {
|
|
417
|
+
projectRoot = canonicalPath(projectRoot);
|
|
418
|
+
runHome = canonicalPath(runHome);
|
|
419
|
+
const bytes = fs.readFileSync(file, "utf8");
|
|
420
|
+
const map = JSON.parse(bytes);
|
|
372
421
|
let changed = false;
|
|
373
422
|
const normalize = (value) => {
|
|
374
423
|
if (!path.isAbsolute(value))
|
|
375
424
|
return value;
|
|
376
|
-
const source =
|
|
425
|
+
const source = canonicalPath(value, false);
|
|
377
426
|
const inProject = source.startsWith(`${projectRoot}${path.sep}`);
|
|
378
427
|
const inRun = source.startsWith(`${runHome}${path.sep}`);
|
|
379
428
|
if (!inProject && !inRun)
|
|
@@ -386,8 +435,7 @@ function normalizeAspectMapRefs(file, projectRoot, runHome, runId) {
|
|
|
386
435
|
for (const aspect of map.aspects ?? [])
|
|
387
436
|
if (Array.isArray(aspect.planned_artifact_refs))
|
|
388
437
|
aspect.planned_artifact_refs = aspect.planned_artifact_refs.map(normalize);
|
|
389
|
-
|
|
390
|
-
fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
|
|
438
|
+
return changed ? `${JSON.stringify(map, null, 2)}\n` : bytes;
|
|
391
439
|
}
|
|
392
440
|
function projectCodeWorkBatch(input) {
|
|
393
441
|
const obligationMap = acceptedObligationMap(input.home);
|
|
@@ -431,7 +479,7 @@ function projectCodeWorkBatch(input) {
|
|
|
431
479
|
provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
|
|
432
480
|
stop_conditions: item.execution_context.stop_conditions,
|
|
433
481
|
depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
|
|
434
|
-
result_schema: "dd-flow/code-work-result@
|
|
482
|
+
result_schema: "dd-flow/code-work-result@3"
|
|
435
483
|
});
|
|
436
484
|
}));
|
|
437
485
|
const byProtocol = new Map(input.plans.map((plan) => [plan.protocolId, plan.value.items]));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -7,6 +8,7 @@ import { previewNextEntityId } from "./ids.js";
|
|
|
7
8
|
import { registerProtocol } from "./protocols.js";
|
|
8
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
10
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
11
|
+
import { resolveStageTransition } from "./execution-policy.js";
|
|
10
12
|
import { validateSchema } from "./schema-validation.js";
|
|
11
13
|
import { assertStageStartHookEvent, hookSessionIdentity } from "./hooks.js";
|
|
12
14
|
import { bindStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
@@ -21,9 +23,15 @@ const stage = "protocolize";
|
|
|
21
23
|
function stageSessionMode(context, run) {
|
|
22
24
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
|
|
23
25
|
const value = row ? JSON.parse(row.index_json) : null;
|
|
24
|
-
const
|
|
26
|
+
const profile = value?.execution_profile;
|
|
27
|
+
const mode = profile?.settings.stage_session_mode;
|
|
25
28
|
if (mode !== "same_session" && mode !== "new_session")
|
|
26
29
|
throw new AppError("execution_profile_invalid", "RUN is missing its frozen stage session mode", 1, { run_id: run.id });
|
|
30
|
+
if (profile?.settings.execution) {
|
|
31
|
+
if (!profile.agent_profiles)
|
|
32
|
+
throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
|
|
33
|
+
return resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, fromStage: "protocolize", stage: "plan", stageSessionMode: mode, mergeMode: profile.settings.merge_mode }).session_mode;
|
|
34
|
+
}
|
|
27
35
|
return mode;
|
|
28
36
|
}
|
|
29
37
|
export function isVnextProtocolizeRun(context, input) {
|
|
@@ -54,13 +62,13 @@ export function prepareVnextProtocolize(context, input) {
|
|
|
54
62
|
if (!fs.existsSync(templatePath))
|
|
55
63
|
throw new AppError("not_found", "vNext PROTOCOLIZE prompt template is missing", 1, { path: templatePath });
|
|
56
64
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId: work.work_id, projectRoot });
|
|
57
|
-
const prompt = renderPrompt({ projectRoot, runId: run.id, workId: work.work_id, specifyPath, obligations, resultPath, handoff: input.handoff, catalog: catalog(projectRoot), policy, template: fs.readFileSync(templatePath, "utf8"), pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand),
|
|
65
|
+
const prompt = renderPrompt({ projectRoot, runId: run.id, workId: work.work_id, specifyPath, obligations, resultPath, handoff: input.handoff, catalog: catalog(projectRoot), policy, template: fs.readFileSync(templatePath, "utf8"), 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`) });
|
|
58
66
|
fs.writeFileSync(promptPath, prompt);
|
|
59
67
|
// Preparation is deliberately non-productive: the standalone stage-start
|
|
60
68
|
// command atomically installs external context, binds the worker and opens
|
|
61
69
|
// the lifecycle Stage. Do not make this prompt an implicit running stage.
|
|
62
70
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_prepared", work_id: work.work_id, stage, handoff_mode: input.handoff.stage_handoff.effective });
|
|
63
|
-
const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: prompt } : { kind: "await_new_session", stage, work_id: work.work_id, prompt_path: promptPath, resume_command: `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json` };
|
|
71
|
+
const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: prompt } : { 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`) };
|
|
64
72
|
return { directive, prompt_path: promptPath };
|
|
65
73
|
}
|
|
66
74
|
export function startVnextProtocolize(context, input) {
|
|
@@ -77,7 +85,6 @@ export function startVnextProtocolize(context, input) {
|
|
|
77
85
|
const handoff = specifyHandoff(path.join(requireRunHome(run), "01-specify", "work-context.json"), work.work_id);
|
|
78
86
|
prepareVnextProtocolize(context, { projectRoot, runId: run.id, workId: work.work_id, handoff });
|
|
79
87
|
}
|
|
80
|
-
const handoff = JSON.parse(fs.readFileSync(contextPath, "utf8"));
|
|
81
88
|
const frozenContext = JSON.parse(fs.readFileSync(contextPath, "utf8"));
|
|
82
89
|
const previous = context.db.get("SELECT ws.session_id, s.stop_reason FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, work.work_id]);
|
|
83
90
|
const activeWorkSession = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
@@ -91,7 +98,7 @@ export function startVnextProtocolize(context, input) {
|
|
|
91
98
|
return false;
|
|
92
99
|
}
|
|
93
100
|
})();
|
|
94
|
-
const mode =
|
|
101
|
+
const mode = frozenContext.system?.handoff?.stage_handoff.effective;
|
|
95
102
|
const promptPath = path.join(protocolizeRoot, "prompt.md");
|
|
96
103
|
const externalContext = applyExternalStageContext({ stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
97
104
|
const sessionId = hookSessionIdentity(context, project.id, input.hookEventId).sessionId;
|
|
@@ -135,7 +142,7 @@ export function finishVnextProtocolize(context, input) {
|
|
|
135
142
|
const root = path.join(requireRunHome(run), "02-protocolize");
|
|
136
143
|
const resultFile = path.resolve(input.resultFile);
|
|
137
144
|
inside(root, resultFile);
|
|
138
|
-
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
145
|
+
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
139
146
|
const result = readResult(resultFile);
|
|
140
147
|
if (result.outcome !== "protocolized")
|
|
141
148
|
throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
|
|
@@ -162,12 +169,12 @@ export function finishVnextProtocolize(context, input) {
|
|
|
162
169
|
const reportMarkdown = path.join(root, "stage-report.md");
|
|
163
170
|
const reportHtml = path.join(root, "stage-report.html");
|
|
164
171
|
writeStageReport(root, report);
|
|
165
|
-
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
172
|
+
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
166
173
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "protocolize-result.json", dataSchemaId: "dd-flow/vnext-protocolize-result@3", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
167
174
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "protocol_documents_materialized", work_id: work.work_id, protocol_ids: protocolIds, stage });
|
|
168
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
175
|
+
advanceFlowRun(context, { settlementStage: "protocolize", projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
169
176
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
170
|
-
const nextCommand = `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json
|
|
177
|
+
const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
171
178
|
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", next_command: nextCommand, next: { kind: "start_stage", stage: "plan", command: nextCommand, cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode }, 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")) } };
|
|
172
179
|
}
|
|
173
180
|
function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
@@ -318,7 +325,7 @@ function readResult(file) {
|
|
|
318
325
|
function acceptedSpecifyObligations(context, projectRoot, runId, specifyPath) {
|
|
319
326
|
if (!fs.existsSync(specifyPath))
|
|
320
327
|
throw new AppError("not_found", "PROTOCOLIZE requires accepted specify.json", 1, { path: specifyPath });
|
|
321
|
-
validateSchema({ schemaName: "vnext-specify", file: specifyPath, projectRoot, ddFlowHome: context.ddFlowHome, runId });
|
|
328
|
+
validateSchema({ schemaName: "vnext-specify", file: specifyPath, projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: requireRunHome(requireRun(context, projectRoot, runId)) });
|
|
322
329
|
const specify = readVnextSpecifyResult(specifyPath);
|
|
323
330
|
return [
|
|
324
331
|
...specify.requirements.map((obligation) => ({ ...obligation, kind: "requirement" })),
|
|
@@ -435,7 +442,7 @@ function resultTemplate(obligations = []) {
|
|
|
435
442
|
}
|
|
436
443
|
function renderPrompt(input) {
|
|
437
444
|
const obligationList = input.obligations.map((obligation) => `- ${obligation.id}: ${obligation.statement}`).join("\n");
|
|
438
|
-
return ["<work_context>", `- RUN: ${input.runId}`, `- Work: ${input.workId}`, `- Accepted SPECIFY JSON: ${input.specifyPath}`, `- Handoff: ${input.handoff.stage_handoff.effective}`, "</work_context>", "", "<accepted_obligations>", obligationList, "</accepted_obligations>", "", "<frozen_git_policy>", `- route: ${input.policy.route}`, `- integration branch: ${input.policy.integration_branch ?? "not applicable"}`, `- base ref: ${input.policy.base_ref ?? "not applicable"}`, `- feature branch: ${input.policy.feature_branch ?? "not applicable"}`, `- service worktree: ${input.policy.worktree_path ?? "not applicable"}`, `- bootstrap: ${input.policy.bootstrap_status}`, `- decision: ${input.policy.reason}`, "The CLI already created and bootstrapped this workspace at PROTOCOLIZE start. Do not create branches, worktrees, protocol files or Git commits yourself. This transition Work may run from the stable session; edit only the supplied RUN result file.", "</frozen_git_policy>", "", "<catalog>", input.catalog.length ? input.catalog.map((x) => `- ${x}`).join("\n") : "- inactive", "</catalog>", "", "<stage_instructions>", input.template.trim(), "</stage_instructions>", "", "<output_contract>", `Edit only ${input.resultPath}. Keep this exact JSON shape; CLI allocates ids and writes PRT/PSET/feature documents in the already provisioned workspace:\n\n\`\`\`json\n${JSON.stringify(resultTemplate(), null, 2)}\n\`\`\``, "Allocate every supplied R-* and AC-* exactly once in obligation_ownership. The CLI rejects unknown, duplicate or missing obligations, and every member must own at least one AC-*.", "If PROTOCOLIZE itself exposes a material user decision with no reasonable default, do not finish and do not return to SPECIFY. Run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message and stop. Resume this same PROTOCOLIZE Work using the exact command returned by pause.", `When the answer is incorporated and the result is protocolized, finish:\n\n${input.
|
|
445
|
+
return ["<work_context>", `- RUN: ${input.runId}`, `- Work: ${input.workId}`, `- Accepted SPECIFY JSON: ${input.specifyPath}`, `- Handoff: ${input.handoff.stage_handoff.effective}`, "</work_context>", "", "<accepted_obligations>", obligationList, "</accepted_obligations>", "", "<frozen_git_policy>", `- route: ${input.policy.route}`, `- integration branch: ${input.policy.integration_branch ?? "not applicable"}`, `- base ref: ${input.policy.base_ref ?? "not applicable"}`, `- feature branch: ${input.policy.feature_branch ?? "not applicable"}`, `- service worktree: ${input.policy.worktree_path ?? "not applicable"}`, `- bootstrap: ${input.policy.bootstrap_status}`, `- decision: ${input.policy.reason}`, "The CLI already created and bootstrapped this workspace at PROTOCOLIZE start. Do not create branches, worktrees, protocol files or Git commits yourself. This transition Work may run from the stable session; edit only the supplied RUN result file.", "</frozen_git_policy>", "", "<catalog>", input.catalog.length ? input.catalog.map((x) => `- ${x}`).join("\n") : "- inactive", "</catalog>", "", "<stage_instructions>", input.template.trim(), "</stage_instructions>", "", "<output_contract>", `Edit only ${input.resultPath}. Keep this exact JSON shape; CLI allocates ids and writes PRT/PSET/feature documents in the already provisioned workspace:\n\n\`\`\`json\n${JSON.stringify(resultTemplate(), null, 2)}\n\`\`\``, "Allocate every supplied R-* and AC-* exactly once in obligation_ownership. The CLI rejects unknown, duplicate or missing obligations, and every member must own at least one AC-*.", "If PROTOCOLIZE itself exposes a material user decision with no reasonable default, do not finish and do not return to SPECIFY. Run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message and stop. Resume this same PROTOCOLIZE Work using the exact command returned by pause.", `When the answer is incorporated and the result is protocolized, finish:\n\n${input.finishCommand}`, "</output_contract>", ""].join("\n");
|
|
439
446
|
}
|
|
440
447
|
function provisionWorkspaceForProtocolize(context, projectRoot, projectId, run, stageRoot) {
|
|
441
448
|
const receiptPath = path.join(stageRoot, "workspace-route.json");
|
|
@@ -498,10 +505,10 @@ function featureIndex(epicRoot, featureSlug) {
|
|
|
498
505
|
}
|
|
499
506
|
function requireWork(context, id) { const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]); if (!work)
|
|
500
507
|
throw new AppError("not_found", "Work is not registered", 1, { work_id: id }); return work; }
|
|
501
|
-
function requireRun(context, projectRoot, id) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, short_id, slug, project_id, workspace_root,
|
|
508
|
+
function requireRun(context, projectRoot, id) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, short_id, slug, project_id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
|
|
502
509
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: id }); return run; }
|
|
503
|
-
function requireRunHome(run) { if (!run.
|
|
504
|
-
throw new AppError("runtime_missing", "RUN
|
|
510
|
+
function requireRunHome(run) { if (!run.run_root)
|
|
511
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
505
512
|
function inside(root, file) { const relative = path.relative(root, file); if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
506
513
|
throw new AppError("path_escape", "Result must be inside the protocolize workspace", 2); }
|
|
507
514
|
function writeJson(file, value) { const tmp = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(tmp, file); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -9,6 +10,7 @@ import { resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
|
|
|
9
10
|
import { preflightMemoryPermissions } from "./memory-permissions.js";
|
|
10
11
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
11
12
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
13
|
+
import { resolveStageTransition } from "./execution-policy.js";
|
|
12
14
|
import { validateSchema } from "./schema-validation.js";
|
|
13
15
|
import { bindRunningWorkSession, refreshRunWorkProjection } from "./work-registry.js";
|
|
14
16
|
import { nextWorkId } from "./ids.js";
|
|
@@ -65,7 +67,7 @@ export function launchVnextSpecify(context, input) {
|
|
|
65
67
|
if (preflight.ok !== true)
|
|
66
68
|
throw new AppError("permission_preflight_failed", "SPECIFY workspace is not writable", 1, { preflight });
|
|
67
69
|
const grounding = projectGrounding(projectRoot);
|
|
68
|
-
const handoff = executionSnapshot(context, run.id);
|
|
70
|
+
const handoff = executionSnapshot(context, run.project_id, run.id);
|
|
69
71
|
const workContext = {
|
|
70
72
|
schema_id: "dd-flow/work-context@1",
|
|
71
73
|
system: { run_id: run.id, work_id: workId, stage: stageId },
|
|
@@ -82,7 +84,7 @@ export function launchVnextSpecify(context, input) {
|
|
|
82
84
|
};
|
|
83
85
|
writeJson(contextPath, workContext);
|
|
84
86
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage: stageId, workId, projectRoot });
|
|
85
|
-
const prompt = renderPrompt({ workId, runId: run.id, projectRoot, stageRoot, resultPath, discussion, template, workContext, grounding, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand),
|
|
87
|
+
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`) });
|
|
86
88
|
fs.writeFileSync(promptPath, prompt);
|
|
87
89
|
const externalContext = applyExternalStageContext({ stageRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
88
90
|
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)
|
|
@@ -126,7 +128,9 @@ export function submitVnextSpecify(context, input) {
|
|
|
126
128
|
if (work.project_id !== project.id)
|
|
127
129
|
throw new AppError("project_mismatch", "Work does not belong to --project-root", 1, { work_id: work.work_id });
|
|
128
130
|
if (work.status !== "running") {
|
|
129
|
-
|
|
131
|
+
// This check precedes all result reads and mutations. A later legal state
|
|
132
|
+
// transition may make a new invocation appropriate, but never replays this one.
|
|
133
|
+
throw new AppError("invalid_work_state", "Work is not waiting for a SPECIFY result", 1, { work_id: work.work_id, status: work.status, retryable_no_effect: true });
|
|
130
134
|
}
|
|
131
135
|
const run = requireRun(context, projectRoot, work.run_id);
|
|
132
136
|
if (runStatus(context, project.id, run.id) !== "running") {
|
|
@@ -141,7 +145,7 @@ export function submitVnextSpecify(context, input) {
|
|
|
141
145
|
throw new AppError("not_found", "--result-file must point to an existing file inside the SPECIFY workspace", 1, { result_file: input.resultFile });
|
|
142
146
|
}
|
|
143
147
|
try {
|
|
144
|
-
validateSchema({ schemaName: "vnext-specify", file: candidateFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
148
|
+
validateSchema({ schemaName: "vnext-specify", file: candidateFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
|
|
145
149
|
const result = readVnextSpecifyResult(candidateFile);
|
|
146
150
|
validateObligations(result, candidateFile);
|
|
147
151
|
const normalizedResult = `${JSON.stringify(result, null, 2)}\n`;
|
|
@@ -169,7 +173,7 @@ export function submitVnextSpecify(context, input) {
|
|
|
169
173
|
const htmlPath = path.join(stageRoot, "stage-report.html");
|
|
170
174
|
const report = buildStageReport({ run, work, workSession, outcome, result, resultMarkdown: renderedMarkdown, now, stageRoot, resultFile });
|
|
171
175
|
writeStageReport(stageRoot, report);
|
|
172
|
-
validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
176
|
+
validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
|
|
173
177
|
completeFlowRunStage(context, {
|
|
174
178
|
projectRoot,
|
|
175
179
|
runId: run.id,
|
|
@@ -187,9 +191,9 @@ export function submitVnextSpecify(context, input) {
|
|
|
187
191
|
appendFlowRunTimelineEvent(context, run.project_id, run.id, { type: "work_completed", work_id: work.work_id, outcome, stage: stageId });
|
|
188
192
|
if (continuesToProtocolize) {
|
|
189
193
|
const nextWork = requireWork(context, work.work_id);
|
|
190
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
|
|
194
|
+
advanceFlowRun(context, { settlementStage: "specify", projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
|
|
191
195
|
refreshRunWorkProjection(context, run.project_id, run.id);
|
|
192
|
-
const nextCommand = `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json
|
|
196
|
+
const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
193
197
|
return {
|
|
194
198
|
ok: true,
|
|
195
199
|
schema_id: "dd-flow/vnext-work-submit@1",
|
|
@@ -268,7 +272,7 @@ function readIntake(input) {
|
|
|
268
272
|
}
|
|
269
273
|
function requireRun(context, projectRoot, runId) {
|
|
270
274
|
const project = requireProjectByRoot(context, projectRoot);
|
|
271
|
-
const run = context.db.get("SELECT id, project_id, project_root, workspace_root,
|
|
275
|
+
const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);
|
|
272
276
|
if (!run)
|
|
273
277
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
274
278
|
return run;
|
|
@@ -280,9 +284,9 @@ function runStatus(context, projectId, runId) {
|
|
|
280
284
|
return run.status;
|
|
281
285
|
}
|
|
282
286
|
function requiredRunHome(run) {
|
|
283
|
-
if (!run.
|
|
284
|
-
throw new AppError("runtime_missing", "RUN has no portable
|
|
285
|
-
return run.
|
|
287
|
+
if (!run.run_root)
|
|
288
|
+
throw new AppError("runtime_missing", "RUN has no portable artifact root", 1, { run_id: run.id });
|
|
289
|
+
return run.run_root;
|
|
286
290
|
}
|
|
287
291
|
function requireWork(context, workId) {
|
|
288
292
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [workId]);
|
|
@@ -397,8 +401,8 @@ function renderPrompt(input) {
|
|
|
397
401
|
input.pauseCommandTemplate,
|
|
398
402
|
"```",
|
|
399
403
|
"The response tells you exactly what to ask and how to resume this same stage. Ask its user_message and stop the Turn.",
|
|
400
|
-
`Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.
|
|
401
|
-
"Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. If finish reports a validation error, correct the same result file and
|
|
404
|
+
`Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.finishCommand}\`.`,
|
|
405
|
+
"Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. If finish reports a validation error, correct the same result file and use its returned retry_command in this Session; only if no invocation-id was issued may you rerun the original command. After success, follow only its returned next directive; a focused eval controller may explicitly stop at this stage boundary.",
|
|
402
406
|
"</output_contract>",
|
|
403
407
|
""
|
|
404
408
|
].join("\n");
|
|
@@ -562,12 +566,18 @@ export function readVnextFlowDefinition(projectRoot) {
|
|
|
562
566
|
}
|
|
563
567
|
return null;
|
|
564
568
|
}
|
|
565
|
-
function executionSnapshot(context, runId) {
|
|
566
|
-
const row = context.db.get("SELECT index_json FROM runs WHERE id = ?", [runId]);
|
|
569
|
+
function executionSnapshot(context, projectId, runId) {
|
|
570
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
567
571
|
const snapshot = row ? JSON.parse(row.index_json) : null;
|
|
568
|
-
const
|
|
572
|
+
const profile = snapshot?.execution_profile;
|
|
573
|
+
let handoff = profile?.settings.stage_session_mode;
|
|
569
574
|
if (handoff !== "same_session" && handoff !== "new_session")
|
|
570
575
|
throw new AppError("execution_profile_invalid", "RUN has no frozen stage session mode", 1, { run_id: runId });
|
|
576
|
+
if (profile?.settings.execution) {
|
|
577
|
+
if (!profile.agent_profiles)
|
|
578
|
+
throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
|
|
579
|
+
handoff = resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, fromStage: "specify", stage: "protocolize", stageSessionMode: handoff, mergeMode: profile.settings.merge_mode }).session_mode;
|
|
580
|
+
}
|
|
571
581
|
return { stage_handoff: { effective: handoff, source: "run_override" } };
|
|
572
582
|
}
|
|
573
583
|
function executionSnapshotFromPath(contextPath, workId) {
|