@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100

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.
Files changed (153) hide show
  1. package/CHANGELOG.md +561 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +91 -15
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1506 -334
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
  19. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  21. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  22. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  23. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  24. package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
  25. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
  26. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  27. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
  28. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  29. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  30. package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  33. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  34. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  35. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  36. package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
  37. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  38. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  39. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  40. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  41. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  42. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  43. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  44. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  45. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  46. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  47. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  48. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  49. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  50. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  51. package/dist/runtime/context.js +3 -1
  52. package/dist/schemas/agent-profile.schema.json +1 -1
  53. package/dist/schemas/code-review-result.schema.json +2 -2
  54. package/dist/schemas/code-work-batch.schema.json +4 -3
  55. package/dist/schemas/code-work-result.schema.json +4 -4
  56. package/dist/schemas/harness-config.schema.json +23 -0
  57. package/dist/schemas/plan-review-decision.schema.json +1 -1
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +75 -21
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +676 -79
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +134 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +125 -80
  73. package/dist/services/eval-snapshots.js +838 -74
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +110 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +68 -0
  78. package/dist/services/hooks.js +609 -310
  79. package/dist/services/lanes.js +62 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +425 -0
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +78 -27
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/portable-refs.js +57 -0
  93. package/dist/services/projects.js +18 -5
  94. package/dist/services/prompts.js +27 -18
  95. package/dist/services/protocols.js +64 -20
  96. package/dist/services/recovery-observation-budget.js +61 -0
  97. package/dist/services/recovery-snapshot-database.js +107 -0
  98. package/dist/services/repair-intents.js +40 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +363 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +222 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +20 -62
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-projection.js +10 -8
  112. package/dist/services/run-recovery-runtime.js +69 -0
  113. package/dist/services/run-recovery.js +343 -0
  114. package/dist/services/runs.js +437 -81
  115. package/dist/services/runtime-budget.js +439 -0
  116. package/dist/services/runtime-command.js +43 -0
  117. package/dist/services/runtime-scope-capture.js +50 -0
  118. package/dist/services/runtime-scope-control.js +450 -0
  119. package/dist/services/runtime-scope-resume.js +565 -0
  120. package/dist/services/runtime-scope-stop.js +99 -0
  121. package/dist/services/runtime-scope-worker.js +243 -0
  122. package/dist/services/runtime-service.js +97 -0
  123. package/dist/services/schema-validation.js +21 -19
  124. package/dist/services/session-identity.js +19 -0
  125. package/dist/services/sessions.js +61 -80
  126. package/dist/services/stage-blocker.js +17 -7
  127. package/dist/services/stage-context.js +44 -18
  128. package/dist/services/stage-lifecycle.js +112 -108
  129. package/dist/services/stage-pause.js +117 -73
  130. package/dist/services/stage-work-graph.js +35 -0
  131. package/dist/services/usage.js +108 -132
  132. package/dist/services/vnext-code-review.js +245 -98
  133. package/dist/services/vnext-code.js +355 -257
  134. package/dist/services/vnext-execution-profile.js +5 -3
  135. package/dist/services/vnext-fanout.js +166 -26
  136. package/dist/services/vnext-merge.js +413 -147
  137. package/dist/services/vnext-plan-review.js +182 -109
  138. package/dist/services/vnext-plan.js +199 -82
  139. package/dist/services/vnext-protocolize.js +133 -60
  140. package/dist/services/vnext-specify.js +94 -71
  141. package/dist/services/work-registry.js +813 -160
  142. package/dist/services/workspace-bootstrap.js +76 -0
  143. package/dist/services/worktrees.js +85 -35
  144. package/dist/shared/entity-references.js +8 -0
  145. package/dist/shared/errors.js +12 -0
  146. package/dist/storage/database.js +569 -16
  147. package/dist/storage/paths.js +17 -4
  148. package/dist/storage/work-references.js +23 -0
  149. package/dist/storage/writer-contract.js +77 -0
  150. package/dist/storage/writer-migration.js +102 -0
  151. package/package.json +24 -13
  152. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
  153. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,19 +1,24 @@
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";
4
5
  import { spawnSync } from "node:child_process";
5
6
  import { AppError } from "../shared/errors.js";
6
- import { applyExternalStageContext } from "./stage-context.js";
7
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
7
8
  import { ensureVnextWorkStorage } from "../storage/database.js";
8
- import { resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
9
+ import { projectHome, resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
9
10
  import { preflightMemoryPermissions } from "./memory-permissions.js";
10
11
  import { registerProject, requireProjectByRoot } from "./projects.js";
11
- import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
12
+ import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, prepareFlowRunStart, 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";
15
17
  import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
16
18
  import { writeStageReport } from "./stage-report-renderer.js";
19
+ import { prepareError, prepareJsonSchema, prepareJsonText, prepareOutputDirectory, prepareOutputFile, prepareTextFile } from "../cli/input-preparation.js";
20
+ import { hookSessionIdentity } from "./hooks.js";
21
+ import { assertRecoveryHookCurrent, assertRunMutationAllowed } from "./run-recovery.js";
17
22
  const flowId = "mb-sdlc-vnext-specify";
18
23
  const protocolizeFlowId = "mb-sdlc-vnext-protocolize";
19
24
  const flowVersion = 4;
@@ -27,28 +32,57 @@ export function isVnextSpecifyRun(context, input) {
27
32
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
28
33
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind IN ('vnext_specify', 'vnext_protocolize')", [project.id, input.runId]));
29
34
  }
30
- export function launchVnextSpecify(context, input) {
31
- ensureVnextWorkStorage(context.db);
35
+ export function prepareVnextSpecifyStart(context, input) {
32
36
  const projectRoot = resolveProjectRoot(input.projectRoot);
33
- registerProject(context, { root: projectRoot });
34
37
  const intake = readIntake(input);
35
38
  const discussion = intake.markdown.trim();
36
39
  if (!discussion)
37
40
  throw new AppError("validation", "SPECIFY intake must not be empty", 2);
38
41
  const flow = assertFlowDefinition(projectRoot);
39
42
  const template = readRequiredFile(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "specify.md"), "vNext SPECIFY prompt template");
40
- const started = input.runId ? null : startFlowRun(context, {
41
- projectRoot,
42
- flowKind: flow.id === protocolizeFlowId ? "vnext_protocolize" : "vnext_specify",
43
- subjectType: "discussion",
44
- subjectId: "SPECIFY",
45
- slug: input.slug ?? "specify",
46
- nextAction: "await_work_session"
47
- });
43
+ const startInput = { projectRoot, flowKind: flow.id === protocolizeFlowId ? "vnext_protocolize" : "vnext_specify", subjectType: "discussion", subjectId: "SPECIFY", slug: input.slug ?? "specify", nextAction: "await_work_session" };
44
+ const start = input.runId ? undefined : prepareFlowRunStart(context, startInput);
45
+ if (input.runId)
46
+ assertSpecifyNotStarted(context, projectRoot, input.runId);
47
+ const run = input.runId ? requireRun(context, projectRoot, input.runId) : undefined;
48
+ if (!run) {
49
+ const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
50
+ prepareOutputDirectory(project ? path.join(projectHome(context.ddFlowHome, project.id), "runs") : path.join(context.ddFlowHome, "projects"));
51
+ }
52
+ if (run) {
53
+ assertRunMutationAllowed(context, run.project_id, run.id);
54
+ const home = requiredRunHome(run), stageRoot = path.join(home, "01-specify");
55
+ for (const relative of ["intake/discussion.md", "01-specify/prompt.md", "01-specify/work-context.json"])
56
+ prepareOutputFile(path.join(home, relative));
57
+ if (input.externalContext)
58
+ prepareExternalStageContext({ stageRoot, loaded: input.externalContext });
59
+ }
60
+ const preflight = preflightMemoryPermissions({ root: projectRoot, memoryBank: ".memory-bank", flow: "custom", mode: "read", targets: [{ path: ".memory-bank", mode: "read" }] });
61
+ if (preflight.ok !== true)
62
+ throw new AppError("permission_preflight_failed", "SPECIFY project context is not readable", 1, { preflight });
63
+ const grounding = projectGrounding(projectRoot);
64
+ const handoff = run ? executionSnapshot(context, run.project_id, run.id) : executionSnapshotFromProfile(context, start?.executionProfile);
65
+ return { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff };
66
+ }
67
+ function assertSpecifyNotStarted(context, projectRoot, runId) {
68
+ const existing = getFlowRunStatus(context, { projectRoot, runId });
69
+ if (existing.index?.stage_runs?.some(entry => entry.stage === stageId))
70
+ throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: runId });
71
+ }
72
+ export function launchVnextSpecify(context, input, prepared = prepareVnextSpecifyStart(context, input)) {
73
+ const { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff } = prepared;
74
+ if (input.runId)
75
+ assertSpecifyNotStarted(context, projectRoot, input.runId);
76
+ const project = requireProjectByRoot(context, projectRoot);
77
+ const identity = hookSessionIdentity(context, project.id, input.hookEventId);
78
+ if (input.runId) {
79
+ assertRunMutationAllowed(context, project.id, input.runId);
80
+ assertRecoveryHookCurrent(context, project.id, input.runId, identity);
81
+ }
82
+ ensureVnextWorkStorage(context.db);
83
+ registerProject(context, { root: projectRoot });
84
+ const started = input.runId ? null : startFlowRun(context, startInput, start);
48
85
  const run = requireRun(context, projectRoot, input.runId ?? started.run.id);
49
- const existingStages = getFlowRunStatus(context, { projectRoot, runId: run.id });
50
- if (existingStages.index?.stage_runs?.some((entry) => entry.stage === stageId))
51
- throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: run.id });
52
86
  const runHome = requiredRunHome(run);
53
87
  const stageRoot = path.join(runHome, "01-specify");
54
88
  const intakeDir = path.join(runHome, "intake");
@@ -61,11 +95,6 @@ export function launchVnextSpecify(context, input) {
61
95
  const contextPath = path.join(stageRoot, "work-context.json");
62
96
  const workId = nextWorkId(context, run.project_id, "root");
63
97
  const now = context.now();
64
- const preflight = writablePreflight(projectRoot, stageRoot);
65
- if (preflight.ok !== true)
66
- throw new AppError("permission_preflight_failed", "SPECIFY workspace is not writable", 1, { preflight });
67
- const grounding = projectGrounding(projectRoot);
68
- const handoff = executionSnapshot(context, run.id);
69
98
  const workContext = {
70
99
  schema_id: "dd-flow/work-context@1",
71
100
  system: { run_id: run.id, work_id: workId, stage: stageId },
@@ -82,7 +111,7 @@ export function launchVnextSpecify(context, input) {
82
111
  };
83
112
  writeJson(contextPath, workContext);
84
113
  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), flow: flowCommand(context) });
114
+ 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
115
  fs.writeFileSync(promptPath, prompt);
87
116
  const externalContext = applyExternalStageContext({ stageRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
88
117
  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 +155,9 @@ export function submitVnextSpecify(context, input) {
126
155
  if (work.project_id !== project.id)
127
156
  throw new AppError("project_mismatch", "Work does not belong to --project-root", 1, { work_id: work.work_id });
128
157
  if (work.status !== "running") {
129
- throw new AppError("invalid_work_state", "Work is not waiting for a SPECIFY result", 1, { work_id: work.work_id, status: work.status });
158
+ // This check precedes all result reads and mutations. A later legal state
159
+ // transition may make a new invocation appropriate, but never replays this one.
160
+ 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
161
  }
131
162
  const run = requireRun(context, projectRoot, work.run_id);
132
163
  if (runStatus(context, project.id, run.id) !== "running") {
@@ -141,7 +172,7 @@ export function submitVnextSpecify(context, input) {
141
172
  throw new AppError("not_found", "--result-file must point to an existing file inside the SPECIFY workspace", 1, { result_file: input.resultFile });
142
173
  }
143
174
  try {
144
- validateSchema({ schemaName: "vnext-specify", file: candidateFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
175
+ validateSchema({ schemaName: "vnext-specify", file: candidateFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
145
176
  const result = readVnextSpecifyResult(candidateFile);
146
177
  validateObligations(result, candidateFile);
147
178
  const normalizedResult = `${JSON.stringify(result, null, 2)}\n`;
@@ -169,7 +200,7 @@ export function submitVnextSpecify(context, input) {
169
200
  const htmlPath = path.join(stageRoot, "stage-report.html");
170
201
  const report = buildStageReport({ run, work, workSession, outcome, result, resultMarkdown: renderedMarkdown, now, stageRoot, resultFile });
171
202
  writeStageReport(stageRoot, report);
172
- validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
203
+ validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
173
204
  completeFlowRunStage(context, {
174
205
  projectRoot,
175
206
  runId: run.id,
@@ -187,9 +218,9 @@ export function submitVnextSpecify(context, input) {
187
218
  appendFlowRunTimelineEvent(context, run.project_id, run.id, { type: "work_completed", work_id: work.work_id, outcome, stage: stageId });
188
219
  if (continuesToProtocolize) {
189
220
  const nextWork = requireWork(context, work.work_id);
190
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
221
+ advanceFlowRun(context, { settlementStage: "specify", projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
191
222
  refreshRunWorkProjection(context, run.project_id, run.id);
192
- const nextCommand = `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`;
223
+ const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`);
193
224
  return {
194
225
  ok: true,
195
226
  schema_id: "dd-flow/vnext-work-submit@1",
@@ -241,13 +272,23 @@ export function finishVnextSpecifyStage(context, input) {
241
272
  const run = requireRun(context, projectRoot, input.runId);
242
273
  const stageRoot = path.join(requiredRunHome(run), "01-specify");
243
274
  const acceptedResult = path.join(stageRoot, "specify.json");
244
- const suppliedResult = input.resultJson ?? readRequiredFile(path.resolve(input.resultFile ?? acceptedResult), "SPECIFY result");
275
+ const source = input.resultFile ? path.resolve(input.resultFile) : acceptedResult;
276
+ const prepared = input.result
277
+ ? { file: source, value: input.result }
278
+ : input.resultJson !== undefined
279
+ ? prepareJsonText(input.resultJson, { file: "<stdin>", label: "SPECIFY result" })
280
+ : prepareJsonSchema(source, "SPECIFY result", { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
281
+ try {
282
+ validateSchema({ schemaName: "vnext-specify", file: prepared.file, data: prepared.value, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
283
+ validateObligations(prepared.value, prepared.file);
284
+ }
285
+ catch (error) {
286
+ return prepareError(error, { file: prepared.file, label: "SPECIFY result" });
287
+ }
245
288
  const candidate = path.join(stageRoot, `.specify-candidate-${crypto.randomUUID()}.json`);
246
- writeText(candidate, suppliedResult);
247
- // A previous worker contract allowed the final projection path as input.
248
- // Do not leave that unvalidated payload behind as an accepted-looking result.
249
- if (input.resultFile && path.resolve(input.resultFile) === acceptedResult)
250
- fs.rmSync(acceptedResult, { force: true });
289
+ // Candidate storage starts only after the exact input has passed schema and
290
+ // semantic checks; it is never the caller's source file.
291
+ writeText(candidate, `${JSON.stringify(prepared.value, null, 2)}\n`);
251
292
  try {
252
293
  return submitVnextSpecify(context, { projectRoot, workId: work.work_id, resultFile: candidate, outcome: input.outcome });
253
294
  }
@@ -260,15 +301,11 @@ function readIntake(input) {
260
301
  return { markdown: input.intakeMarkdown };
261
302
  if (!input.intakeFile)
262
303
  throw new AppError("usage", "SPECIFY intake is required", 2);
263
- const intakeFile = path.resolve(input.intakeFile);
264
- if (!fs.existsSync(intakeFile) || !fs.statSync(intakeFile).isFile()) {
265
- throw new AppError("not_found", "--intake-file must point to an existing file", 1, { intake_file: input.intakeFile });
266
- }
267
- return { markdown: fs.readFileSync(intakeFile, "utf8") };
304
+ return { markdown: prepareTextFile(input.intakeFile, "SPECIFY intake").text };
268
305
  }
269
306
  function requireRun(context, projectRoot, runId) {
270
307
  const project = requireProjectByRoot(context, projectRoot);
271
- const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_home_path FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);
308
+ 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
309
  if (!run)
273
310
  throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
274
311
  return run;
@@ -280,9 +317,9 @@ function runStatus(context, projectId, runId) {
280
317
  return run.status;
281
318
  }
282
319
  function requiredRunHome(run) {
283
- if (!run.run_home_path)
284
- throw new AppError("runtime_missing", "RUN has no portable workspace", 1, { run_id: run.id });
285
- return run.run_home_path;
320
+ if (!run.run_root)
321
+ throw new AppError("runtime_missing", "RUN has no portable artifact root", 1, { run_id: run.id });
322
+ return run.run_root;
286
323
  }
287
324
  function requireWork(context, workId) {
288
325
  const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [workId]);
@@ -299,28 +336,6 @@ function requireWorkSession(context, workSessionId) {
299
336
  function latestWorkSession(context, workId) {
300
337
  return context.db.get("SELECT * FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC, id DESC LIMIT 1", [workId]) ?? null;
301
338
  }
302
- function writablePreflight(projectRoot, stageRoot) {
303
- const memoryBank = preflightMemoryPermissions({
304
- root: projectRoot,
305
- memoryBank: ".memory-bank",
306
- flow: "custom",
307
- mode: "write",
308
- targets: [{ path: ".memory-bank", mode: "read" }]
309
- });
310
- const probe = path.join(stageRoot, `.dd-flow-probe-${crypto.randomUUID()}`);
311
- try {
312
- fs.writeFileSync(probe, "");
313
- fs.rmSync(probe);
314
- return { ...memoryBank, ok: memoryBank.ok === true, stage_workspace: { path: stageRoot, status: "passed" } };
315
- }
316
- catch (error) {
317
- try {
318
- fs.rmSync(probe, { force: true });
319
- }
320
- catch { /* best effort */ }
321
- return { ...memoryBank, ok: false, stage_workspace: { path: stageRoot, status: "failed", message: String(error) } };
322
- }
323
- }
324
339
  function projectGrounding(projectRoot) {
325
340
  return [
326
341
  ".memory-bank/index.md",
@@ -392,13 +407,13 @@ function renderPrompt(input) {
392
407
  "```",
393
408
  "The JSON must preserve the problem-space contract for a fresh PROTOCOLIZE worker: user intent, scope, acceptance and verification, settled defaults, relevant project facts, gap-method outcomes, task assessment, delivery shape and handoff. Use stable R-001... identifiers in requirements and AC-001... identifiers in acceptance_criteria. Do not describe implementation design.",
394
409
  "Do not finish while a material user question remains. Use stage pause instead of adding a question to specify.json.",
395
- "If a user answer is required, make this the lifecycle command instead of finish. Run this exact one-command heredoc, replacing only the placeholder body with the concise user-facing question packet. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:",
410
+ "If a user answer is required, use the file and standalone lifecycle command below instead of finish. Write only the concise user-facing question packet to the named text file; do not combine file creation and dd-flow in one shell command:",
396
411
  "```sh",
397
412
  input.pauseCommandTemplate,
398
413
  "```",
399
414
  "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.flow} stage finish ${input.runId} --project-root "${input.projectRoot}" --stage specify --result-file ${JSON.stringify(path.join(input.stageRoot, "specify-result.json"))} --outcome specified --json\`.`,
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 rerun that exact command in this Session. After success, follow only its returned next directive; a focused eval controller may explicitly stop at this stage boundary.",
415
+ `Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.finishCommand}\`.`,
416
+ "Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. If finish reports a no-effect validation error, correct the same result file and use its returned retry_command in this Session. Internal invocation IDs are bound by the runtime and must not be copied or invented. After success, follow only its returned next directive; a focused eval controller may explicitly stop at this stage boundary.",
402
417
  "</output_contract>",
403
418
  ""
404
419
  ].join("\n");
@@ -562,12 +577,20 @@ export function readVnextFlowDefinition(projectRoot) {
562
577
  }
563
578
  return null;
564
579
  }
565
- function executionSnapshot(context, runId) {
566
- const row = context.db.get("SELECT index_json FROM runs WHERE id = ?", [runId]);
580
+ function executionSnapshot(context, projectId, runId) {
581
+ const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
567
582
  const snapshot = row ? JSON.parse(row.index_json) : null;
568
- const handoff = snapshot?.execution_profile?.settings?.stage_session_mode;
583
+ return executionSnapshotFromProfile(context, snapshot?.execution_profile, runId);
584
+ }
585
+ function executionSnapshotFromProfile(context, profile, runId) {
586
+ let handoff = profile?.settings.stage_session_mode;
569
587
  if (handoff !== "same_session" && handoff !== "new_session")
570
588
  throw new AppError("execution_profile_invalid", "RUN has no frozen stage session mode", 1, { run_id: runId });
589
+ if (profile?.settings.execution) {
590
+ if (!profile.agent_profiles)
591
+ throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
592
+ 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;
593
+ }
571
594
  return { stage_handoff: { effective: handoff, source: "run_override" } };
572
595
  }
573
596
  function executionSnapshotFromPath(contextPath, workId) {