@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155

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 +23 -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 +273 -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 +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  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 +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  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 +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  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 +214 -24
  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 -4
  190. package/dist/services/retained-native-hook.js +245 -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 +12 -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 +209 -43
  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 +9 -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 +252 -66
  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 +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -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
+ }
@@ -0,0 +1,148 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
6
+ import { ObservedMemorySources } from "./memory-source-observations.js";
7
+ import { memorySourceKey } from "./memory-stage-results.js";
8
+ import { localEnvironmentAllowed } from "./workspace-files.js";
9
+ function refuse(reason) {
10
+ throw new AppError("interactive_before_versions_invalid", "Interactive must retain selected original versions before changing them", 2, { reason, effect: "no_effect" });
11
+ }
12
+ function safe(name) {
13
+ return typeof name === "string" && !!name && name.length <= 4096 && !path.posix.isAbsolute(name) && !path.win32.isAbsolute(name)
14
+ && !name.includes("\\") && !name.includes("\0") && name.split("/").every(part => part && part !== "." && part !== ".."
15
+ && ![".git", ".dd-flow", "node_modules"].includes(part.normalize("NFC").toLowerCase()) && localEnvironmentAllowed(part));
16
+ }
17
+ function absent(root, logical) {
18
+ let current = root;
19
+ const parts = logical.split("/");
20
+ for (let index = 0; index < parts.length; index++) {
21
+ current = path.join(current, parts[index]);
22
+ try {
23
+ const stat = fs.lstatSync(current);
24
+ if (index === parts.length - 1 || !stat.isDirectory())
25
+ refuse("unobserved_existing_source");
26
+ }
27
+ catch (error) {
28
+ if (error.code === "ENOENT")
29
+ return;
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ function rootIdentity(baseline) {
35
+ const value = baseline.snapshot(), actual = fs.lstatSync(value.workspace);
36
+ if (!actual.isDirectory() || fs.realpathSync(value.workspace) !== value.workspace
37
+ || actual.dev !== value.workspace_identity.dev || actual.ino !== value.workspace_identity.ino)
38
+ refuse("workspace_identity_changed");
39
+ return value;
40
+ }
41
+ /** Before-image evidence, not write approval. Reuses the existing bounded,
42
+ * private-file-aware source reader. No backup file, Git object, RUN or project
43
+ * mutation is created; the request/Work owner must retain these bytes first. */
44
+ export class PreparedInteractiveBeforeVersions {
45
+ sha256;
46
+ #value;
47
+ #baseline;
48
+ #observed;
49
+ constructor(value, baseline, observed) {
50
+ if (Buffer.byteLength(JSON.stringify(value)) > 96 * 1024 * 1024)
51
+ refuse("before_versions_exceed_bound");
52
+ this.#value = structuredClone(value);
53
+ this.#baseline = baseline;
54
+ this.#observed = observed;
55
+ this.sha256 = createHash("sha256").update(JSON.stringify(value)).digest("hex");
56
+ Object.freeze(this);
57
+ }
58
+ static capture(baseline, paths) {
59
+ if (!(baseline instanceof PreparedInteractiveGitBaseline) || !Array.isArray(paths) || paths.length > 20_000
60
+ || paths.some(name => !safe(name)) || new Set(paths).size !== paths.length)
61
+ refuse("invalid_selected_paths");
62
+ const source = rootIdentity(baseline), byPath = new Map(source.entries.map(item => [item.path, item]));
63
+ const entries = [], files = [];
64
+ for (const name of [...paths].sort()) {
65
+ const before = byPath.get(name);
66
+ if (!before || before.kind === "missing") {
67
+ absent(source.workspace, name);
68
+ entries.push({ path: name, kind: "missing", mode: null, sha256: null, base64: null });
69
+ }
70
+ else {
71
+ if (before.kind !== "file")
72
+ refuse("regular_file_scope_required");
73
+ const actual = fs.lstatSync(path.join(source.workspace, name));
74
+ if (!actual.isFile() || actual.nlink !== 1 || (actual.mode & 0o7777) !== before.mode)
75
+ refuse("before_version_changed");
76
+ files.push(name);
77
+ entries.push({ path: name, kind: "file", mode: before.mode, sha256: before.sha256, base64: null });
78
+ }
79
+ }
80
+ const observed = ObservedMemorySources.capture({ workspace: source.workspace }, files.map(name => ({ root: "workspace", path: name })));
81
+ const hashes = observed.hashes();
82
+ for (const item of entries.filter(item => item.kind === "file")) {
83
+ if (hashes.get(memorySourceKey("workspace", item.path)) !== item.sha256)
84
+ refuse("before_version_changed");
85
+ item.base64 = observed.bytes({ root: "workspace", path: item.path }).toString("base64");
86
+ }
87
+ const prepared = new PreparedInteractiveBeforeVersions({ schema_id: "dd-flow/interactive-before-versions@1",
88
+ baseline_sha256: baseline.sha256, workspace: source.workspace, entries }, baseline, observed);
89
+ prepared.revalidate();
90
+ return prepared;
91
+ }
92
+ snapshot() { return structuredClone(this.#value); }
93
+ bytes() { return Buffer.from(JSON.stringify(this.#value)); }
94
+ reference() { return { path: "runtime/before-versions.json", sha256: this.sha256 }; }
95
+ revalidate() {
96
+ rootIdentity(this.#baseline);
97
+ this.#observed.revalidate();
98
+ for (const item of this.#value.entries) {
99
+ if (item.kind === "missing")
100
+ absent(this.#value.workspace, item.path);
101
+ else {
102
+ const stat = fs.lstatSync(path.join(this.#value.workspace, item.path));
103
+ if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o7777) !== item.mode)
104
+ refuse("before_version_changed");
105
+ }
106
+ }
107
+ }
108
+ }
109
+ /** Historical parsing against an independently selected retained baseline.
110
+ * Never re-read the now-edited project to reconstruct its original content. */
111
+ export function restoreInteractiveBeforeVersions(bytes, expectedSha256, baseline) {
112
+ if (!(baseline instanceof PreparedInteractiveGitBaseline) || !bytes.length || bytes.length > 96 * 1024 * 1024
113
+ || createHash("sha256").update(bytes).digest("hex") !== expectedSha256)
114
+ refuse("retained_before_versions_changed");
115
+ let value;
116
+ try {
117
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
118
+ }
119
+ catch {
120
+ return refuse("invalid_retained_before_versions");
121
+ }
122
+ const original = baseline.snapshot(), byPath = new Map(original.entries.map(item => [item.path, item]));
123
+ if (!value || Object.keys(value).sort().join() !== "baseline_sha256,entries,schema_id,workspace"
124
+ || value.schema_id !== "dd-flow/interactive-before-versions@1" || value.baseline_sha256 !== baseline.sha256
125
+ || value.workspace !== original.workspace || !Array.isArray(value.entries) || value.entries.length > 20_000)
126
+ refuse("retained_before_scope_changed");
127
+ let previous = "", total = 0;
128
+ for (const item of value.entries) {
129
+ if (!item || Object.keys(item).sort().join() !== "base64,kind,mode,path,sha256" || !safe(item.path) || item.path <= previous)
130
+ refuse("invalid_retained_before_versions");
131
+ const before = byPath.get(item.path);
132
+ if (item.kind === "missing") {
133
+ if (before && before.kind !== "missing" || item.mode !== null || item.sha256 !== null || item.base64 !== null)
134
+ refuse("retained_absence_changed");
135
+ }
136
+ else {
137
+ if (item.kind !== "file" || before?.kind !== "file" || item.mode !== before.mode || item.sha256 !== before.sha256 || typeof item.base64 !== "string")
138
+ refuse("retained_original_changed");
139
+ const content = Buffer.from(item.base64, "base64");
140
+ total += content.length;
141
+ if (content.length > 16 * 1024 * 1024 || total > 64 * 1024 * 1024 || content.toString("base64") !== item.base64
142
+ || createHash("sha256").update(content).digest("hex") !== before.sha256)
143
+ refuse("retained_original_changed");
144
+ }
145
+ previous = item.path;
146
+ }
147
+ return structuredClone(value);
148
+ }