@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134

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 (204) hide show
  1. package/CHANGELOG.md +687 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +14 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +94 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +136 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +753 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1828 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +182 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1083 -194
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +785 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -0,0 +1,27 @@
1
+ import { errorRecord } from "../shared/errors.js";
2
+ /** Cleanup is secondary to the arbiter's known commit outcome. Always visit
3
+ * every connection, including after a failed rollback or close. */
4
+ export function cleanupScopeWriters(targets) {
5
+ const errors = [];
6
+ for (const target of targets) {
7
+ let failed = false;
8
+ if (target.rollback) {
9
+ try {
10
+ target.db.exec("ROLLBACK");
11
+ }
12
+ catch (error) {
13
+ failed = true;
14
+ errors.push({ target: target.name, phase: "rollback", message: String(error), error: errorRecord(error) });
15
+ }
16
+ }
17
+ if (target.close === "always" || failed) {
18
+ try {
19
+ target.db.close?.();
20
+ }
21
+ catch (error) {
22
+ errors.push({ target: target.name, phase: "close", message: String(error), error: errorRecord(error) });
23
+ }
24
+ }
25
+ }
26
+ return errors;
27
+ }
@@ -1,9 +1,9 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs";
3
2
  import { continuationPolicies, flowKinds } from "../domain/contracts.js";
4
3
  import { AppError } from "../shared/errors.js";
5
- import { commandOption, commandPosition, parseLifecycleCommand } from "./lifecycle-command.js";
4
+ import { hookSessionIdentity } from "./hooks.js";
6
5
  import { parseJsonObject } from "../shared/json.js";
6
+ import { prepareTextFile } from "../cli/input-preparation.js";
7
7
  import { resolveProjectRoot } from "../storage/paths.js";
8
8
  import { appendAudit } from "./audit.js";
9
9
  import { cancelLaneWaitersForWorker } from "./lanes.js";
@@ -30,8 +30,8 @@ export function recordFlowSessionObservation(context, input) {
30
30
  const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
31
31
  WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
32
32
  const now = context.now();
33
- const runId = input.runId ?? null;
34
- const protocolId = input.protocolId ?? null;
33
+ const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
34
+ const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
35
35
  if (open && open.run_id === runId && open.protocol_id === protocolId) {
36
36
  return { changed: false, segment_id: open.id };
37
37
  }
@@ -42,27 +42,6 @@ export function recordFlowSessionObservation(context, input) {
42
42
  VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
43
43
  return { changed: true, segment_id: Number(result.lastInsertRowid) };
44
44
  }
45
- export function bindObservedFlowSession(context, project, payload, sessionId) {
46
- if (resolveProjectRoot(payload.project_root) !== project.root) {
47
- throw new AppError("project_mismatch", "Observed session payload project_root does not match hook cwd project", 1, {
48
- expected: project.root,
49
- actual: payload.project_root
50
- });
51
- }
52
- const session = upsertFlowSession(context, project, payload, sessionId);
53
- if (session.session_kind !== "orchestrator") {
54
- const now = context.now();
55
- for (const unit of payload.coverage_units ?? []) {
56
- if (!unit.job_id)
57
- continue;
58
- context.db.run(`UPDATE flow_jobs SET status = 'running', worker_session_id = ?, attempts = attempts + 1, started_at = COALESCE(started_at, ?), updated_at = ?
59
- WHERE project_id = ? AND run_id = ? AND job_id = ? AND status IN ('pending', 'failed')`, [session.session_id, now, now, project.id, session.run_id ?? "", unit.job_id]);
60
- }
61
- }
62
- if (session.run_id)
63
- refreshRunSessionProjection(context, project.id, session.run_id);
64
- return session;
65
- }
66
45
  export function registerFlowJob(context, input) {
67
46
  const jobId = input.jobId ?? `JOB-${input.runId}-${input.planItemId}`.replace(/[^A-Za-z0-9_-]+/g, "-");
68
47
  const now = context.now();
@@ -76,9 +55,33 @@ export function flowJobsForRun(context, projectId, runId) {
76
55
  return context.db.all("SELECT * FROM flow_jobs WHERE project_id = ? AND run_id = ? ORDER BY plan_item_id", [projectId, runId]);
77
56
  }
78
57
  export function registerFlowSession(context, input) {
79
- const payload = decodeFlowSessionPayload(input);
58
+ const payload = input.preparedPayload ?? prepareFlowSessionPayload(input);
80
59
  const project = requireProjectByRoot(context, resolveProjectRoot(payload.project_root));
81
- const session = upsertFlowSession(context, project, payload, input.sessionId);
60
+ if (payload.run_id && !context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [project.id, payload.run_id])) {
61
+ throw new AppError("not_found", `Run is not registered: ${payload.run_id}`, 1, { run_id: payload.run_id, effect: "no_effect" });
62
+ }
63
+ const identity = input.hookEventId ? hookSessionIdentity(context, project.id, input.hookEventId) : undefined;
64
+ if (!identity && input.sessionId && payload.provider_session_id && payload.provider_session_id !== input.sessionId) {
65
+ throw new AppError("validation", "--session-id conflicts with payload provider_session_id", 2);
66
+ }
67
+ const session = upsertFlowSession(context, project, identity ? {
68
+ ...payload, harness: identity.harness, provider_session_id: identity.providerSessionId,
69
+ parent_session_id: identity.parentSessionId, agent_id: identity.agentId,
70
+ provider: identity.provider, model: identity.model, reasoning: identity.reasoning,
71
+ mode: identity.mode, agent_type: identity.agentType
72
+ } : { ...payload, ...(input.sessionId ? { provider_session_id: input.sessionId } : {}) }, identity?.sessionId ?? input.sessionId);
73
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId: session.session_id,
74
+ runId: session.run_id, protocolId: session.protocol_id, cwd: session.cwd,
75
+ eventKey: `session-register-${crypto.randomUUID()}` });
76
+ // Logical Work/job binding belongs to the successful CLI registration, never
77
+ // to observation of model-supplied payload by a pre-tool hook.
78
+ if (session.session_kind !== "orchestrator")
79
+ for (const unit of payload.coverage_units ?? []) {
80
+ if (!unit.job_id)
81
+ continue;
82
+ context.db.run(`UPDATE flow_jobs SET status = 'running', worker_session_id = ?, attempts = attempts + 1, started_at = COALESCE(started_at, ?), updated_at = ?
83
+ WHERE project_id = ? AND run_id = ? AND job_id = ? AND status IN ('pending', 'failed')`, [session.session_id, context.now(), context.now(), project.id, session.run_id ?? "", unit.job_id]);
84
+ }
82
85
  if (session.run_id)
83
86
  appendFlowRunTimelineEvent(context, project.id, session.run_id, { type: "session_bound", session_id: session.session_id });
84
87
  if (session.run_id)
@@ -195,52 +198,15 @@ export function updateFlowSessionContinuation(context, projectId, sessionId, act
195
198
  refreshRunSessionProjection(context, projectId, session.run_id);
196
199
  return nextCount;
197
200
  }
198
- export function flowSessionPayloadFromRegisterCommand(command) {
199
- const invocation = typeof command === "string" ? invocationFromCommand(command) : command;
200
- if (!invocation)
201
- return undefined;
202
- if (invocation.operation === "stage_start")
203
- return flowSessionPayloadFromStageStartCommand(invocation);
204
- if (invocation.operation !== "session_register")
205
- return undefined;
206
- const payloadBase64 = commandOption(invocation, "payload-base64");
207
- const payloadJson = commandOption(invocation, "payload-json");
208
- const payloadFile = commandOption(invocation, "payload-file");
209
- if (!payloadBase64 && !payloadJson && !payloadFile) {
210
- return undefined;
211
- }
212
- return decodeFlowSessionPayload({ payloadBase64, payloadJson, payloadFile });
213
- }
214
- function flowSessionPayloadFromStageStartCommand(invocation) {
215
- const projectRoot = commandOption(invocation, "project-root");
216
- const stage = commandOption(invocation, "stage");
217
- const run = commandPosition(invocation, 0);
218
- if (!projectRoot || !stage || !run || run.startsWith("--"))
219
- return undefined;
220
- return {
221
- project_root: projectRoot,
222
- flow_kind: ["code", "implementation", "readiness", "merge"].includes(stage) ? "implementation" : "planning",
223
- run_id: run,
224
- protocol_id: null,
225
- worker_id: null,
226
- workspace_path: null,
227
- continuation_policy: "go_router",
228
- session_kind: "orchestrator",
229
- current_stage: stage,
230
- coverage_units: []
231
- };
232
- }
233
- function invocationFromCommand(command) {
234
- const analysis = parseLifecycleCommand(command);
235
- return analysis.kind === "none" ? undefined : analysis.invocation;
236
- }
237
- function decodeFlowSessionPayload(input) {
201
+ export function prepareFlowSessionPayload(input) {
202
+ if ([input.payloadFile, input.payloadBase64, input.payloadJson].filter(value => value !== undefined).length !== 1)
203
+ throw new AppError("usage", "session register requires exactly one of --payload-file, --payload-base64, or --payload-json", 2);
238
204
  if (input.payloadFile) {
239
- return normalizeFlowSessionPayload(parseJsonObject(fs.readFileSync(input.payloadFile, "utf8"), "session payload"));
205
+ return normalizeFlowSessionPayload(parseJsonObject(prepareTextFile(input.payloadFile, "session payload").text, "session payload"));
240
206
  }
241
207
  if (input.payloadBase64) {
242
208
  if (/\$[A-Za-z_{(]/.test(input.payloadBase64)) {
243
- throw new AppError("validation", "session register --payload-base64 must be a literal base64 value because Codex PreToolUse hooks run before shell variable expansion; use --payload-file from prompts", 2);
209
+ throw new AppError("validation", "session register received an unexpanded shell variable as --payload-base64; expand it before invoking the CLI or use --payload-file", 2);
244
210
  }
245
211
  return normalizeFlowSessionPayload(parseJsonObject(Buffer.from(input.payloadBase64, "base64").toString("utf8"), "session payload"));
246
212
  }
@@ -1,19 +1,24 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { resolveProjectRoot } from "../storage/paths.js";
3
3
  import { requireProjectByRoot } from "./projects.js";
4
- import { blockFlowRunStage, unblockFlowRunStage } from "./runs.js";
4
+ import { blockFlowRunStage, unblockFlowRunStage, prepareFlowRunStageBlock, prepareFlowRunStageUnblock } from "./runs.js";
5
5
  import { flowCommand } from "./stage-pause.js";
6
6
  import { refreshRunWorkProjection } from "./work-registry.js";
7
- export function blockStageForRuntime(context, input) {
7
+ export function prepareStageRuntimeBlock(context, input) {
8
+ if (typeof input.code !== "string" || !input.code.trim() || typeof input.summary !== "string" || !input.summary.trim())
9
+ throw new AppError("validation", "stage block requires a code and summary", 2);
8
10
  const projectRoot = resolveProjectRoot(input.projectRoot);
9
11
  const project = requireProjectByRoot(context, projectRoot);
10
12
  const run = requireRun(context, project.id, input.runId);
11
13
  const work = requireWork(context, project.id, run.id, input.workId);
12
14
  if (work.status !== "running")
13
15
  throw new AppError("invalid_work_state", "Only a running Work can be blocked", 1, { work_id: work.work_id, status: work.status });
14
- if (!input.code.trim() || !input.summary.trim())
15
- throw new AppError("validation", "stage block requires a code and summary", 2);
16
- const state = blockFlowRunStage(context, { ...input, projectRoot });
16
+ const flow = prepareFlowRunStageBlock(context, { ...input, projectRoot });
17
+ return { projectRoot, project, work, flow };
18
+ }
19
+ export function blockStageForRuntime(context, input) {
20
+ const { projectRoot, project, work, flow } = prepareStageRuntimeBlock(context, input);
21
+ const state = blockFlowRunStage(context, { ...input, projectRoot }, flow);
17
22
  const now = context.now();
18
23
  context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
19
24
  refreshRunWorkProjection(context, project.id, work.run_id);
@@ -30,14 +35,19 @@ export function blockStageForRuntime(context, input) {
30
35
  state
31
36
  };
32
37
  }
33
- export function unblockStageAfterRuntimeRepair(context, input) {
38
+ export function prepareStageRuntimeUnblock(context, input) {
34
39
  const projectRoot = resolveProjectRoot(input.projectRoot);
35
40
  const project = requireProjectByRoot(context, projectRoot);
36
41
  const run = requireRun(context, project.id, input.runId);
37
42
  const work = requireWork(context, project.id, run.id, input.workId);
38
43
  if (work.status !== "paused")
39
44
  throw new AppError("invalid_work_state", "stage unblock requires its paused Work", 1, { work_id: work.work_id, status: work.status });
40
- const state = unblockFlowRunStage(context, { projectRoot, runId: work.run_id, stage: input.stage, workId: work.work_id });
45
+ const flow = prepareFlowRunStageUnblock(context, { ...input, projectRoot, runId: work.run_id });
46
+ return { projectRoot, project, work, flow };
47
+ }
48
+ export function unblockStageAfterRuntimeRepair(context, input) {
49
+ const { projectRoot, project, work, flow } = prepareStageRuntimeUnblock(context, input);
50
+ const state = unblockFlowRunStage(context, { projectRoot, runId: work.run_id, stage: input.stage, workId: work.work_id }, flow);
41
51
  const now = context.now();
42
52
  context.db.run("UPDATE works SET status = 'running', updated_at = ? WHERE work_id = ? AND status = 'paused'", [now, work.work_id]);
43
53
  refreshRunWorkProjection(context, project.id, work.run_id);
@@ -2,33 +2,170 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { parseJsonObject } from "../shared/json.js";
5
+ import { prepareFileBytes, prepareJsonText, prepareOutputFile } from "../cli/input-preparation.js";
6
+ export function runContextRelocations(context, projectId, runId) {
7
+ return context.db.all("SELECT id, payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.publication_relocation' ORDER BY id", [projectId]).flatMap(row => {
8
+ const receipt = JSON.parse(row.payload_json);
9
+ if (receipt.run_id !== runId || !receipt.path_mapping)
10
+ return [];
11
+ return [{ receipt_id: `${row.id}:${receipt.import_id}`, source_manifest_sha256: receipt.source_manifest_sha256, path_mapping: receipt.path_mapping }];
12
+ });
13
+ }
14
+ function assertContextRelocation(receipt) {
15
+ if (!receipt.receipt_id || !/^[a-f0-9]{64}$/.test(receipt.source_manifest_sha256)
16
+ || !Array.isArray(receipt.path_mapping) || receipt.path_mapping.some(item => !item || typeof item.from !== "string" || typeof item.to !== "string" || !path.isAbsolute(item.from) || !path.isAbsolute(item.to)))
17
+ throw new AppError("context_relocation_invalid", "Stage context relocation lacks a verified import binding", 2);
18
+ }
19
+ /** Physical packet locators may move; accepted decision/provenance blocks may not. */
20
+ export function projectPacketLocators(text, receipts) {
21
+ receipts.forEach(assertContextRelocation);
22
+ return text.split(/(<run_operational_contract>[\s\S]*?<\/run_operational_contract>|<external_stage_context>[\s\S]*?<\/source_precedence>|DD_FLOW_(?:INVOCATION_SCOPE|CURRENT_INVOCATION|DAEMON_ID)=(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s'"])+)/g).map(part => {
23
+ if (part.startsWith("<run_operational_contract>") || part.startsWith("<external_stage_context>") || part.startsWith("DD_FLOW_INVOCATION_SCOPE=") || part.startsWith("DD_FLOW_CURRENT_INVOCATION=") || part.startsWith("DD_FLOW_DAEMON_ID="))
24
+ return part;
25
+ for (const receipt of receipts) {
26
+ const mappings = [...receipt.path_mapping].sort((a, b) => b.from.length - a.from.length);
27
+ if (!mappings.length)
28
+ continue;
29
+ const prefixes = mappings.map(item => item.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
30
+ // One longest-prefix replacement per receipt: a new root may itself sit
31
+ // below an old prefix and must not be mapped twice by that receipt.
32
+ const locator = new RegExp(`(^|[^A-Za-z0-9_./~:\\\\-])(${prefixes})(?=[/\\\\]|$|[\\s"'\x60<>{}\\[\\](),;!?])`, "g");
33
+ part = part.replace(locator, (_match, boundary, prefix) => `${boundary}${mappings.find(item => item.from === prefix).to}`);
34
+ }
35
+ return part;
36
+ }).join("");
37
+ }
38
+ /** Current delivery packet is derived; imported prompt/context bytes remain provenance. */
39
+ export function currentStagePrompt(context, promptPath, runHome = path.dirname(path.dirname(promptPath)), sourceText) {
40
+ let original = sourceText ?? fs.readFileSync(promptPath, "utf8");
41
+ let sourcePromptPath = promptPath;
42
+ if (original.startsWith("<current_prompt_projection>\n")) {
43
+ const prior = JSON.parse(original.split("\n")[1]);
44
+ const source = path.resolve(runHome, prior.source_prompt);
45
+ const relative = path.relative(runHome, source);
46
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !fs.existsSync(source))
47
+ throw new AppError("context_checksum_mismatch", "Current packet has no retained source prompt", 2);
48
+ original = fs.readFileSync(source, "utf8");
49
+ if (sha256(original) !== prior.original_prompt_sha256)
50
+ throw new AppError("context_checksum_mismatch", "Retained source prompt changed", 2);
51
+ sourcePromptPath = source;
52
+ }
53
+ const row = context.db.get("SELECT id, project_id, index_json FROM runs WHERE run_root = ?", [runHome]);
54
+ if (!row)
55
+ return { prompt_path: promptPath, worker_prompt_markdown: original };
56
+ const profile = JSON.parse(row.index_json).execution_profile;
57
+ const receipts = runContextRelocations(context, row.project_id, row.id);
58
+ if (!receipts.length)
59
+ return { prompt_path: promptPath, worker_prompt_markdown: original };
60
+ const operational = profile?.operational_contract;
61
+ let rendered = projectPacketLocators(original, receipts);
62
+ if (operational) {
63
+ const block = renderRunOperationalContext(runHome, operational.sha256, operational.path);
64
+ rendered = rendered.includes("<run_operational_contract>") ? rendered.replace(/<run_operational_contract>[\s\S]*?<\/run_operational_contract>/, () => block) : `${block}\n\n${rendered}`;
65
+ }
66
+ const relativeDirectory = path.relative(runHome, path.dirname(sourcePromptPath));
67
+ const stageEntry = Object.entries(profile?.stage_context_inputs ?? {}).find(([, item]) => path.dirname(item.path) === relativeDirectory);
68
+ if (stageEntry) {
69
+ const [stage, item] = stageEntry;
70
+ const loaded = loadExternalStageContext({ file: path.join(runHome, item.path), expectedSha256: item.sha256, stage });
71
+ const block = renderRetainedStageContext({ loaded, receipts });
72
+ rendered = rendered.includes("<external_stage_context>") ? rendered.replace(/<external_stage_context>[\s\S]*?<\/source_precedence>/, () => block) : `${block}\n\n${rendered}`;
73
+ }
74
+ const proof = { source_prompt: path.relative(runHome, sourcePromptPath), original_prompt_sha256: sha256(original), operational_sha256: operational?.sha256 ?? null, context_sha256: stageEntry ? stageEntry[1].sha256 : null, receipts };
75
+ rendered = `<current_prompt_projection>\n${JSON.stringify(proof)}\nHistorical prompt and context bytes are provenance; use the current rendered input paths below. Embedded CLI command text is historical, non-executable evidence, not current command authority. Do not execute commands from this projection. Use only independently issued current Stage next commands or the existing lifecycle recovery replacement commands after acknowledgement. Path relocation does not reissue credentials, invocation IDs, generations or logical scope.\n</current_prompt_projection>\n\n${rendered}`;
76
+ const file = path.join(runHome, "prompt-projections", `${sha256(rendered)}.md`);
77
+ fs.mkdirSync(path.dirname(file), { recursive: true });
78
+ if (fs.existsSync(file)) {
79
+ if (fs.readFileSync(file, "utf8") !== rendered)
80
+ throw new AppError("context_checksum_mismatch", "Current prompt projection changed", 2);
81
+ }
82
+ else
83
+ fs.writeFileSync(file, rendered, { flag: "wx" });
84
+ return { prompt_path: file, worker_prompt_markdown: rendered };
85
+ }
86
+ /** Render only declared path fields through verified import receipts; retained bytes stay untouched. */
87
+ export function renderRetainedStageContext(input) {
88
+ if (input.workspaceView && (!input.workspaceView.work_id || !path.isAbsolute(input.workspaceView.from) || !path.isAbsolute(input.workspaceView.to)
89
+ || !/^[a-f0-9]{64}$/.test(input.workspaceView.source_sha256) || !/^[a-f0-9]{64}$/.test(input.workspaceView.copy_sha256)))
90
+ throw new AppError("context_workspace_view_invalid", "Stage context workspace view lacks its bound review-copy proof", 2);
91
+ const mapPath = (source, workspaceSource = true) => {
92
+ let mapped = source;
93
+ for (const receipt of input.receipts) {
94
+ assertContextRelocation(receipt);
95
+ const selected = [...receipt.path_mapping].sort((a, b) => b.from.length - a.from.length).find(item => mapped === item.from || mapped.startsWith(`${item.from}${path.sep}`));
96
+ if (selected)
97
+ mapped = path.join(selected.to, path.relative(selected.from, mapped));
98
+ }
99
+ const view = input.workspaceView;
100
+ if (view && workspaceSource && (mapped === view.from || mapped.startsWith(`${view.from}${path.sep}`)))
101
+ mapped = path.join(view.to, path.relative(view.from, mapped));
102
+ return mapped;
103
+ };
104
+ const value = input.loaded.value;
105
+ const mappedValue = { ...value,
106
+ ...(value.task_input ? { task_input: value.task_input.map(item => ({ ...item, path: mapPath(item.path) })) } : {}),
107
+ ...(value.sources ? { sources: value.sources.map(item => ({ ...item, path: mapPath(item.path, item.root !== "run") })) } : {}) };
108
+ return [input.receipts.length ? `<context_path_mapping>\nOriginal context SHA-256: ${input.loaded.materializedSha256}\nVerified import receipts: ${JSON.stringify(input.receipts)}\nOnly rendered physical path fields are relocated; original decisions and source bytes are unchanged.\n</context_path_mapping>` : "",
109
+ input.workspaceView ? `<review_workspace_view>\nBound review copy: ${JSON.stringify(input.workspaceView)}\nOnly project/workspace input paths use this read-only view. Original authority and RUN artifacts stay in their retained locations.\n</review_workspace_view>` : "",
110
+ renderExternalStageContext({ loaded: { ...input.loaded, value: mappedValue }, installed: { path: input.loaded.sourcePath } })].filter(Boolean).join("\n\n");
111
+ }
112
+ /** One precedence rule for coordinator and delegated Work packets. */
113
+ export const acceptedDecisionPrecedence = "Read the accepted RUN decisions before applying older general project rules. An accepted, explicitly scoped user decision supersedes a conflicting general rule only within that scope; preserve the rule elsewhere. Do not ask the user again about a decision already settled in those artifacts or invent a broader exception.";
114
+ /** Shared Stage/Work projection; historical RUNs do not gain a new grant. */
115
+ export function renderRunOperationalContext(runHome, expectedSha256, artifactPath) {
116
+ const indexFile = path.join(runHome, "run.json");
117
+ const index = fs.existsSync(indexFile) ? JSON.parse(fs.readFileSync(indexFile, "utf8")) : null;
118
+ const metadata = index?.execution_profile?.operational_contract;
119
+ const file = path.join(runHome, artifactPath ?? metadata?.path ?? "operational-contract.json");
120
+ const binding = expectedSha256 ?? metadata?.sha256;
121
+ if (!binding)
122
+ return "";
123
+ if (!fs.existsSync(file) || sha256(fs.readFileSync(file)) !== binding)
124
+ throw new AppError("operational_contract_changed", "Frozen RUN operational input changed or is missing", 2, { file });
125
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
126
+ if (value.policy_source && (!fs.existsSync(path.join(runHome, value.policy_source.path)) || sha256(fs.readFileSync(path.join(runHome, value.policy_source.path))) !== value.policy_source.sha256))
127
+ throw new AppError("operational_contract_changed", "Retained policy source changed", 2);
128
+ return ["<run_operational_contract>", `- immutable artifact: ${file}`, `- sha256: ${binding}`, `- logical scope: ${JSON.stringify(value.binding)}`,
129
+ `- delivery: ${value.settings.merge_delivery.strategy}; cleanup: ${value.settings.merge_cleanup.source}`,
130
+ ...(value.decision ? [`- accepted decision ${value.decision.id}: ${JSON.stringify(value.decision)}`, `- retained policy source: ${value.policy_source ? path.join(runHome, value.policy_source.path) : path.resolve(index?.workspace?.project_root ?? value.binding.project_root, value.decision.policy.path)} (original locators above are provenance only)`, acceptedDecisionPrecedence] : ["No scoped override is accepted. Preserve project policy; frozen defaults do not grant an exception."]),
131
+ "Local completion means proven integration into the declared target commit, not remote publication, deployment, release, or a feature-only handoff. Source retention applies only to this RUN's source. Preserve dirty/unmerged, branch-protection and immutable-tag guards.",
132
+ "A retention review date requests review; it is not an automatic expiry or deletion instruction.", "</run_operational_contract>"].join("\n");
133
+ }
6
134
  export function loadExternalStageContext(input) {
7
- const sourcePath = path.resolve(input.file);
8
- if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile())
9
- throw new AppError("not_found", "Stage context file is missing", 2, { context_file: sourcePath });
10
- const bytes = fs.readFileSync(sourcePath);
135
+ const { file: sourcePath, bytes } = prepareFileBytes(input.file, "Stage context");
11
136
  const materializedSha256 = sha256(bytes);
12
137
  if (materializedSha256 !== input.expectedSha256)
13
138
  throw new AppError("context_checksum_mismatch", "Stage context checksum does not match --context-sha256", 2, { context_file: sourcePath, expected_sha256: input.expectedSha256, actual_sha256: materializedSha256 });
14
- const value = parseJsonObject(bytes.toString("utf8"), "stage context");
139
+ const value = prepareJsonText(bytes.toString("utf8"), { file: sourcePath, label: "Stage context" }).value;
15
140
  validateContext(value, input.stage);
16
- return { value, sourcePath, materializedSha256 };
141
+ return { value, sourcePath, materializedSha256, bytes };
17
142
  }
18
- /** Installs exactly one immutable external context for a stage attempt. */
19
- export function installExternalStageContext(input) {
20
- fs.mkdirSync(input.stageRoot, { recursive: true });
143
+ export function prepareExternalStageContext(input) {
144
+ const destination = externalStageContextDestination(input);
145
+ if (!fs.existsSync(destination))
146
+ prepareOutputFile(destination);
147
+ return destination;
148
+ }
149
+ function externalStageContextDestination(input) {
21
150
  const destination = path.join(input.stageRoot, "stage-context.json");
22
151
  if (fs.existsSync(destination)) {
23
152
  const existing = sha256(fs.readFileSync(destination));
24
153
  if (existing !== input.loaded.materializedSha256)
25
154
  throw new AppError("stage_context_replacement", "A different stage context is already installed for this Stage", 2, { stage_context: destination, expected_sha256: input.loaded.materializedSha256, actual_sha256: existing });
26
155
  }
27
- else {
156
+ return destination;
157
+ }
158
+ /** Installs exactly one immutable external context for a stage attempt. */
159
+ export function installExternalStageContext(input) {
160
+ // Publication may follow earlier effects. Recheck immutable identity here,
161
+ // but never reclassify a late output failure as a no-effect input rejection.
162
+ const destination = externalStageContextDestination(input);
163
+ fs.mkdirSync(input.stageRoot, { recursive: true });
164
+ if (!fs.existsSync(destination)) {
28
165
  // The receipt is the hash of the runner-materialized bytes, not of a
29
166
  // convenient reserialization. Preserve those exact bytes as evidence.
30
167
  const temporary = `${destination}.${process.pid}.tmp`;
31
- fs.copyFileSync(input.loaded.sourcePath, temporary);
168
+ fs.writeFileSync(temporary, input.loaded.bytes);
32
169
  fs.renameSync(temporary, destination);
33
170
  }
34
171
  return {
@@ -41,6 +178,15 @@ export function installExternalStageContext(input) {
41
178
  /** Install the runner slice and make it visible in the agent packet before a
42
179
  * caller performs its lifecycle transition. */
43
180
  export function applyExternalStageContext(input) {
181
+ const home = path.dirname(input.stageRoot);
182
+ const row = input.context?.db.get("SELECT index_json FROM runs WHERE run_root = ?", [home]);
183
+ const authoritative = row ? JSON.parse(row.index_json).execution_profile?.operational_contract : undefined;
184
+ const operational = input.context ? authoritative ? renderRunOperationalContext(home, authoritative.sha256, authoritative.path) : "" : renderRunOperationalContext(home);
185
+ if (operational && fs.existsSync(input.promptPath)) {
186
+ const current = fs.readFileSync(input.promptPath, "utf8");
187
+ if (!current.includes("<run_operational_contract>"))
188
+ fs.writeFileSync(input.promptPath, `${operational}\n\n${current}`);
189
+ }
44
190
  if (!input.loaded)
45
191
  return undefined;
46
192
  const installed = installExternalStageContext({ stageRoot: input.stageRoot, loaded: input.loaded });
@@ -72,19 +218,39 @@ export function renderExternalStageContext(input) {
72
218
  "</project_context>", "",
73
219
  "<accepted_decisions>",
74
220
  ...(decisions.length ? decisions.map((item) => `- ${typeof item === "string" ? item : `${item.id ? `${item.id}: ` : ""}${item.statement ?? ""}`}`) : ["- No additional accepted decisions."]),
75
- "</accepted_decisions>"
221
+ "</accepted_decisions>", "",
222
+ "<source_precedence>",
223
+ acceptedDecisionPrecedence,
224
+ "</source_precedence>"
76
225
  ].join("\n");
77
226
  }
78
227
  function validateContext(value, expectedStage) {
79
- if (value.schema_id !== "dd-eval/stage-context@1")
80
- throw new AppError("validation", "Stage context must use dd-eval/stage-context@1", 2);
228
+ const object = (item) => Boolean(item && typeof item === "object" && !Array.isArray(item));
229
+ const text = (item) => typeof item === "string" && Boolean(item.trim());
230
+ if (!object(value))
231
+ throw new AppError("validation", "Stage context must be an object", 2);
232
+ if (value.schema_id !== "dd-flow/stage-context@1" && value.schema_id !== "dd-eval/stage-context@1")
233
+ throw new AppError("validation", "Stage context must use dd-flow/stage-context@1", 2);
81
234
  if (value.stage !== expectedStage)
82
235
  throw new AppError("validation", "Stage context does not match --stage", 2, { expected_stage: expectedStage, context_stage: value.stage });
83
- if (!value.objective?.trim())
236
+ if (!text(value.objective))
84
237
  throw new AppError("validation", "Stage context objective is required", 2);
85
- for (const source of value.sources ?? []) {
86
- if (!source.role?.trim() || !source.path?.trim() || !source.reason?.trim())
87
- throw new AppError("validation", "Every stage context source needs role, path and reason", 2);
238
+ for (const key of ["task_input", "sources", "accepted_decisions", "dynamic_roles"]) {
239
+ if (value[key] === undefined)
240
+ continue;
241
+ if (!Array.isArray(value[key]))
242
+ throw new AppError("validation", `Stage context ${key} must be an array`, 2);
243
+ for (const item of value[key]) {
244
+ const valid = key === "accepted_decisions" ? text(item) || object(item) && (text(item.id) || text(item.statement)) && [item.id, item.statement].every(field => field === undefined || text(field))
245
+ : object(item) && text(item.role) && (key === "dynamic_roles" ? item.reason === undefined || text(item.reason)
246
+ : text(item.path) && (key === "sources" ? text(item.reason) && (item.required === undefined || typeof item.required === "boolean") && (item.root === undefined || typeof item.root === "string" && ["project", "workspace", "run"].includes(item.root)) : item.sha256 === undefined || text(item.sha256)));
247
+ if (!valid)
248
+ throw new AppError("validation", `Invalid stage context ${key} item`, 2, { parameter: key });
249
+ }
250
+ }
251
+ for (const key of ["semantic_package_sha256", "context_slice_sha256"]) {
252
+ if (value[key] !== undefined && !text(value[key]))
253
+ throw new AppError("validation", `Stage context ${key} must be a nonempty string`, 2);
88
254
  }
89
255
  }
90
256
  function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); }