@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
@@ -0,0 +1,85 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ const partLimit = 1024 * 1024, totalLimit = 32 * partLimit;
4
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
5
+ function refuse(reason) {
6
+ throw new AppError("interactive_bundle_data_invalid", "Interactive data must fit the existing retained bundle limits", 2, { reason, effect: "no_effect" });
7
+ }
8
+ function pathFor(logical, ordinal) { return `${logical}.parts/${String(ordinal).padStart(4, "0")}.bin`; }
9
+ function supported(logical) { if (!["runtime/git-baseline.json", "runtime/before-versions.json", "runtime/work-context.json"].includes(logical))
10
+ refuse("unsupported_data_role"); }
11
+ /** Packaging only, not admission. Keep ordinary small JSON unchanged; large
12
+ * before-images use explicit immutable parts instead of widening global limits. */
13
+ export function putInteractiveBundleData(blobs, logical, input) {
14
+ supported(logical);
15
+ if (!input.length || input.byteLength > totalLimit)
16
+ refuse("unbounded_data");
17
+ const bytes = Buffer.from(input), additions = new Map();
18
+ const sha256 = hash(bytes);
19
+ if (bytes.length <= partLimit)
20
+ additions.set(logical, bytes);
21
+ else {
22
+ const parts = [];
23
+ for (let offset = 0; offset < bytes.length; offset += partLimit) {
24
+ const part = Buffer.from(bytes.subarray(offset, offset + partLimit)), file = pathFor(logical, parts.length);
25
+ additions.set(file, part);
26
+ parts.push({ path: file, bytes: part.length, sha256: hash(part) });
27
+ }
28
+ const index = { schema_id: "dd-flow/interactive-bundle-parts@1", logical_path: logical, bytes: bytes.length, sha256, parts };
29
+ additions.set(logical, Buffer.from(JSON.stringify(index)));
30
+ }
31
+ const existing = new Set([...blobs.keys()].map(name => name.toLowerCase()));
32
+ if ([...additions.keys()].some(name => existing.has(name.toLowerCase())))
33
+ refuse("data_path_collision");
34
+ const total = [...blobs.values(), ...additions.values()].reduce((sum, value) => sum + value.length, 0);
35
+ if (total > totalLimit)
36
+ refuse("bundle_budget_exceeded");
37
+ for (const [file, content] of additions)
38
+ blobs.set(file, content);
39
+ return { sha256, reference: { path: logical, sha256: hash(additions.get(logical)) } };
40
+ }
41
+ /** Reads only the selected inventory, never nearby files or current project
42
+ * bytes. For multipart data reference hashes the physical index; sha256 hashes
43
+ * the reconstructed logical data. They intentionally are not interchangeable. */
44
+ export function readInteractiveBundleData(bundle, logical) {
45
+ supported(logical);
46
+ const physical = (file) => {
47
+ const refs = bundle.manifest.inventory.filter(item => item.path === file);
48
+ if (refs.length !== 1)
49
+ refuse("data_reference_required");
50
+ const bytes = bundle.blob(file);
51
+ if (!bytes.length || bytes.length > partLimit || hash(bytes) !== refs[0].sha256)
52
+ refuse("data_part_changed");
53
+ return { bytes, reference: refs[0] };
54
+ };
55
+ const root = physical(logical);
56
+ let value;
57
+ try {
58
+ value = JSON.parse(root.bytes.toString("utf8"));
59
+ }
60
+ catch {
61
+ value = null;
62
+ }
63
+ if (!value || typeof value !== "object" || value.schema_id !== "dd-flow/interactive-bundle-parts@1")
64
+ return { ...root, sha256: root.reference.sha256 };
65
+ const index = value;
66
+ if (Object.keys(index).sort().join() !== "bytes,logical_path,parts,schema_id,sha256" || index.logical_path !== logical
67
+ || !Number.isSafeInteger(index.bytes) || index.bytes <= partLimit || index.bytes > totalLimit
68
+ || typeof index.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(index.sha256) || !Array.isArray(index.parts)
69
+ || index.parts.length !== Math.ceil(index.bytes / partLimit))
70
+ refuse("invalid_data_index");
71
+ const parts = [];
72
+ for (const [ordinal, part] of index.parts.entries()) {
73
+ if (!part || Object.keys(part).sort().join() !== "bytes,path,sha256" || part.path !== pathFor(logical, ordinal)
74
+ || part.bytes !== Math.min(partLimit, index.bytes - ordinal * partLimit))
75
+ refuse("invalid_data_part");
76
+ const selected = physical(part.path);
77
+ if (selected.reference.sha256 !== part.sha256 || selected.bytes.length !== part.bytes)
78
+ refuse("data_part_changed");
79
+ parts.push(selected.bytes);
80
+ }
81
+ const bytes = Buffer.concat(parts);
82
+ if (bytes.length !== index.bytes || hash(bytes) !== index.sha256)
83
+ refuse("logical_data_changed");
84
+ return { bytes, sha256: index.sha256, reference: root.reference };
85
+ }
@@ -0,0 +1,179 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { hybridDigest } from "./hybrid-contracts.js";
3
+ import { appendAudit } from "./audit.js";
4
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
5
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
6
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
7
+ import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
8
+ import { readInteractiveWorkGit } from "./interactive-work-start.js";
9
+ import { readInteractiveWorkHistory } from "./interactive-work-history.js";
10
+ import { codeCheckProfileHash, projectPolicyCheckDeclarations, runCodeChecks, PreparedCheckEvidence, validateCheckDeclaration } from "./code-checks.js";
11
+ import { memoryCheckEnvironment } from "./memory-verification-checks.js";
12
+ function refuse(reason) {
13
+ throw new AppError("interactive_checks_invalid", "Interactive checks require retained obligations and the exact current patch", 2, { reason, effect: "no_effect" });
14
+ }
15
+ const event = "interactive.check_plan";
16
+ function latest(context, scope) {
17
+ const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? ORDER BY id DESC LIMIT 1", [scope.projectId, event, scope.runId]);
18
+ if (!row)
19
+ return null;
20
+ if (Buffer.byteLength(row.payload_json) > 1024 * 1024)
21
+ refuse("unbounded_check_plan");
22
+ const value = JSON.parse(row.payload_json);
23
+ if (value.run_id !== scope.runId || value.plan?.run_id !== scope.runId || value.plan.schema_id !== "dd-flow/interactive-check-plan@1"
24
+ || value.sha256 !== hybridDigest(value.plan) || !Array.isArray(value.plan.checks))
25
+ refuse("changed_check_plan");
26
+ return value;
27
+ }
28
+ /** One existing aggregate check runner and receipt store. Selection is retained
29
+ * before execution, independently of the Stage proposal being assessed. */
30
+ export class PreparedInteractiveChecks {
31
+ sha256;
32
+ #selected;
33
+ #packet;
34
+ #changes;
35
+ #history;
36
+ #plan;
37
+ #environment;
38
+ constructor(selected, packet, changes, history, plan, environment) {
39
+ this.#selected = selected;
40
+ this.#packet = packet;
41
+ this.#changes = changes;
42
+ this.#history = history;
43
+ this.#plan = structuredClone(plan);
44
+ this.#environment = environment;
45
+ this.sha256 = hybridDigest(plan);
46
+ Object.freeze(this);
47
+ }
48
+ static prepare(context, input) {
49
+ if (context.db.isOwnedWriteTransaction?.())
50
+ refuse("preparation_outside_writer_required");
51
+ if (!["hardening", "review"].includes(input.stage) || !Array.isArray(input.additionalChecks) || input.additionalChecks.length > 4096)
52
+ refuse("invalid_check_selection");
53
+ const selected = readInteractiveStageScope(context, input), { scope, bundle, run } = selected;
54
+ const packet = readDescriptorStageInput(context, scope, bundle), frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
55
+ if (!run.run_root)
56
+ refuse("run_root_required");
57
+ if (selected.selected.needs_scope_refresh)
58
+ refuse("current_scope_required");
59
+ const checks = [...projectPolicyCheckDeclarations(frozen.workspace, "code"), ...projectPolicyCheckDeclarations(frozen.workspace, "readiness")];
60
+ const ids = new Set(checks.map(check => check.id));
61
+ for (const extra of input.additionalChecks) {
62
+ if (!extra || Object.keys(extra).sort().join() !== "command,id,purpose" || typeof extra.id !== "string" || !/^CHK-[A-Za-z0-9-]{1,64}$/.test(extra.id)
63
+ || ids.has(extra.id) || [extra.command, extra.purpose].some(value => typeof value !== "string" || !value.trim() || value.includes("\0") || Buffer.byteLength(value) > 16384))
64
+ refuse("invalid_check_selection");
65
+ ids.add(extra.id);
66
+ checks.push({ ...extra, source: "plan", availability: "available", run_at: "readiness" });
67
+ }
68
+ checks.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
69
+ for (const check of checks)
70
+ validateCheckDeclaration(frozen.workspace, check);
71
+ if (!checks.length && (typeof input.noChecksReason !== "string" || !input.noChecksReason.trim() || input.noChecksReason.includes("\0") || Buffer.byteLength(input.noChecksReason) > 16384))
72
+ refuse("explicit_no_checks_reason_required");
73
+ if (checks.length && input.noChecksReason !== undefined)
74
+ refuse("no_checks_reason_with_checks");
75
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot,
76
+ workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
77
+ const plan = { schema_id: "dd-flow/interactive-check-plan@1", run_id: input.runId, stage: input.stage, attempt: packet.attempt,
78
+ bundle_sha256: bundle.sha256, input_sha256: packet.inputSha256, user_inputs_sha256: selected.selected.inputs.sha256,
79
+ scope_revision: selected.selected.revision, file_changes_sha256: hybridDigest(changes.snapshot().delta.changes),
80
+ profile_sha256: codeCheckProfileHash(frozen.workspace, checks), checks, no_checks_reason: checks.length ? null : input.noChecksReason };
81
+ if (Buffer.byteLength(JSON.stringify({ run_id: plan.run_id, sha256: "a".repeat(64), plan })) > 1024 * 1024)
82
+ refuse("unbounded_check_plan");
83
+ const prepared = new PreparedInteractiveChecks(selected, packet, changes, hybridDigest(readInteractiveWorkHistory(context, scope, bundle)), plan, memoryCheckEnvironment(context, scope.projectId, scope.runId));
84
+ prepared.revalidate(context);
85
+ return prepared;
86
+ }
87
+ static restore(context, input) {
88
+ const selected = readInteractiveStageScope(context, input), retained = latest(context, selected.scope);
89
+ if (!retained || retained.plan.stage !== input.stage)
90
+ refuse("retained_check_plan_required");
91
+ const prepared = this.prepare(context, { ...input, additionalChecks: retained.plan.checks.filter(check => check.source === "plan")
92
+ .map(({ id, command, purpose }) => ({ id, command, purpose })),
93
+ ...(retained.plan.no_checks_reason === null ? {} : { noChecksReason: retained.plan.no_checks_reason }) });
94
+ if (prepared.sha256 !== retained.sha256)
95
+ refuse("retained_check_plan_changed");
96
+ return prepared;
97
+ }
98
+ /** Carry declared obligations into the initial critic's current Stage. The
99
+ * common evidence owner still rejects changed task/patch/policy or missing
100
+ * receipts; this does not execute or bless an author's test claims. */
101
+ static prepareReview(context, input) {
102
+ const selected = readInteractiveStageScope(context, { ...input, stage: "review" }), retained = latest(context, selected.scope);
103
+ if (!retained)
104
+ return null;
105
+ return this.prepare(context, { ...input, stage: "review", additionalChecks: retained.plan.checks.filter(check => check.source === "plan")
106
+ .map(({ id, command, purpose }) => ({ id, command, purpose })),
107
+ ...(retained.plan.no_checks_reason === null ? {} : { noChecksReason: retained.plan.no_checks_reason }) });
108
+ }
109
+ value() { return structuredClone(this.#plan); }
110
+ checkInput() {
111
+ const { bundle_sha256, user_inputs_sha256, scope_revision, file_changes_sha256, profile_sha256, checks } = this.#plan;
112
+ return { projectId: this.#selected.scope.projectId, runId: this.#plan.run_id, runHome: this.#packet.runRoot,
113
+ workspaceRoot: this.#selected.bundle.execution.snapshot().scope.workspace, scope: "aggregate",
114
+ checks: structuredClone(checks), expectedProfileHash: profile_sha256,
115
+ // Stage custody changes do not by themselves invalidate checks of the
116
+ // same task/patch/policy. The common runner still owns reuse eligibility.
117
+ evidenceContext: `interactive:${hybridDigest({ bundle_sha256, user_inputs_sha256, scope_revision, file_changes_sha256, profile_sha256, checks })}`,
118
+ artifactDir: `${this.#selected.bundle.descriptor.stage(this.#plan.stage).directory}/checks/${this.sha256}`,
119
+ ...(this.#environment ? { checkEnvironment: this.#environment } : {}) };
120
+ }
121
+ assertSelection(context) {
122
+ this.#selected.assertCustody(context);
123
+ const { scope, bundle } = this.#selected, run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
124
+ if (run.status !== "running" || index.current_stage !== this.#plan.stage
125
+ || hybridDigest(readDescriptorStageInput(context, scope, bundle)) !== hybridDigest(this.#packet)
126
+ || hybridDigest(readInteractiveWorkHistory(context, scope, bundle)) !== this.#history)
127
+ refuse("check_selection_changed");
128
+ const previous = latest(context, scope);
129
+ if (previous && previous.plan.user_inputs_sha256 === this.#plan.user_inputs_sha256
130
+ && previous.plan.checks.some(check => !this.#plan.checks.some(current => hybridDigest(current) === hybridDigest(check))))
131
+ refuse("check_obligation_dropped");
132
+ }
133
+ revalidate(context) {
134
+ this.#changes.revalidate();
135
+ readInteractiveWorkGit(this.#selected.bundle);
136
+ if (codeCheckProfileHash(this.checkInput().workspaceRoot, this.#plan.checks) !== this.#plan.profile_sha256
137
+ || hybridDigest(memoryCheckEnvironment(context, this.#selected.scope.projectId, this.#plan.run_id) ?? null) !== hybridDigest(this.#environment ?? null))
138
+ refuse("check_policy_changed");
139
+ context.db.readSnapshot(() => this.assertSelection(context));
140
+ }
141
+ retain(context) {
142
+ this.revalidate(context);
143
+ return context.db.writeTransaction(() => {
144
+ this.#changes.assertCustody(context);
145
+ this.assertSelection(context);
146
+ if (latest(context, this.#selected.scope)?.sha256 === this.sha256)
147
+ return { changed: false };
148
+ appendAudit(context, { projectId: this.#selected.scope.projectId, eventType: event, payload: { run_id: this.#plan.run_id, sha256: this.sha256, plan: this.value() } });
149
+ return { changed: true };
150
+ });
151
+ }
152
+ assertCustody(context) {
153
+ this.#changes.assertCustody(context);
154
+ this.assertSelection(context);
155
+ if (latest(context, this.#selected.scope)?.sha256 !== this.sha256)
156
+ refuse("retained_check_plan_required");
157
+ }
158
+ async execute(context) {
159
+ this.retain(context);
160
+ if (!this.#plan.checks.length)
161
+ return [];
162
+ const receipts = await runCodeChecks(context, this.checkInput());
163
+ try {
164
+ this.revalidate(context);
165
+ }
166
+ catch (error) {
167
+ if (error instanceof AppError)
168
+ error.details.effect = "committed";
169
+ throw error;
170
+ }
171
+ return receipts;
172
+ }
173
+ evidence(context) {
174
+ this.revalidate(context);
175
+ if (latest(context, this.#selected.scope)?.sha256 !== this.sha256)
176
+ refuse("retained_check_plan_required");
177
+ return this.#plan.checks.length ? PreparedCheckEvidence.prepare(context, this.checkInput()) : null;
178
+ }
179
+ }
@@ -0,0 +1,33 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { FrozenGenericExecution } from "./generic-execution.js";
3
+ import { PreparedInteractiveAttachedSession } from "./interactive-attached-session.js";
4
+ import { prepareOperatorProfile, resolveOperatorProfile } from "./operator-profiles.js";
5
+ /** Fresh interactive selection only; continuation restores the retained bundle
6
+ * rather than re-reading current defaults or claiming a different Session. */
7
+ export function prepareInteractiveExecution(context, input) {
8
+ const { scope, flow, profiles, selection } = input;
9
+ if (flow.descriptor.id !== "interactive" || Object.hasOwn(selection, "attachedSessionReceipt")) {
10
+ throw new AppError("interactive_execution_invalid", "Interactive execution must use its registered flow and CLI-owned Session binding", 2, { effect: "no_effect" });
11
+ }
12
+ const mode = selection.execution ?? "attached";
13
+ const stageSessionMode = selection.stageSessionMode ?? (mode === "attached" ? "same_session" : "new_session");
14
+ let attached = null;
15
+ let receipts = new Map();
16
+ const receiptBytes = new Map();
17
+ if (mode === "attached") {
18
+ if (!input.source && !input.launchArgs)
19
+ throw new AppError("interactive_attached_session_required", "Interactive defaults to the current trusted Session; select managed execution explicitly when unavailable", 2, { phase: "prepare", effect: "no_effect", next_action: "provide_matching_attached_session_or_choose_managed" });
20
+ if (stageSessionMode !== "same_session")
21
+ throw new AppError("interactive_execution_invalid", "An attached root cannot claim a new physical Session per Stage; choose managed execution for that route", 2, { effect: "no_effect" });
22
+ const profile = prepareOperatorProfile(typeof selection.rootProfile === "object" ? selection.rootProfile : resolveOperatorProfile(profiles, selection.rootProfile));
23
+ attached = input.source ? PreparedInteractiveAttachedSession.prepare(context, scope, profile, input.source)
24
+ : PreparedInteractiveAttachedSession.prepareCurrent(context, scope, profile, input.launchArgs);
25
+ const evidence = attached.evidence();
26
+ receipts = evidence.receipts;
27
+ receiptBytes.set(evidence.reference.sha256, evidence.bytes);
28
+ }
29
+ const execution = FrozenGenericExecution.prepare(flow, profiles, { ...selection, execution: mode,
30
+ stageSessionMode, delegation: selection.delegation ?? { mode: "external", max_parallel: 1 },
31
+ ...(attached ? { attachedSessionReceipt: attached.evidence().reference } : {}) }, scope, receipts);
32
+ return { execution, attached, receipts, receiptBytes };
33
+ }
@@ -0,0 +1,236 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { hybridDigest } from "./hybrid-contracts.js";
6
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
7
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
8
+ import { readAcceptedInteractiveStage } from "./interactive-stage-handlers.js";
9
+ import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
10
+ import { PreparedInteractiveWorkspaceChanges, assertInteractiveUserBasis } from "./interactive-mutation-scope.js";
11
+ import { readInteractiveWorkGit } from "./interactive-work-start.js";
12
+ import { assertReviewedProjectGitInventory } from "./project-git-integration.js";
13
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
14
+ function refuse(reason) {
15
+ throw new AppError("interactive_finish_integration_invalid", "Integration requires the accepted interactive task, review and exact original Git inputs", 2, { reason, effect: "no_effect" });
16
+ }
17
+ /** Frozen SQL/bundle policy, never ambient branch inference or Git authority. */
18
+ export function readInteractiveGitPolicy(context, scope, bundle) {
19
+ if (bundle.manifest.flow_id !== "interactive")
20
+ refuse("interactive_bundle_required");
21
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), logical = "runtime/git-policy.json";
22
+ const references = bundle.manifest.inventory.filter(ref => ref.path === logical), bytes = bundle.blob(logical);
23
+ if (references.length !== 1 || !bytes.length || bytes.length > 32768 || references[0].sha256 !== hash(bytes))
24
+ refuse("frozen_git_policy_required");
25
+ let value;
26
+ try {
27
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
28
+ }
29
+ catch {
30
+ return refuse("invalid_git_policy");
31
+ }
32
+ const frozen = bundle.execution.snapshot().scope;
33
+ if (value?.schema_id !== "dd-flow/interactive-git-policy@1" || Object.keys(value).sort().join() !== "git_executable,git_sha256,policy,schema_id,scope"
34
+ || Object.keys(value.scope ?? {}).sort().join() !== "projectRoot,requestId,workspaceRoot"
35
+ || value.scope?.projectRoot !== run.project_root || value.scope?.workspaceRoot !== run.workspace_root
36
+ || value.scope.projectRoot !== frozen.projectRoot || value.scope.workspaceRoot !== frozen.workspace || value.scope.requestId !== frozen.requestId
37
+ || typeof value.git_executable !== "string" || !path.isAbsolute(value.git_executable) || !/^[a-f0-9]{64}$/.test(value.git_sha256)
38
+ || value.policy?.source !== "explicit_request" || value.policy.push_authorized !== false
39
+ || !["feature_worktree", "integration_branch_direct"].includes(value.policy.route)
40
+ || typeof value.policy.repository !== "string" || !path.isAbsolute(value.policy.repository)
41
+ || !value.policy.workspace_branch || !value.policy.integration_branch
42
+ || value.policy.initial_head !== null && !/^[a-f0-9]{40,64}$/.test(value.policy.initial_head))
43
+ refuse("git_policy_scope_changed");
44
+ return structuredClone(value);
45
+ }
46
+ /** Read-only early inspection; no accepted review, Git write or inclusion
47
+ * approval is required. Resolve any user decision before freezing review. */
48
+ export function observeInteractiveOriginalGitInputs(context, input) {
49
+ if (context.db.isOwnedWriteTransaction?.())
50
+ refuse("physical_observation_outside_writer_required");
51
+ const selected = readInteractiveStageScope(context, input), policy = readInteractiveGitPolicy(context, selected.scope, selected.bundle);
52
+ const git = readInteractiveWorkGit(selected.bundle);
53
+ if (git.git_executable !== policy.git_executable || git.git_sha256 !== policy.git_sha256)
54
+ refuse("selected_git_changed");
55
+ const baseline = selected.selected.baseline.snapshot();
56
+ if (policy.policy.repository !== baseline.repository || policy.policy.workspace_branch !== baseline.branch
57
+ || policy.policy.initial_head !== baseline.head)
58
+ refuse("reviewed_git_baseline_changed");
59
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, selected.scope, selected.bundle, {
60
+ projectRoot: policy.scope.projectRoot, workspace: policy.scope.workspaceRoot, gitExecutable: git.git_executable, env: context.env
61
+ });
62
+ const originalInputs = readOriginalGitInputs(context, policy, selected.selected.active_paths, selected.selected.before_versions);
63
+ changes.revalidate();
64
+ selected.assertCustody(context);
65
+ if (hybridDigest(readInteractiveGitPolicy(context, selected.scope, selected.bundle)) !== hybridDigest(policy))
66
+ refuse("git_policy_scope_changed");
67
+ return { baseline_sha256: selected.selected.baseline.sha256, user_inputs_sha256: selected.selected.inputs.sha256,
68
+ scope_revision: selected.selected.revision, original_inputs: originalInputs };
69
+ }
70
+ function readOriginalGitInputs(context, policy, paths, beforeVersions) {
71
+ if (paths.length > 4096)
72
+ refuse("reviewed_inventory_exceeds_bound");
73
+ const before = new Map(beforeVersions.map(item => [item.path, item]));
74
+ const env = { ...Object.fromEntries(Object.entries(context.env).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" };
75
+ const git = (args) => {
76
+ const result = spawnSync(policy.git_executable, ["-c", "core.fsmonitor=false", "-C", policy.scope.workspaceRoot, ...args], { env, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
77
+ if (result.error || result.signal || result.status !== 0)
78
+ refuse("original_git_inputs_unavailable");
79
+ return result.stdout;
80
+ };
81
+ let total = 0;
82
+ const version = (header, retain) => {
83
+ const match = /^([0-7]{6}) (?:blob )?([a-f0-9]{40,64})(?: 0)?$/.exec(header);
84
+ if (!match || !["100644", "100755", "120000"].includes(match[1]))
85
+ refuse("regular_original_git_input_required");
86
+ const bytes = git(["cat-file", "blob", match[2]]);
87
+ total += bytes.length;
88
+ if (total > 64 * 1024 * 1024)
89
+ refuse("original_git_inputs_exceed_bound");
90
+ return { sha256: hash(bytes), git_mode: match[1], ...(retain ? { base64: bytes.toString("base64") } : {}) };
91
+ };
92
+ const entries = (bytes, retain) => {
93
+ let text;
94
+ try {
95
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
96
+ }
97
+ catch {
98
+ return refuse("original_git_paths_invalid");
99
+ }
100
+ const values = new Map();
101
+ for (const entry of text.split("\0").filter(Boolean)) {
102
+ const tab = entry.indexOf("\t"), logical = entry.slice(tab + 1);
103
+ if (tab < 0 || !paths.includes(logical) || values.has(logical))
104
+ refuse("original_git_paths_invalid");
105
+ values.set(logical, version(entry.slice(0, tab), retain));
106
+ }
107
+ return values;
108
+ };
109
+ const heads = entries(paths.length && policy.policy.initial_head ? git(["ls-tree", "-z", policy.policy.initial_head, "--", ...paths]) : Buffer.alloc(0), false);
110
+ const indexes = entries(paths.length ? git(["ls-files", "--stage", "-z", "--", ...paths]) : Buffer.alloc(0), true);
111
+ const same = (left, right) => left?.sha256 === right?.sha256 && left?.git_mode === right?.git_mode;
112
+ return paths.map(logical => {
113
+ const workspace = before.get(logical);
114
+ if (!workspace)
115
+ refuse("working_change_before_version_missing");
116
+ const head = heads.get(logical) ?? null, index = indexes.get(logical) ?? null;
117
+ const source = workspace.kind === "file" ? { sha256: workspace.sha256, git_mode: workspace.mode & 0o111 ? "100755" : "100644" } : null;
118
+ return { path: logical, workspace: structuredClone(workspace), head, index,
119
+ preexisting_changes: !same(source, head) || !same(index, head) };
120
+ });
121
+ }
122
+ /** Select the exact accepted review and its entry-relative file delta from
123
+ * retained SQL. Works after Git effects: no fresh workspace/HEAD is used to
124
+ * manufacture a replacement reviewed inventory. Dispatch still needs original
125
+ * dirty-input admission, a retained operation intent and physical revalidation. */
126
+ export function prepareInteractiveReviewedGitInputs(context, input) {
127
+ if (context.db.isOwnedWriteTransaction?.())
128
+ refuse("preparation_outside_writer_required");
129
+ const selected = readInteractiveStageScope(context, { ...input, stage: "integration" }), { scope, bundle } = selected;
130
+ const review = readAcceptedInteractiveStage(context, { ...input, stage: "review" });
131
+ const read = (writer) => {
132
+ const policy = readInteractiveGitPolicy(writer, scope, bundle), state = selected.selected;
133
+ const result = review.result, collection = review.proof.collection;
134
+ if (!collection || result.details.review?.verdict !== "accept" || state.needs_scope_refresh
135
+ || result.baseline_sha256 !== state.baseline.sha256 || result.scope_revision !== state.revision
136
+ || result.user_inputs_sha256 !== state.inputs.sha256 || !state.inputs.finish_requested)
137
+ refuse("current_accepted_review_required");
138
+ const work = readCompletedInteractiveWork(writer, { ...scope, workId: collection.work_id }, bundle), packet = work.start.assignment.packet;
139
+ if (packet.stage !== "review" || packet.owner_stage !== "review" || work.result.verdict !== "ready" || work.receipt.changes.length
140
+ || packet.input_sha256 !== result.input_sha256 || packet.baseline_sha256 !== result.baseline_sha256
141
+ || packet.scope_revision !== result.scope_revision || packet.user_inputs_sha256 !== result.user_inputs_sha256
142
+ || packet.file_changes_sha256 !== result.file_changes_sha256)
143
+ refuse("accepted_readonly_critic_required");
144
+ const baseline = state.baseline.snapshot(), delta = state.baseline.changes(work.start.baseline);
145
+ if (delta.head_changed || delta.index_changed || hybridDigest(delta.changes) !== result.file_changes_sha256
146
+ || work.receipt.observed_snapshot_sha256 !== work.start.baseline.sha256
147
+ || policy.policy.repository !== baseline.repository || policy.policy.workspace_branch !== baseline.branch
148
+ || policy.policy.initial_head !== baseline.head)
149
+ refuse("reviewed_git_baseline_changed");
150
+ if (delta.changes.length > 4096)
151
+ refuse("reviewed_inventory_exceeds_bound");
152
+ const originals = new Map(state.before_versions.map(item => [item.path, item]));
153
+ const allowed = new Set(state.active_paths);
154
+ const references = [], removals = [];
155
+ for (const change of delta.changes) {
156
+ const before = originals.get(change.path);
157
+ if (!allowed.has(change.path) || !before
158
+ || change.before && (change.before.kind !== "file" || before.sha256 !== change.before.sha256 || before.mode !== change.before.mode)
159
+ || !change.before && before.kind !== "missing" || change.after && change.after.kind !== "file")
160
+ refuse("reviewed_file_scope_changed");
161
+ const entry = change.after ?? change.before;
162
+ if (!entry?.sha256 || entry.mode === null)
163
+ refuse("reviewed_regular_file_required");
164
+ (change.after ? references : removals).push({ root: "workspace", path: change.path, sha256: entry.sha256,
165
+ git_mode: entry.mode & 0o111 ? "100755" : "100644" });
166
+ }
167
+ // An honest empty patch is a terminal no-op, not permission to merge a
168
+ // pre-existing feature branch or commit unrelated staged/dirty files.
169
+ if (delta.changes.length)
170
+ assertReviewedProjectGitInventory(references, removals);
171
+ return { policy, review_sha256: review.proof.result_sha256, review_result: review.reference, review_work_id: collection.work_id,
172
+ reviewed_snapshot_sha256: work.receipt.observed_snapshot_sha256, reviewed_sources_sha256: hybridDigest(work.start.baseline.snapshot().entries), baseline_sha256: state.baseline.sha256,
173
+ user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision, file_changes_sha256: result.file_changes_sha256,
174
+ changes: delta.changes, inventory: { references, removals }, empty: delta.changes.length === 0 };
175
+ };
176
+ const original = context.db.readSnapshot(() => read(context));
177
+ const assertCustody = (writer) => {
178
+ selected.assertCustody(writer);
179
+ review.assertCustody(writer);
180
+ if (hybridDigest(read(writer)) !== hybridDigest(original))
181
+ refuse("accepted_integration_custody_changed");
182
+ };
183
+ const observeBeforeDispatch = () => {
184
+ if (context.db.isOwnedWriteTransaction?.())
185
+ refuse("physical_observation_outside_writer_required");
186
+ const git = readInteractiveWorkGit(bundle);
187
+ if (git.git_executable !== original.policy.git_executable || git.git_sha256 !== original.policy.git_sha256)
188
+ refuse("selected_git_changed");
189
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: original.policy.scope.projectRoot,
190
+ workspace: original.policy.scope.workspaceRoot, gitExecutable: git.git_executable, env: context.env });
191
+ if (hash(Buffer.from(JSON.stringify(changes.currentSnapshot()))) !== original.reviewed_snapshot_sha256)
192
+ refuse("reviewed_sources_changed");
193
+ assertCustody(context);
194
+ return changes;
195
+ };
196
+ const assertReviewedSources = () => {
197
+ if (context.db.isOwnedWriteTransaction?.())
198
+ refuse("physical_observation_outside_writer_required");
199
+ // Admit the owned HEAD/index effects, not a different working source tree.
200
+ // An unselected dependency changing after checks cannot inherit readiness.
201
+ const observed = selected.selected.baseline.observeChanges({ projectRoot: original.policy.scope.projectRoot,
202
+ workspace: original.policy.scope.workspaceRoot, gitExecutable: original.policy.git_executable, env: context.env }, selected.selected.active_paths);
203
+ if (hybridDigest(observed.current.snapshot().entries) !== original.reviewed_sources_sha256)
204
+ refuse("reviewed_source_footprint_changed");
205
+ assertCustody(context);
206
+ };
207
+ const observeOriginalInputs = () => {
208
+ const changes = observeBeforeDispatch(), policy = original.policy;
209
+ const inputs = readOriginalGitInputs(context, policy, original.changes.map(change => change.path), selected.selected.before_versions);
210
+ changes.revalidate();
211
+ assertCustody(context);
212
+ return inputs;
213
+ };
214
+ /** Semantic interpretation stays with the agent. The CLI verifies that a
215
+ * declared inclusion decision cites genuine delivered user input and covers
216
+ * exactly the pre-existing selected changes. Finish alone is not approval.
217
+ * This value must be retained with the future Git operation's intent before
218
+ * effects; calling this helper itself performs no acceptance or Git writes. */
219
+ const prepareOriginalInputsAgreement = (agreement) => {
220
+ const originals = observeOriginalInputs(), paths = originals.filter(item => item.preexisting_changes).map(item => item.path).sort();
221
+ if (!paths.length) {
222
+ if (agreement)
223
+ throw new AppError("interactive_finish_integration_invalid", "Selected files have no pre-RUN changes; retry integration without --result-file", 2, { reason: "unexpected_existing_input_agreement", phase: "prepare", effect: "no_effect", recoverable: true });
224
+ return { original_inputs: originals, agreement: null };
225
+ }
226
+ if (!agreement)
227
+ throw new AppError("interactive_git_agreement_required", "Selected files contain pre-RUN changes; preserve them or explicitly agree their inclusion before commit", 2, { reason: "preexisting_selected_changes", effect: "no_effect", recoverable: true, paths, continuation: { kind: "existing_input_decision_required" } });
228
+ if (typeof agreement.reason !== "string" || !agreement.reason.trim() || agreement.reason.length > 64 * 1024
229
+ || hybridDigest(agreement.include_existing_paths) !== hybridDigest(paths))
230
+ refuse("exact_existing_input_agreement_required");
231
+ assertInteractiveUserBasis(agreement.basis, selected.selected.inputs);
232
+ assertCustody(context);
233
+ return { original_inputs: originals, agreement: { ...structuredClone(agreement), user_inputs_sha256: original.user_inputs_sha256 } };
234
+ };
235
+ return { original: structuredClone(original), assertCustody, observeBeforeDispatch, assertReviewedSources, observeOriginalInputs, prepareOriginalInputsAgreement };
236
+ }