@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111

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 (162) hide show
  1. package/CHANGELOG.md +526 -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 +89 -13
  6. package/dist/cli/hook-ingress.js +77 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/native-hook-ingress.js +48 -0
  9. package/dist/cli/run-cli.js +1465 -384
  10. package/dist/cli.js +8 -2
  11. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  12. package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
  13. package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
  14. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  15. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  16. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  17. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  18. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  19. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  20. package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
  21. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  22. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
  23. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  24. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  25. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
  26. package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
  27. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  28. package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
  30. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  31. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
  32. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
  35. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  36. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  37. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  38. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  39. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  40. package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
  41. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  42. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
  43. package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
  44. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  45. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  46. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  47. package/dist/harness-runtime/lib/process-json.mjs +71 -0
  48. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  49. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  50. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  51. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  52. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  53. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  54. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  55. package/dist/protocol/local-files.js +18 -9
  56. package/dist/runtime/context.js +2 -2
  57. package/dist/schemas/agent-profile.schema.json +1 -1
  58. package/dist/schemas/code-review-result.schema.json +1 -1
  59. package/dist/schemas/code-work-batch.schema.json +3 -3
  60. package/dist/schemas/code-work-result.schema.json +3 -3
  61. package/dist/schemas/plan-review-result.schema.json +2 -2
  62. package/dist/schemas/run-control-receipt.schema.json +99 -0
  63. package/dist/schemas/run-control-request.schema.json +36 -0
  64. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  65. package/dist/services/canon.js +11 -8
  66. package/dist/services/cleanup.js +132 -39
  67. package/dist/services/cli-operation-classifier.js +22 -24
  68. package/dist/services/code-checks.js +589 -106
  69. package/dist/services/codex-hook-delivery.js +28 -0
  70. package/dist/services/command-context.js +80 -0
  71. package/dist/services/config.js +11 -8
  72. package/dist/services/continuation-outcome.js +20 -0
  73. package/dist/services/controller-fanout.js +173 -0
  74. package/dist/services/dashboard.js +103 -31
  75. package/dist/services/engines.js +121 -76
  76. package/dist/services/eval-snapshots.js +994 -97
  77. package/dist/services/execution-policy.js +181 -0
  78. package/dist/services/external-work-launch.js +166 -0
  79. package/dist/services/harness-adapter.js +147 -24
  80. package/dist/services/harness-config.js +2 -0
  81. package/dist/services/hooks.js +702 -327
  82. package/dist/services/lanes.js +78 -65
  83. package/dist/services/lifecycle-command.js +113 -18
  84. package/dist/services/lifecycle-contract.js +30 -0
  85. package/dist/services/lifecycle-invocations.js +1379 -0
  86. package/dist/services/managed-daemon-binding.js +38 -0
  87. package/dist/services/managed-processes.js +302 -34
  88. package/dist/services/merge-queue.js +228 -149
  89. package/dist/services/merge-server.js +66 -28
  90. package/dist/services/migrations.js +37 -16
  91. package/dist/services/native-daemon-history.js +49 -0
  92. package/dist/services/native-session-control.js +39 -0
  93. package/dist/services/plan-runtime.js +26 -11
  94. package/dist/services/plans.js +28 -25
  95. package/dist/services/projects.js +20 -15
  96. package/dist/services/prompts.js +25 -16
  97. package/dist/services/protocols.js +160 -86
  98. package/dist/services/recovery-observation-budget.js +61 -0
  99. package/dist/services/recovery-snapshot-database.js +142 -0
  100. package/dist/services/repair-intents.js +40 -0
  101. package/dist/services/review-copy.js +136 -0
  102. package/dist/services/run-control-receipt.js +56 -0
  103. package/dist/services/run-control-worker.js +351 -0
  104. package/dist/services/run-control.js +888 -0
  105. package/dist/services/run-controller-adapter.js +231 -0
  106. package/dist/services/run-controller-capture.js +167 -0
  107. package/dist/services/run-controller-process.js +180 -0
  108. package/dist/services/run-controller-recovery.js +221 -0
  109. package/dist/services/run-controller-state.js +36 -0
  110. package/dist/services/run-controller.js +798 -0
  111. package/dist/services/run-engine-bindings.js +1 -1
  112. package/dist/services/run-fork.js +142 -0
  113. package/dist/services/run-observations.js +112 -0
  114. package/dist/services/run-projection.js +22 -64
  115. package/dist/services/run-recovery-runtime.js +80 -0
  116. package/dist/services/run-recovery.js +345 -0
  117. package/dist/services/runs.js +536 -181
  118. package/dist/services/runtime-budget.js +413 -0
  119. package/dist/services/runtime-command.js +43 -0
  120. package/dist/services/runtime-scope-capture.js +52 -0
  121. package/dist/services/runtime-scope-control.js +460 -0
  122. package/dist/services/runtime-scope-resume.js +577 -0
  123. package/dist/services/runtime-scope-stop.js +159 -0
  124. package/dist/services/runtime-scope-worker.js +242 -0
  125. package/dist/services/runtime-service.js +97 -0
  126. package/dist/services/schema-validation.js +10 -8
  127. package/dist/services/scope-writer-cleanup.js +26 -0
  128. package/dist/services/sessions.js +35 -69
  129. package/dist/services/stage-blocker.js +17 -7
  130. package/dist/services/stage-context.js +50 -19
  131. package/dist/services/stage-lifecycle.js +100 -103
  132. package/dist/services/stage-pause.js +88 -75
  133. package/dist/services/stage-report-renderer.js +116 -6
  134. package/dist/services/stage-work-graph.js +35 -0
  135. package/dist/services/usage.js +35 -98
  136. package/dist/services/vnext-code-review.js +197 -84
  137. package/dist/services/vnext-code.js +278 -251
  138. package/dist/services/vnext-execution-profile.js +5 -3
  139. package/dist/services/vnext-fanout.js +172 -17
  140. package/dist/services/vnext-merge.js +345 -111
  141. package/dist/services/vnext-plan-review.js +171 -111
  142. package/dist/services/vnext-plan.js +180 -87
  143. package/dist/services/vnext-protocolize.js +320 -74
  144. package/dist/services/vnext-specify.js +115 -77
  145. package/dist/services/work-registry.js +1001 -187
  146. package/dist/services/workspace-bootstrap.js +76 -0
  147. package/dist/services/workspace-files.js +83 -0
  148. package/dist/services/worktrees.js +88 -36
  149. package/dist/shared/entity-references.js +8 -0
  150. package/dist/shared/errors.js +12 -0
  151. package/dist/storage/database.js +692 -35
  152. package/dist/storage/foreign-key-transaction.js +39 -0
  153. package/dist/storage/immutable-input.js +45 -0
  154. package/dist/storage/import-guard.js +102 -0
  155. package/dist/storage/native-cleanup.js +52 -0
  156. package/dist/storage/paths.js +17 -4
  157. package/dist/storage/projection-lock.js +73 -0
  158. package/dist/storage/work-references.js +23 -0
  159. package/dist/storage/writer-contract.js +77 -0
  160. package/dist/storage/writer-migration.js +85 -0
  161. package/package.json +24 -13
  162. package/tools/repair-paused-run-status.mjs +84 -0
@@ -1,19 +1,25 @@
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, prepareFlowRunStageCompletion, prepareFlowRunStart, startFlowRun, withStageSettlement } 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
- import { writeStageReport } from "./stage-report-renderer.js";
18
+ import { acceptStageReport, prepareStageReport } 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";
22
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
17
23
  const flowId = "mb-sdlc-vnext-specify";
18
24
  const protocolizeFlowId = "mb-sdlc-vnext-protocolize";
19
25
  const flowVersion = 4;
@@ -27,28 +33,57 @@ export function isVnextSpecifyRun(context, input) {
27
33
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
28
34
  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
35
  }
30
- export function launchVnextSpecify(context, input) {
31
- ensureVnextWorkStorage(context.db);
36
+ export function prepareVnextSpecifyStart(context, input) {
32
37
  const projectRoot = resolveProjectRoot(input.projectRoot);
33
- registerProject(context, { root: projectRoot });
34
38
  const intake = readIntake(input);
35
39
  const discussion = intake.markdown.trim();
36
40
  if (!discussion)
37
41
  throw new AppError("validation", "SPECIFY intake must not be empty", 2);
38
42
  const flow = assertFlowDefinition(projectRoot);
39
43
  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
- });
44
+ const startInput = { projectRoot, flowKind: flow.id === protocolizeFlowId ? "vnext_protocolize" : "vnext_specify", subjectType: "discussion", subjectId: "SPECIFY", slug: input.slug ?? "specify", nextAction: "await_work_session" };
45
+ const start = input.runId ? undefined : prepareFlowRunStart(context, startInput);
46
+ if (input.runId)
47
+ assertSpecifyNotStarted(context, projectRoot, input.runId);
48
+ const run = input.runId ? requireRun(context, projectRoot, input.runId) : undefined;
49
+ if (!run) {
50
+ const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
51
+ prepareOutputDirectory(project ? path.join(projectHome(context.ddFlowHome, project.id), "runs") : path.join(context.ddFlowHome, "projects"));
52
+ }
53
+ if (run) {
54
+ assertRunMutationAllowed(context, run.project_id, run.id);
55
+ const home = requiredRunHome(run), stageRoot = path.join(home, "01-specify");
56
+ for (const relative of ["intake/discussion.md", "01-specify/prompt.md", "01-specify/work-context.json"])
57
+ prepareOutputFile(path.join(home, relative));
58
+ if (input.externalContext)
59
+ prepareExternalStageContext({ stageRoot, loaded: input.externalContext });
60
+ }
61
+ const preflight = preflightMemoryPermissions({ root: projectRoot, memoryBank: ".memory-bank", flow: "custom", mode: "read", targets: [{ path: ".memory-bank", mode: "read" }] });
62
+ if (preflight.ok !== true)
63
+ throw new AppError("permission_preflight_failed", "SPECIFY project context is not readable", 1, { preflight });
64
+ const grounding = projectGrounding(projectRoot);
65
+ const handoff = run ? executionSnapshot(context, run.project_id, run.id, projectRoot, run.workspace_root) : executionSnapshotFromProfile(context, start?.executionProfile, projectRoot, projectRoot);
66
+ return { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff };
67
+ }
68
+ function assertSpecifyNotStarted(context, projectRoot, runId) {
69
+ const existing = getFlowRunStatus(context, { projectRoot, runId });
70
+ if (existing.index?.stage_runs?.some(entry => entry.stage === stageId))
71
+ throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: runId });
72
+ }
73
+ export function launchVnextSpecify(context, input, prepared = prepareVnextSpecifyStart(context, input)) {
74
+ const { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff } = prepared;
75
+ if (input.runId)
76
+ assertSpecifyNotStarted(context, projectRoot, input.runId);
77
+ const project = requireProjectByRoot(context, projectRoot);
78
+ const identity = hookSessionIdentity(context, project.id, input.hookEventId);
79
+ if (input.runId) {
80
+ assertRunMutationAllowed(context, project.id, input.runId);
81
+ assertRecoveryHookCurrent(context, project.id, input.runId, identity);
82
+ }
83
+ ensureVnextWorkStorage(context.db);
84
+ registerProject(context, { root: projectRoot });
85
+ const started = input.runId ? null : startFlowRun(context, startInput, start);
48
86
  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
87
  const runHome = requiredRunHome(run);
53
88
  const stageRoot = path.join(runHome, "01-specify");
54
89
  const intakeDir = path.join(runHome, "intake");
@@ -61,11 +96,6 @@ export function launchVnextSpecify(context, input) {
61
96
  const contextPath = path.join(stageRoot, "work-context.json");
62
97
  const workId = nextWorkId(context, run.project_id, "root");
63
98
  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
99
  const workContext = {
70
100
  schema_id: "dd-flow/work-context@1",
71
101
  system: { run_id: run.id, work_id: workId, stage: stageId },
@@ -82,7 +112,7 @@ export function launchVnextSpecify(context, input) {
82
112
  };
83
113
  writeJson(contextPath, workContext);
84
114
  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) });
115
+ 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
116
  fs.writeFileSync(promptPath, prompt);
87
117
  const externalContext = applyExternalStageContext({ stageRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
88
118
  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 +156,9 @@ export function submitVnextSpecify(context, input) {
126
156
  if (work.project_id !== project.id)
127
157
  throw new AppError("project_mismatch", "Work does not belong to --project-root", 1, { work_id: work.work_id });
128
158
  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 });
159
+ // This check precedes all result reads and mutations. A later legal state
160
+ // transition may make a new invocation appropriate, but never replays this one.
161
+ 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
162
  }
131
163
  const run = requireRun(context, projectRoot, work.run_id);
132
164
  if (runStatus(context, project.id, run.id) !== "running") {
@@ -156,21 +188,14 @@ export function submitVnextSpecify(context, input) {
156
188
  const handoff = continuesToProtocolize ? executionSnapshotFromPath(path.join(stageRoot, "work-context.json"), work.work_id) : null;
157
189
  const sameSessionContinuation = handoff?.stage_handoff.effective === "same_session";
158
190
  const terminal = !continuesToProtocolize && (outcome === "specified" || outcome === "failed" || outcome === "cancelled");
159
- if (terminal) {
160
- context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [outcome === "specified" ? "completed" : outcome, normalizedResult, now, now, work.work_id]);
161
- context.db.run("UPDATE work_sessions SET status = ?, result_path = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [outcome === "specified" ? "completed" : outcome, resultFile, now, now, work.work_id]);
162
- }
163
191
  const workSession = latestWorkSession(context, work.work_id);
164
- if (workSession && !sameSessionContinuation) {
165
- context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [resultFile, now, now, workSession.id]);
166
- }
167
192
  const reportPath = path.join(stageRoot, "stage-report.json");
168
193
  const markdownPath = path.join(stageRoot, "stage-report.md");
169
194
  const htmlPath = path.join(stageRoot, "stage-report.html");
170
195
  const report = buildStageReport({ run, work, workSession, outcome, result, resultMarkdown: renderedMarkdown, now, stageRoot, resultFile });
171
- writeStageReport(stageRoot, report);
172
- validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
173
- completeFlowRunStage(context, {
196
+ const preparedReport = prepareStageReport(report);
197
+ validateSchema({ schemaName: "stage-report", file: reportPath, data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
198
+ const completion = {
174
199
  projectRoot,
175
200
  runId: run.id,
176
201
  stage: stageId,
@@ -178,7 +203,20 @@ export function submitVnextSpecify(context, input) {
178
203
  data: "specify.json",
179
204
  dataSchemaId: "dd-flow/specify@1",
180
205
  report: "stage-report.md",
181
- stageReport: "stage-report.html"
206
+ stageReport: "stage-report.html",
207
+ dataSha256: crypto.createHash("sha256").update(fs.readFileSync(resultFile)).digest("hex")
208
+ };
209
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
210
+ withStageSettlement(context, () => {
211
+ const current = requireWork(context, work.work_id);
212
+ if (current.status !== "running" || current.updated_at !== work.updated_at)
213
+ throw new AppError("work_generation_stale", "SPECIFY Work changed during finish preparation", 1, { work_id: work.work_id });
214
+ if (terminal && context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running' AND updated_at = ?", [outcome === "specified" ? "completed" : outcome, normalizedResult, now, now, work.work_id, work.updated_at]).changes !== 1)
215
+ throw new AppError("work_generation_stale", "SPECIFY Work changed during finish preparation", 1, { work_id: work.work_id });
216
+ if (workSession && !sameSessionContinuation && context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ? AND status = 'running' AND updated_at = ?", [resultFile, now, now, workSession.id, workSession.updated_at]).changes !== 1)
217
+ throw new AppError("work_generation_stale", "SPECIFY Session changed during finish preparation", 1, { work_id: work.work_id, work_session_id: workSession.id });
218
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage: stageId, root: stageRoot, prepared: preparedReport });
219
+ completeFlowRunStage(context, completion, preparedCompletion);
182
220
  });
183
221
  const updatedWork = requireWork(context, work.work_id);
184
222
  const updatedWorkSession = workSession ? requireWorkSession(context, workSession.id) : null;
@@ -187,9 +225,9 @@ export function submitVnextSpecify(context, input) {
187
225
  appendFlowRunTimelineEvent(context, run.project_id, run.id, { type: "work_completed", work_id: work.work_id, outcome, stage: stageId });
188
226
  if (continuesToProtocolize) {
189
227
  const nextWork = requireWork(context, work.work_id);
190
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
228
+ advanceFlowRun(context, { settlementStage: "specify", projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
191
229
  refreshRunWorkProjection(context, run.project_id, run.id);
192
- const nextCommand = `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`;
230
+ const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`);
193
231
  return {
194
232
  ok: true,
195
233
  schema_id: "dd-flow/vnext-work-submit@1",
@@ -241,13 +279,23 @@ export function finishVnextSpecifyStage(context, input) {
241
279
  const run = requireRun(context, projectRoot, input.runId);
242
280
  const stageRoot = path.join(requiredRunHome(run), "01-specify");
243
281
  const acceptedResult = path.join(stageRoot, "specify.json");
244
- const suppliedResult = input.resultJson ?? readRequiredFile(path.resolve(input.resultFile ?? acceptedResult), "SPECIFY result");
282
+ const source = input.resultFile ? path.resolve(input.resultFile) : acceptedResult;
283
+ const prepared = input.result
284
+ ? { file: source, value: input.result }
285
+ : input.resultJson !== undefined
286
+ ? prepareJsonText(input.resultJson, { file: "<stdin>", label: "SPECIFY result" })
287
+ : prepareJsonSchema(source, "SPECIFY result", { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
288
+ try {
289
+ validateSchema({ schemaName: "vnext-specify", file: prepared.file, data: prepared.value, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
290
+ validateObligations(prepared.value, prepared.file);
291
+ }
292
+ catch (error) {
293
+ return prepareError(error, { file: prepared.file, label: "SPECIFY result" });
294
+ }
245
295
  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 });
296
+ // Candidate storage starts only after the exact input has passed schema and
297
+ // semantic checks; it is never the caller's source file.
298
+ writeText(candidate, `${JSON.stringify(prepared.value, null, 2)}\n`);
251
299
  try {
252
300
  return submitVnextSpecify(context, { projectRoot, workId: work.work_id, resultFile: candidate, outcome: input.outcome });
253
301
  }
@@ -260,11 +308,7 @@ function readIntake(input) {
260
308
  return { markdown: input.intakeMarkdown };
261
309
  if (!input.intakeFile)
262
310
  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") };
311
+ return { markdown: prepareTextFile(input.intakeFile, "SPECIFY intake").text };
268
312
  }
269
313
  function requireRun(context, projectRoot, runId) {
270
314
  const project = requireProjectByRoot(context, projectRoot);
@@ -299,28 +343,6 @@ function requireWorkSession(context, workSessionId) {
299
343
  function latestWorkSession(context, workId) {
300
344
  return context.db.get("SELECT * FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC, id DESC LIMIT 1", [workId]) ?? null;
301
345
  }
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
346
  function projectGrounding(projectRoot) {
325
347
  return [
326
348
  ".memory-bank/index.md",
@@ -368,6 +390,13 @@ function renderPrompt(input) {
368
390
  "```",
369
391
  "</trusted_runtime_context>",
370
392
  "",
393
+ "<workspace_contract>",
394
+ `- authoritative SPECIFY artifact directory: ${input.stageRoot}`,
395
+ `- result file to write: ${input.resultPath}`,
396
+ `- project root is read-only grounding for this Work: ${input.projectRoot}`,
397
+ "Write only the named result and stage-local artifacts. Do not switch cwd, create a worktree, or write another RUN/native home.",
398
+ "</workspace_contract>",
399
+ "",
371
400
  "<eval_input_boundary>",
372
401
  "Use only this prompt, the current project root, this stage workspace and project files reached through the supplied grounding. Do not read or search another RUN, transcript, prior result, reviewer material or other project under ~/.dd-flow or ~/.codex.",
373
402
  "If stage finish rejects the result, correct this same JSON result only from the returned validation error. Do not seek another schema or example outside this stage workspace.",
@@ -392,13 +421,14 @@ function renderPrompt(input) {
392
421
  "```",
393
422
  "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
423
  "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:",
424
+ "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
425
  "```sh",
397
426
  input.pauseCommandTemplate,
398
427
  "```",
399
428
  "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.",
429
+ `Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.finishCommand}\`.`,
430
+ "Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. After success, follow only its returned next directive; a focused eval controller may explicitly stop at this stage boundary.",
431
+ stageLifecycleInstruction,
402
432
  "</output_contract>",
403
433
  ""
404
434
  ].join("\n");
@@ -562,12 +592,20 @@ export function readVnextFlowDefinition(projectRoot) {
562
592
  }
563
593
  return null;
564
594
  }
565
- function executionSnapshot(context, runId) {
566
- const row = context.db.get("SELECT index_json FROM runs WHERE id = ?", [runId]);
595
+ function executionSnapshot(context, projectId, runId, projectRoot, workspaceRoot) {
596
+ const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
567
597
  const snapshot = row ? JSON.parse(row.index_json) : null;
568
- const handoff = snapshot?.execution_profile?.settings?.stage_session_mode;
598
+ return executionSnapshotFromProfile(context, snapshot?.execution_profile, projectRoot, workspaceRoot, runId);
599
+ }
600
+ function executionSnapshotFromProfile(context, profile, projectRoot, workspaceRoot, runId) {
601
+ let handoff = profile?.settings.stage_session_mode;
569
602
  if (handoff !== "same_session" && handoff !== "new_session")
570
603
  throw new AppError("execution_profile_invalid", "RUN has no frozen stage session mode", 1, { run_id: runId });
604
+ if (profile?.settings.execution) {
605
+ if (!profile.agent_profiles)
606
+ throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
607
+ handoff = resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot, workspaceRoot, fromStage: "specify", stage: "protocolize", stageSessionMode: handoff, mergeMode: profile.settings.merge_mode }).session_mode;
608
+ }
571
609
  return { stage_handoff: { effective: handoff, source: "run_override" } };
572
610
  }
573
611
  function executionSnapshotFromPath(contextPath, workId) {