@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -145,10 +145,11 @@ export class FrozenGenericExecution {
145
145
  this.#flow.stage(id);
146
146
  return { profile: this.profile(this.settings.stage_profiles[id]), session_mode: this.settings.stage_session_mode };
147
147
  }
148
- work(stage, profileId) {
148
+ work(stage, profileId, workerStage = stage) {
149
149
  const coordinator = this.stage(stage).profile;
150
- const id = profileId ?? coordinator.id;
151
- if (id !== coordinator.id && !this.settings.work_profiles.includes(id))
150
+ const roleProfile = this.stage(workerStage).profile;
151
+ const id = profileId ?? roleProfile.id;
152
+ if (id !== roleProfile.id && id !== coordinator.id && !this.settings.work_profiles.includes(id))
152
153
  fail("Work profile is not approved for this RUN");
153
154
  const worker = this.profile(id);
154
155
  if (this.settings.delegation.mode === "native" && !sameSessionProfile(coordinator, worker))
@@ -11,7 +11,6 @@ import { readOperationProgress, readOperationRecord, readOperationWaiting, suspe
11
11
  import { recoverDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
12
12
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
13
13
  import { readBoundedReleaseFile } from "./release-source.js";
14
- const maxAdapterOutputBytes = 1_000_000;
15
14
  /** Native hooks need a real executable, not a PATH-dependent shell expression. */
16
15
  export function prepareHarnessFlowExecutable(context, stateDir, engine) {
17
16
  // Native login shells rewrite search paths. The owned SDK must execute with
@@ -132,7 +131,6 @@ function runAdapterTransport(input, stateDir, binding = {}) {
132
131
  const child = spawn(input.executable, input.args, { env, ...(input.cwd ? { cwd: input.cwd } : {}), detached, stdio: ["ignore", "pipe", "pipe"] });
133
132
  let stdout = "";
134
133
  let stderr = "";
135
- let outputTooLarge = false;
136
134
  let timedOut = false;
137
135
  let aborted = false;
138
136
  let cleanup;
@@ -159,7 +157,7 @@ function runAdapterTransport(input, stateDir, binding = {}) {
159
157
  reject(callbackError);
160
158
  }
161
159
  else
162
- reject(new AppError(outputTooLarge ? "harness_adapter_output_limit" : timedOut ? "harness_adapter_timeout" : aborted ? "harness_adapter_aborted" : "harness_adapter_cleanup_unconfirmed", "Adapter transport cleanup could not be confirmed", 1, details));
160
+ reject(new AppError(timedOut ? "harness_adapter_timeout" : aborted ? "harness_adapter_aborted" : "harness_adapter_cleanup_unconfirmed", "Adapter transport cleanup could not be confirmed", 1, details));
163
161
  });
164
162
  };
165
163
  const deliver = (callback) => {
@@ -220,14 +218,6 @@ function runAdapterTransport(input, stateDir, binding = {}) {
220
218
  else
221
219
  input.signal?.addEventListener("abort", abort, { once: true });
222
220
  const evidence = (value) => input.onEvidence?.({ operation_id: input.operationId, executable: input.executable, args: input.args, ...value, at: new Date().toISOString() });
223
- const append = (current, chunk) => {
224
- if (Buffer.byteLength(current) + Buffer.byteLength(chunk) > maxAdapterOutputBytes) {
225
- outputTooLarge = true;
226
- stopTransport();
227
- return current;
228
- }
229
- return current + chunk;
230
- };
231
221
  const observeDiagnostic = (line) => {
232
222
  if (prettyDiagnostic === null) {
233
223
  if (line.trimEnd() === "}")
@@ -281,7 +271,7 @@ function runAdapterTransport(input, stateDir, binding = {}) {
281
271
  }
282
272
  }
283
273
  };
284
- child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
274
+ child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
285
275
  // Diagnostics have no lifetime quota. Stream to the existing evidence sink
286
276
  // and carry only a bounded tail in the disposable RPC receipt.
287
277
  child.stderr.setEncoding("utf8").on("data", (chunk) => {
@@ -322,8 +312,6 @@ function runAdapterTransport(input, stateDir, binding = {}) {
322
312
  deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
323
313
  if (spawnError)
324
314
  return reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { cause: adapterCause(spawnError.message), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
325
- if (outputTooLarge)
326
- return reject(new AppError("harness_adapter_output_limit", "Harness adapter exceeded the bounded reply limit", 1, { executable: input.executable, args: input.args.slice(0, 3), max_output_bytes: maxAdapterOutputBytes }));
327
315
  if (timedOut)
328
316
  return reject(Object.assign(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }), { observationClock: clock }));
329
317
  if (aborted)
@@ -143,7 +143,7 @@ export function memoryRepairAssignedHookCwd(context, input) {
143
143
  export function memoryNativeReviewAssignedHookCwd(context, input) {
144
144
  const work = context.db.get("SELECT status,payload_json FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
145
145
  const payload = JSON.parse(work?.payload_json ?? "null");
146
- if (payload?.kind !== "memory-review" || payload.read_only !== true)
146
+ if (!["memory-review", "interactive-review"].includes(payload?.kind) || payload?.read_only !== true)
147
147
  return null;
148
148
  const ready = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.native_review_copy_ready' AND json_extract(payload_json,'$.intent.run_id')=? AND json_extract(payload_json,'$.intent.work_id')=?", [input.projectId, input.runId, input.workId]);
149
149
  if (!ready)
@@ -319,7 +319,11 @@ export function handleCodexHook(context, input) {
319
319
  const lifecycle = lifecycleFacts(command);
320
320
  if (!lifecycle && !continuationCommand(command))
321
321
  return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
322
- const parentProviderSessionId = rawChildProviderSessionId ? codexNativeParentId(payload, rawChildProviderSessionId) : null;
322
+ const parentProviderSessionId = rawChildProviderSessionId
323
+ ? context.codexHookProof?.sessionId === rawChildProviderSessionId
324
+ ? context.codexHookProof.parentSessionId
325
+ : codexNativeParentId(payload, rawChildProviderSessionId)
326
+ : null;
323
327
  // Resume legitimately receives an answer through stdin. It is matched by
324
328
  // immutable lifecycle arguments below; never reject or rewrite that pipe.
325
329
  const bootstrapStageStart = lifecycle?.bootstrapStageStart;
@@ -415,7 +419,18 @@ export function handleCodexHook(context, input) {
415
419
  const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
416
420
  if (!daemonId || !rootProviderSessionId || !providerSessionId || !toolCallId)
417
421
  throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
418
- const executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
422
+ let executionCwd;
423
+ try {
424
+ executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
425
+ }
426
+ catch (error) {
427
+ // A proven external Work is not its Stage coordinator. Deny the tool
428
+ // normally; a policy refusal is not a broken native hook or a receipt.
429
+ if (!(error instanceof AppError) || error.code !== "work_stage_boundary")
430
+ throw error;
431
+ return { ...common, observed: false, reason: error.code, hookSpecificOutput: { hookEventName: "PreToolUse",
432
+ permissionDecision: "deny", permissionDecisionReason: error.message } };
433
+ }
419
434
  const receipt = observeLifecycleInvocation(context, {
420
435
  currentEventKey: eventKey,
421
436
  ...(invocationId ? { id: invocationId } : {}),
@@ -741,6 +756,12 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
741
756
  if ((!facts && !auxiliary) || facts?.analysis.supported === false)
742
757
  return undefined;
743
758
  const proven = identity && harness ? managedNativeInvocationScope(context, projectRoot, identity, harness) : null;
759
+ if (proven && (facts?.invocation.operation.startsWith("stage_") || facts?.invocation.operation.startsWith("delivery_")) && !facts.bootstrapStageStart
760
+ && context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'")) {
761
+ const worker = context.db.get("SELECT w.work_id FROM works w JOIN work_launches l ON l.work_id=w.work_id JOIN work_sessions ws ON ws.work_id=w.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id JOIN projects p ON p.id=w.project_id WHERE p.root=? AND w.run_id=? AND s.harness=? AND s.provider_session_id=? AND l.status IN ('launching','start_requested') LIMIT 1", [proven.projectRoot, proven.runId, harness, providerSessionId]);
762
+ if (worker)
763
+ throw new AppError("work_stage_boundary", "This external Work cannot issue coordinator Stage or delivery commands. Complete only its assigned Work; after successful work finish, end this Turn. The coordinator continues the Stage.", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
764
+ }
744
765
  const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
745
766
  const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
746
767
  if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
@@ -885,7 +906,10 @@ function handleControlledToolEvent(context, input, event, harness) {
885
906
  // Syntax refusal precedes assignment/cwd lookup: an unsupported AST cannot
886
907
  // supply a route or execution authority, including a short public Stage call.
887
908
  const shellRefusal = lifecycle?.analysis.supported === false;
888
- const nativeIdentity = { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
909
+ const proof = context.nativeHookProof;
910
+ if (proof && (proof.harness !== harness || proof.daemonId !== daemonId || proof.sessionId !== providerSessionId || proof.toolCallId !== toolCallId))
911
+ throw new AppError("native_hook_unproven", "Verified native Work callback differs from its physical identity", 1);
912
+ const nativeIdentity = { daemonId, rootSessionId: proof?.rootSessionId ?? nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
889
913
  const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
890
914
  const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
891
915
  ? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
@@ -939,7 +963,7 @@ function handleControlledToolEvent(context, input, event, harness) {
939
963
  let inserted = false;
940
964
  const receipt = observeLifecycleInvocation(context, {
941
965
  currentEventKey: baseKey,
942
- command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
966
+ command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: nativeIdentity,
943
967
  recordReceipt: scope => { inserted = persist(scope); return baseKey; }
944
968
  });
945
969
  const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
@@ -1066,6 +1090,7 @@ function lifecycleFacts(command) {
1066
1090
  const workStart = invocation.operation === "work_start";
1067
1091
  const workFinish = invocation.operation === "work_finish";
1068
1092
  const workFail = invocation.operation === "work_fail";
1093
+ const delivery = invocation.operation.startsWith("delivery_");
1069
1094
  return {
1070
1095
  analysis,
1071
1096
  invocation,
@@ -1076,8 +1101,8 @@ function lifecycleFacts(command) {
1076
1101
  workStart, workFinish, workFail,
1077
1102
  bootstrapStageStart: stageStart && commandHasOption(invocation, "bootstrap"),
1078
1103
  commandProjectRoot: commandOption(invocation, "project-root"),
1079
- stageName: stageStart || stageFinish || stagePause || stageResume ? commandOption(invocation, "stage") : undefined,
1080
- runId: stageStart || stageFinish || stagePause || stageResume ? commandPosition(invocation, 0) : undefined,
1104
+ stageName: stageStart || stageFinish || stagePause || stageResume || delivery ? commandOption(invocation, "stage") : undefined,
1105
+ runId: stageStart || stageFinish || stagePause || stageResume || delivery ? commandPosition(invocation, 0) : undefined,
1081
1106
  workId: stagePause || stageResume ? commandOption(invocation, "work") : workStart || workFinish || workFail ? commandPosition(invocation, 0) : undefined,
1082
1107
  contextSha256: commandOption(invocation, "context-sha256")
1083
1108
  };
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import { Ajv2020 } from "ajv/dist/2020.js";
4
4
  import { Ajv } from "ajv/dist/ajv.js";
5
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
6
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
5
7
  /** Static E1 contracts only. No ingress, dispatch or runtime defaults are changed. */
6
8
  export const hybridContractNames = [
7
9
  "release-manifest", "harness-catalog", "model-catalog", "discovery-result",
@@ -249,7 +251,7 @@ export function validateHybridContract(name, input, context = {}) {
249
251
  fail("foreign_bundle_canon", "/canon");
250
252
  if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
251
253
  fail("foreign_bundle_project_pack", "/project_pack");
252
- if ((data.flow_id === "mb-audit" || data.flow_id === "project-review") !== (data.project_pack === null))
254
+ if ((isSourceSnapshotFlow(data.flow_id) || data.flow_id === "mb-fix" || isDeliveryFlow(data.flow_id)) !== (data.project_pack === null))
253
255
  fail("invalid_bundle_project_pack_role", "/project_pack");
254
256
  const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
255
257
  if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
@@ -289,6 +291,9 @@ export function validateHybridContract(name, input, context = {}) {
289
291
  }
290
292
  };
291
293
  const descriptor = loaded(data.descriptor), settings = loaded(data.execution_settings), profiles = loaded(data.profile_settings);
294
+ if (data.flow_id === "interactive" && (!descriptor || record(descriptor).origin !== "project"
295
+ || record(data.project_pack).source_commit !== record(data.canon).commit))
296
+ fail("foreign_project_flow_provenance", "/project_pack");
292
297
  // Execution receipts are evidence references, not just trusted facts. They
293
298
  // must survive reopening the RUN inside the same frozen blob inventory.
294
299
  for (const reference of [record(settings).attached_session_receipt,
@@ -8,7 +8,7 @@ import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
8
8
  import { admitBundledFlow } from "./stage-handlers.js";
9
9
  import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
10
10
  const limit = 1024 * 1024;
11
- const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review"];
11
+ const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review", "mb-distill", "mb-fix", "review-fix", "release", "deploy", "publish"];
12
12
  const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
13
  function refuse(reason) {
14
14
  throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
@@ -117,6 +117,41 @@ export class PreparedInstalledFlowSource {
117
117
  JSON.parse(content.toString("utf8"));
118
118
  blobs.set(workSchema, Buffer.from(content));
119
119
  }
120
+ if (flowId === "mb-fix" || flowId === "review-fix") {
121
+ for (const logical of [`dd-flow/${flowId}/hybrid/index.md`, "mbb/frontmatter-standards.md", "mbb/cross-references.md", "mbb/named-deferrals-guide.md"]) {
122
+ const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
123
+ if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
124
+ refuse("invalid_support_inventory");
125
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
126
+ folded.set(name.toLowerCase(), name);
127
+ blobs.set(name, Buffer.from(content));
128
+ }
129
+ }
130
+ if (flowId === "mb-distill") {
131
+ // Both lenses need their actual comparison baseline, not only titles.
132
+ const directories = ["mbb", "mbb/aspects", "mbb/templates", "dd-flow/common", "dd-flow/vnext",
133
+ "dd-flow/plan", "dd-flow/code", "dd-flow/merge", "dd-flow/workers", "dd-flow/def",
134
+ "dd-flow/mb-distill/aspects", "dd-flow/mb-distill/process-aspects"];
135
+ const support = [...directories.flatMap(logical => {
136
+ const source = containedFile(memoryRoot, logical);
137
+ directory(source);
138
+ return fs.readdirSync(source).filter(name => name.endsWith(".md")).sort().map(name => `${logical}/${name}`);
139
+ }), "dd-flow/mb-audit.md", "dd-flow/mb-upgrade/index.md"];
140
+ for (const logical of support) {
141
+ const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
142
+ if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
143
+ refuse("invalid_support_inventory");
144
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
145
+ folded.set(name.toLowerCase(), name);
146
+ blobs.set(name, Buffer.from(content));
147
+ }
148
+ const workSchema = "schemas/distill-work-result.schema.json";
149
+ const content = readBoundedReleaseFile(containedFile(flowRoot, workSchema), limit);
150
+ if (!content.length || (total += content.length) > 32 * limit || folded.has(workSchema.toLowerCase()))
151
+ refuse("invalid_support_inventory");
152
+ JSON.parse(content.toString("utf8"));
153
+ blobs.set(workSchema, Buffer.from(content));
154
+ }
120
155
  // Verify compressed/expanded archive identity again after collecting bytes.
121
156
  // A coherent descriptor+prompt edit cannot become a new trusted selection.
122
157
  const final = readSetupStatus(home);
@@ -167,7 +202,7 @@ export class PreparedInstalledFlowSource {
167
202
  || typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
168
203
  || !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
169
204
  || !flowIds.some(id => value.descriptor.path === `${id}/flow-descriptor.json`) || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
170
- || !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 129)
205
+ || !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 512)
171
206
  refuse("invalid_retained_source");
172
207
  const blobs = new Map(), folded = new Set();
173
208
  let total = 0;
@@ -0,0 +1,215 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { parse } from "shell-quote";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { appendAudit } from "./audit.js";
7
+ import { runtimeHarness } from "./execution-policy.js";
8
+ import { hybridDigest } from "./hybrid-contracts.js";
9
+ import { hookSessionIdentity } from "./hooks.js";
10
+ import { prepareOperatorProfile } from "./operator-profiles.js";
11
+ import { findProjectByRoot } from "./projects.js";
12
+ import { parseCommandArgs } from "./lifecycle-command.js";
13
+ const eventType = "interactive.attached_session";
14
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+ function refuse(reason) {
16
+ throw new AppError("interactive_attached_session_required", "Interactive needs its current trusted attached Session; choose managed execution explicitly otherwise", 2, { phase: "prepare", effect: "no_effect", reason, next_action: "provide_matching_attached_session_or_choose_managed" });
17
+ }
18
+ function checkedScope(scope) {
19
+ if (!scope || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(scope.requestId)
20
+ || [scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !path.isAbsolute(value)))
21
+ refuse("invalid_scope");
22
+ try {
23
+ if ([scope.projectRoot, scope.workspace].some(value => fs.realpathSync(value) !== value))
24
+ refuse("noncanonical_scope");
25
+ }
26
+ catch {
27
+ refuse("workspace_unavailable");
28
+ }
29
+ }
30
+ function project(context, scope) {
31
+ const value = findProjectByRoot(context, scope.projectRoot);
32
+ if (!value)
33
+ refuse("registered_project_required");
34
+ return value;
35
+ }
36
+ function launchArguments(context, command) {
37
+ let dynamic = false;
38
+ const entries = parse(command, () => { dynamic = true; return ""; });
39
+ if (dynamic || entries.some(entry => typeof entry !== "string") || /[`\n\r]/.test(command))
40
+ refuse("standalone_launch_required");
41
+ const words = entries;
42
+ let index = 0;
43
+ if (words[0]?.startsWith("DD_FLOW_HOME=")) {
44
+ if (words[0].slice("DD_FLOW_HOME=".length) !== context.ddFlowHome)
45
+ refuse("launch_home_mismatch");
46
+ index++;
47
+ }
48
+ if (path.basename(words[index] ?? "") === "node" && path.basename(words[index + 1] ?? "") === "cli.js")
49
+ index++;
50
+ else if (path.basename(words[index] ?? "") !== "dd-flow")
51
+ refuse("standalone_launch_required");
52
+ return words.slice(index + 1);
53
+ }
54
+ function checkLaunchCommand(context, scope, command) {
55
+ const args = parseCommandArgs(launchArguments(context, command)), option = (key) => args.options.get(key);
56
+ if (args.positional.join(" ") !== "flow interactive" || option("help") || option("execution")?.some(value => value !== "attached")
57
+ || option("request-id")?.length !== 1 || option("request-id")[0] !== scope.requestId
58
+ || option("project-root")?.length !== 1 || option("project-root")[0] !== scope.projectRoot
59
+ || (option("workspace") ? option("workspace").length !== 1 || option("workspace")[0] !== scope.workspace : scope.workspace !== scope.projectRoot))
60
+ refuse("launch_scope_mismatch");
61
+ }
62
+ function binding(context, scope, selected, source, fresh) {
63
+ if (!source || typeof source.eventKey !== "string" || !source.eventKey || typeof source.command !== "string"
64
+ || !source.command.trim() || Buffer.byteLength(source.command) > 32 * 1024)
65
+ refuse("exact_launch_receipt_required");
66
+ checkLaunchCommand(context, scope, source.command);
67
+ const profile = prepareOperatorProfile(selected), selectedProject = project(context, scope);
68
+ const event = context.db.get("SELECT id,event_name,status,sanitized_summary,created_at FROM hook_events WHERE project_id=? AND event_key=?", [selectedProject.id, source.eventKey]);
69
+ if (!event || event.event_name !== "PreToolUse" || event.status !== (fresh ? "observed" : "claimed"))
70
+ refuse("unavailable_launch_receipt");
71
+ const age = Date.parse(context.now()) - Date.parse(event.created_at);
72
+ // Reuse the existing hook admission horizon; historical restore does not
73
+ // pretend that the original launch event proves a currently live Session.
74
+ if (fresh && (!Number.isFinite(age) || age < 0 || age > 60_000))
75
+ refuse("stale_launch_receipt");
76
+ let summary;
77
+ try {
78
+ summary = JSON.parse(event.sanitized_summary);
79
+ }
80
+ catch {
81
+ return refuse("exact_launch_receipt_required");
82
+ }
83
+ if ((summary?.command ?? summary?.tool_input?.command) !== source.command)
84
+ refuse("launch_command_mismatch");
85
+ const identity = hookSessionIdentity(context, selectedProject.id, source.eventKey);
86
+ if (identity.parentSessionId || !identity.providerSessionId || identity.harness !== runtimeHarness(profile))
87
+ refuse("root_session_mismatch");
88
+ if (identity.cwd !== scope.workspace)
89
+ refuse("session_workspace_mismatch");
90
+ if (identity.provider !== profile.provider || identity.model !== profile.model || identity.reasoning !== profile.reasoning
91
+ || identity.mode !== profile.mode)
92
+ refuse("session_profile_mismatch");
93
+ return { schema_id: "dd-flow/interactive-attached-session@1", scope: structuredClone(scope), profile,
94
+ hook_event_id: event.id, event_key: source.eventKey, command: source.command, harness: identity.harness,
95
+ provider_session_id: identity.providerSessionId, session_id: identity.sessionId, daemon_id: identity.daemonId, observed_at: event.created_at };
96
+ }
97
+ function retained(context, scope) {
98
+ const selectedProject = project(context, scope);
99
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.binding.scope.requestId')=? LIMIT 2", [selectedProject.id, eventType, scope.requestId]);
100
+ if (rows.length > 1)
101
+ refuse("ambiguous_attached_binding");
102
+ if (!rows.length)
103
+ return null;
104
+ if (Buffer.byteLength(rows[0].payload_json) > 256 * 1024)
105
+ refuse("invalid_attached_binding");
106
+ let value;
107
+ try {
108
+ value = JSON.parse(rows[0].payload_json);
109
+ }
110
+ catch {
111
+ return refuse("invalid_attached_binding");
112
+ }
113
+ if (!value || !value.binding || !value.binding.scope || Object.keys(value).sort().join() !== "binding,sha256" || hybridDigest(value.binding.scope) !== hybridDigest(scope)
114
+ || hash(Buffer.from(JSON.stringify(value.binding))) !== value.sha256)
115
+ refuse("attached_binding_changed");
116
+ const actual = binding(context, scope, value.binding.profile, { eventKey: value.binding.event_key, command: value.binding.command }, false);
117
+ if (hybridDigest(actual) !== hybridDigest(value.binding))
118
+ refuse("attached_receipt_changed");
119
+ return value;
120
+ }
121
+ /** Produces generic execution receipt facts from the trusted hook ledger, not
122
+ * model-authored receipt JSON or ambient Session/model environment variables.
123
+ * This does not create a RUN or prove independent review/delegation capacity. */
124
+ export class PreparedInteractiveAttachedSession {
125
+ sha256;
126
+ #binding;
127
+ constructor(value) {
128
+ this.#binding = structuredClone(value);
129
+ this.sha256 = hash(Buffer.from(JSON.stringify(value)));
130
+ Object.freeze(this);
131
+ }
132
+ static prepare(context, scope, profile, source) {
133
+ checkedScope(scope);
134
+ if (retained(context, scope))
135
+ refuse("restore_retained_attached_binding");
136
+ return new PreparedInteractiveAttachedSession(binding(context, scope, profile, source, true));
137
+ }
138
+ /** Discover only an exact current argv observation. Same request/root/profile
139
+ * with a different intake or selector is not evidence for this invocation. */
140
+ static prepareCurrent(context, scope, profile, args) {
141
+ checkedScope(scope);
142
+ if (!Array.isArray(args) || args.length > 1024 || args.some(arg => typeof arg !== "string" || arg.includes("\0"))
143
+ || args[0] !== "flow" || args[1] !== "interactive")
144
+ refuse("exact_launch_receipt_required");
145
+ if (retained(context, scope))
146
+ refuse("restore_retained_attached_binding");
147
+ const selectedProject = project(context, scope), matches = [];
148
+ const rows = context.db.all("SELECT event_key,sanitized_summary FROM hook_events WHERE project_id=? AND event_name='PreToolUse' AND status='observed' AND created_at>=? ORDER BY id DESC LIMIT 129", [selectedProject.id, new Date(Date.parse(context.now()) - 60_000).toISOString()]);
149
+ if (rows.length > 128)
150
+ refuse("launch_observation_window_exceeds_bound");
151
+ for (const row of rows) {
152
+ if (typeof row.sanitized_summary !== "string" || Buffer.byteLength(row.sanitized_summary) > 64 * 1024)
153
+ continue;
154
+ let summary;
155
+ try {
156
+ summary = JSON.parse(row.sanitized_summary);
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ const command = summary?.command ?? summary?.tool_input?.command;
162
+ if (typeof command !== "string" || Buffer.byteLength(command) > 32 * 1024)
163
+ continue;
164
+ try {
165
+ if (hybridDigest(launchArguments(context, command)) !== hybridDigest(args))
166
+ continue;
167
+ matches.push(binding(context, scope, profile, { eventKey: row.event_key, command }, true));
168
+ }
169
+ catch (error) {
170
+ if (!(error instanceof AppError) || error.code !== "interactive_attached_session_required")
171
+ throw error;
172
+ }
173
+ }
174
+ if (matches.length !== 1)
175
+ refuse(matches.length ? "ambiguous_launch_receipt" : "unavailable_launch_receipt");
176
+ return new PreparedInteractiveAttachedSession(matches[0]);
177
+ }
178
+ static restore(context, scope) {
179
+ checkedScope(scope);
180
+ const value = retained(context, scope);
181
+ if (!value)
182
+ refuse("attached_binding_missing");
183
+ return new PreparedInteractiveAttachedSession(value.binding);
184
+ }
185
+ snapshot() { return structuredClone(this.#binding); }
186
+ evidence() {
187
+ const value = this.#binding, bytes = Buffer.from(JSON.stringify(value));
188
+ const reference = { path: "receipts/interactive-attached-session.json", sha256: this.sha256 };
189
+ const receipts = new Map([[this.sha256, { kind: "attached-session-binding", ...value.scope,
190
+ harness: value.profile.harness, profileRevision: hybridDigest(value.profile) }]]);
191
+ return { reference, bytes, receipts };
192
+ }
193
+ /** Pair hook claim and custody with the caller's request/RUN writer. */
194
+ retainPreparedSql(context) {
195
+ if (!context.db.isOwnedWriteTransaction?.())
196
+ refuse("owned_writer_required");
197
+ const value = this.#binding, prior = retained(context, value.scope);
198
+ if (prior) {
199
+ if (prior.sha256 !== this.sha256)
200
+ refuse("attached_binding_changed");
201
+ return { changed: false };
202
+ }
203
+ const current = binding(context, value.scope, value.profile, { eventKey: value.event_key, command: value.command }, true);
204
+ if (hybridDigest(current) !== hybridDigest(value))
205
+ refuse("attached_receipt_changed");
206
+ const selectedProject = project(context, value.scope);
207
+ const claimed = context.db.run("UPDATE hook_events SET status='claimed',claimed_at=? WHERE id=? AND project_id=? AND status='observed'", [context.now(), value.hook_event_id, selectedProject.id]);
208
+ if (claimed.changes !== 1)
209
+ refuse("launch_receipt_consumed");
210
+ appendAudit(context, { projectId: selectedProject.id, eventType, payload: { binding: value, sha256: this.sha256 } });
211
+ if (retained(context, value.scope)?.sha256 !== this.sha256)
212
+ refuse("attached_readback_failed");
213
+ return { changed: true };
214
+ }
215
+ }
@@ -0,0 +1,54 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
3
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
4
+ import { restoreInteractiveBeforeVersions } from "./interactive-before-versions.js";
5
+ import { readInteractiveBundleData } from "./interactive-bundle-data.js";
6
+ const logical = "runtime/git-baseline.json";
7
+ function refuse(reason) {
8
+ throw new AppError("interactive_baseline_custody_invalid", "Interactive requires its own retained entry baseline", 2, { reason, effect: "no_effect" });
9
+ }
10
+ function selected(context, scope, bundle, file = logical) {
11
+ if (bundle.manifest.flow_id !== "interactive" || bundle.manifest.run_id !== scope.runId)
12
+ refuse("interactive_bundle_required");
13
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
14
+ return readInteractiveBundleData(bundle, file);
15
+ }
16
+ /** Initial physical observation, not RUN allocation or mutation authority. The
17
+ * factory must revalidate before retaining these bytes and starting work. */
18
+ export function prepareInteractiveGitBaseline(input) {
19
+ const baseline = PreparedInteractiveGitBaseline.capture(input);
20
+ return { path: logical, sha256: baseline.sha256, bytes: baseline.bytes(), baseline };
21
+ }
22
+ /** SQL/in-memory fence for Stage acceptance: no project/Git/filesystem lookup,
23
+ * profile selection, fresh capture or adoption of a caller's replacement JSON. */
24
+ export function assertInteractiveGitBaselineCustody(context, scope, bundle, expectedSha256) {
25
+ if (selected(context, scope, bundle).sha256 !== expectedSha256)
26
+ refuse("baseline_binding_changed");
27
+ }
28
+ /** Historical custody uses the frozen execution scope. Current physical scope
29
+ * and supported relocation must be admitted separately before subsequent work. */
30
+ export function readPreparedInteractiveGitBaseline(context, scope, bundle) {
31
+ return context.db.readSnapshot(() => {
32
+ const { bytes, reference, sha256 } = selected(context, scope, bundle);
33
+ const baseline = PreparedInteractiveGitBaseline.restore(bytes, sha256), value = baseline.snapshot();
34
+ const frozen = bundle.execution.snapshot().scope;
35
+ if (value.project_root !== frozen.projectRoot || value.workspace !== frozen.workspace)
36
+ refuse("baseline_scope_changed");
37
+ return { baseline, sha256,
38
+ reference: { root: "run", path: `execution-bundle/${logical}`, sha256: reference.sha256 } };
39
+ });
40
+ }
41
+ export function assertInteractiveBeforeVersionsCustody(context, scope, bundle, expectedSha256) {
42
+ if (selected(context, scope, bundle, "runtime/before-versions.json").sha256 !== expectedSha256)
43
+ refuse("before_versions_binding_changed");
44
+ }
45
+ /** Initial before-images selected from this same RUN/baseline. Later per-turn
46
+ * before-images require their own accepted Work/packet custody, not bundle edits. */
47
+ export function readPreparedInteractiveBeforeVersions(context, scope, bundle) {
48
+ return context.db.readSnapshot(() => {
49
+ const baseline = readPreparedInteractiveGitBaseline(context, scope, bundle);
50
+ const { bytes, reference, sha256 } = selected(context, scope, bundle, "runtime/before-versions.json");
51
+ return { value: restoreInteractiveBeforeVersions(bytes, sha256, baseline.baseline), sha256,
52
+ reference: { root: "run", path: "execution-bundle/runtime/before-versions.json", sha256: reference.sha256 } };
53
+ });
54
+ }