@deksden-com/dd-flow-cli 0.9.0-beta.152 → 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 (226) 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 +114 -16
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +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 +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,245 @@
1
+ import crypto from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, open, readdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { canonicalPath } from "../storage/paths.js";
7
+ import { lifecycleCommandFromPayload } from "./lifecycle-command.js";
8
+ import { matchesIssuedNativeCommand, observedLifecycleInvocation } from "./lifecycle-invocations.js";
9
+ import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
10
+ import { requireProjectByRoot } from "./project-registry.js";
11
+ import { handleAgyEvent, handleCodexHook, handleDroidEvent, handleOpenCodeEvent, handleGrokEvent, handleZcodeEvent } from "./hooks.js";
12
+ import { withDaemonProgressObserver } from "../harness-runtime/lib/daemon-observation.mjs";
13
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
14
+ const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
15
+ async function regularJson(file) {
16
+ try {
17
+ const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
18
+ try {
19
+ if (!(await handle.stat()).isFile())
20
+ return null;
21
+ return object(JSON.parse(await handle.readFile("utf8")));
22
+ }
23
+ finally {
24
+ await handle.close();
25
+ }
26
+ }
27
+ catch (error) {
28
+ if (["ENOENT", "ELOOP", "ENOTDIR"].includes(error.code ?? "") || error instanceof SyntaxError)
29
+ return null;
30
+ throw error;
31
+ }
32
+ }
33
+ async function realDirectory(directory) {
34
+ try {
35
+ const info = await lstat(directory);
36
+ return info.isDirectory() && !info.isSymbolicLink();
37
+ }
38
+ catch (error) {
39
+ if (error.code === "ENOENT")
40
+ return false;
41
+ throw error;
42
+ }
43
+ }
44
+ function payloadCommand(binding) {
45
+ const payload = object(binding.native_payload);
46
+ if (!payload)
47
+ return undefined;
48
+ if (binding.source_harness === "codex-desktop")
49
+ return lifecycleCommandFromPayload(payload) ?? undefined;
50
+ if (binding.source_harness === "grok-acp") {
51
+ const native = object(payload.tool_input ?? payload.toolInput);
52
+ const command = native?.command ?? native?.cmd ?? payload.command;
53
+ return typeof command === "string" ? command : undefined;
54
+ }
55
+ if (binding.source_harness === "zcode-acp") {
56
+ const update = object(object(payload.params)?.update), native = object(update?.rawInput);
57
+ const command = native?.command ?? native?.cmd;
58
+ return typeof command === "string" ? command : undefined;
59
+ }
60
+ const input = object(payload.input);
61
+ const command = input?.command ?? input?.cmd ?? input?.CommandLine;
62
+ return typeof command === "string" ? command : undefined;
63
+ }
64
+ /** The directory is only a locator. The owning daemon must attest the exact
65
+ * binding from its verified native-delivery map before it becomes a receipt. */
66
+ export async function reconcileRetainedNativeHook(context, input) {
67
+ const { scope, invocationId } = input;
68
+ if (!scope.runId || context.env.DD_FLOW_DAEMON_ID !== scope.daemonId)
69
+ return false;
70
+ let owner = null;
71
+ try {
72
+ owner = object(JSON.parse(context.env.DD_FLOW_RUNTIME_OWNER ?? "null"));
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ const stateDir = owner?.state_dir || context.env.DD_FLOW_CODEX_STATE_DIR || context.env.DD_AGY_STATE_DIR || context.env.DD_GROK_STATE_DIR || context.env.DD_ZCODE_STATE_DIR;
78
+ if (typeof stateDir !== "string" || !path.isAbsolute(stateDir) || !await realDirectory(stateDir))
79
+ return false;
80
+ // This is an optional additional observation source. A missing, retiring or
81
+ // unbound daemon must leave the existing hook path intact, never grant it
82
+ // authority and never turn an otherwise valid legacy delivery into a failure.
83
+ let owned;
84
+ let metadata;
85
+ try {
86
+ const state = readNativeDaemonState(stateDir);
87
+ if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId)
88
+ return false;
89
+ const project = requireProjectByRoot(context, scope.projectRoot);
90
+ owned = requireManagedDaemonBinding(context, { stateDir, projectId: project.id, runId: scope.runId });
91
+ metadata = JSON.parse(owned.process.metadata_json);
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ if (metadata.run_generation !== scope.generation)
97
+ return false;
98
+ const operations = path.join(stateDir, "operations");
99
+ if (!await realDirectory(operations))
100
+ return false;
101
+ const candidates = [];
102
+ for (const entry of await readdir(operations, { withFileTypes: true })) {
103
+ if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name))
104
+ continue;
105
+ const directory = path.join(operations, entry.name), hooks = path.join(directory, "hooks");
106
+ if (!await realDirectory(hooks))
107
+ continue;
108
+ const requested = await regularJson(path.join(directory, "requested.json"));
109
+ if (!requested || requested.schema_id !== "dd-flow/daemon-operation@1" || typeof requested.operation_id !== "string"
110
+ || sha256(requested.operation_id) !== entry.name || requested.daemon_id !== scope.daemonId
111
+ || requested.owner_id !== owned.process.owner_id || !["session.create", "session.start", "session.prompt"].includes(String(requested.operation))
112
+ || requested.session_id && requested.session_id !== scope.rootSessionId)
113
+ continue;
114
+ for (const hook of await readdir(hooks, { withFileTypes: true })) {
115
+ if (!hook.isFile() || !/^[A-Za-z0-9-]{1,128}\.json$/.test(hook.name))
116
+ continue;
117
+ const binding = await regularJson(path.join(hooks, hook.name));
118
+ if (!binding || binding.schema_id !== "dd-flow/hook-request@1" || binding.request_id !== hook.name.slice(0, -5)
119
+ || binding.daemon_id !== scope.daemonId || binding.operation_id !== requested.operation_id
120
+ || binding.root_provider_session_id !== scope.rootSessionId || !["codex-desktop", "antigravity-cli", "grok-acp", "zcode-acp", "opencode-server", "droid-cli"].includes(String(binding.source_harness))
121
+ || typeof binding.provider_session_id !== "string" || !binding.provider_session_id
122
+ || !(binding.parent_provider_session_id === null || typeof binding.parent_provider_session_id === "string")
123
+ || typeof binding.tool_call_id !== "string" || !binding.tool_call_id || binding.event !== "PreToolUse"
124
+ || typeof binding.native_command !== "string" || binding.command_sha256 !== sha256(binding.native_command)
125
+ || payloadCommand(binding) !== binding.native_command || !matchesIssuedNativeCommand(context, invocationId, binding.native_command))
126
+ continue;
127
+ candidates.push(binding);
128
+ }
129
+ }
130
+ const verified = [];
131
+ for (const binding of candidates) {
132
+ try {
133
+ const module = binding.source_harness === "codex-desktop"
134
+ ? await import("../harness-runtime/lib/dd-codex-daemon.mjs")
135
+ : binding.source_harness === "grok-acp" ? await import("../harness-runtime/lib/dd-grok-daemon.mjs")
136
+ : binding.source_harness === "zcode-acp" ? await import("../harness-runtime/lib/dd-zcode-daemon.mjs")
137
+ // @ts-expect-error Executable adapter assets have no duplicate typed API.
138
+ : binding.source_harness === "opencode-server" ? await import("../harness-runtime/lib/dd-opencode-daemon.mjs")
139
+ // @ts-expect-error Executable adapter assets have no duplicate typed API.
140
+ : binding.source_harness === "droid-cli" ? await import("../harness-runtime/lib/dd-droid-daemon.mjs")
141
+ : await import("../harness-runtime/lib/dd-agy-daemon.mjs");
142
+ const proof = await withDaemonProgressObserver(progress => {
143
+ if (typeof progress.native_request_id === 'string' && typeof progress.observed_at === 'string')
144
+ input.onProgress?.({ cursor: `${binding.request_id}:${progress.native_request_id}`, observed_at: Date.parse(progress.observed_at) });
145
+ }, () => module.callDaemon(stateDir, "hook.verify", { daemonId: scope.daemonId, requestId: binding.request_id, binding }, 5_000));
146
+ if (binding.native_admission === "assigned_work_start") {
147
+ const resolved = object(proof.nativeHookBinding);
148
+ if (proof.verified !== true || JSON.stringify(proof.registeredHookBinding) !== JSON.stringify(binding) || !resolved
149
+ || !["antigravity-cli", "opencode-server", "droid-cli"].includes(String(binding.source_harness))
150
+ || resolved.native_ancestry !== "confirmed" || typeof resolved.native_cwd !== "string" || !path.isAbsolute(resolved.native_cwd)
151
+ || !(resolved.parent_provider_session_id === null || typeof resolved.parent_provider_session_id === "string")
152
+ || ["request_id", "operation_id", "daemon_id", "turn_generation", "source_harness", "root_provider_session_id", "provider_session_id",
153
+ "tool_call_id", "event", "native_command", "command_sha256", "native_creator", "native_admission"].some(key => resolved[key] !== binding[key])
154
+ || payloadCommand(resolved) !== binding.native_command)
155
+ continue;
156
+ verified.push(resolved);
157
+ }
158
+ else if (proof.verified === true && JSON.stringify(proof.nativeHookBinding) === JSON.stringify(binding))
159
+ verified.push(binding);
160
+ }
161
+ catch { /* Legacy/retired daemons cannot attest a retained fact. */ }
162
+ }
163
+ // Exact old tool delivery belongs to its immutable prior attempt. A public
164
+ // command may be identical in a fresh no-effect successor; do not repeatedly
165
+ // select its predecessor and starve the new native callback.
166
+ const eligible = verified.filter(binding => observedLifecycleInvocation(context, binding.native_command, scope, { identity: {
167
+ daemonId: scope.daemonId, rootSessionId: scope.rootSessionId, sessionId: binding.provider_session_id,
168
+ parentSessionId: binding.parent_provider_session_id, toolCallId: binding.tool_call_id
169
+ } })?.id === invocationId);
170
+ if (!eligible.length)
171
+ return false;
172
+ const identity = (binding) => JSON.stringify([binding.source_harness, binding.daemon_id, binding.root_provider_session_id,
173
+ binding.provider_session_id, binding.parent_provider_session_id, binding.native_cwd]);
174
+ if (new Set(eligible.map(identity)).size !== 1)
175
+ throw new AppError("invocation_ambiguous", "More than one native executor supplied this issued Work command", 1, { effect: "no_effect", invocation_id: invocationId });
176
+ const binding = eligible[0], payload = object(binding.native_payload);
177
+ if (binding.source_harness === "codex-desktop") {
178
+ const proof = object(binding.codex_proof);
179
+ if (!proof || proof.daemonId !== scope.daemonId || proof.rootSessionId !== scope.rootSessionId
180
+ || proof.sessionId !== binding.provider_session_id || proof.parentSessionId !== binding.parent_provider_session_id
181
+ || proof.toolCallId !== binding.tool_call_id || typeof proof.projectRoot !== "string"
182
+ || canonicalPath(proof.projectRoot) !== canonicalPath(scope.projectRoot) || typeof proof.ddFlowHome !== "string"
183
+ || canonicalPath(proof.ddFlowHome) !== canonicalPath(context.ddFlowHome))
184
+ return false;
185
+ const prior = context.codexHookProof;
186
+ try {
187
+ context.codexHookProof = { daemonId: scope.daemonId, rootSessionId: scope.rootSessionId,
188
+ sessionId: binding.provider_session_id, parentSessionId: binding.parent_provider_session_id,
189
+ toolCallId: binding.tool_call_id, projectRoot: proof.projectRoot, command: binding.native_command };
190
+ handleCodexHook(context, { event: "PreToolUse", projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
191
+ }
192
+ finally {
193
+ if (prior)
194
+ context.codexHookProof = prior;
195
+ else
196
+ delete context.codexHookProof;
197
+ }
198
+ }
199
+ else if (binding.source_harness === "grok-acp") {
200
+ const identity = object(object(payload._meta)?.ddGrok);
201
+ if (!identity || identity.daemonId !== scope.daemonId || identity.rootProviderSessionId !== scope.rootSessionId
202
+ || identity.parentProviderSessionId !== binding.parent_provider_session_id
203
+ || (payload.session_id ?? payload.sessionId) !== binding.provider_session_id
204
+ || (payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id) !== binding.tool_call_id)
205
+ return false;
206
+ handleGrokEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
207
+ }
208
+ else if (binding.source_harness === "zcode-acp") {
209
+ const controlled = object(object(payload._meta)?.ddZcode), update = object(object(payload.params)?.update), native = object(object(update?._meta)?.zcodeRuntime);
210
+ if (!controlled || controlled.daemonId !== scope.daemonId || controlled.rootProviderSessionId !== scope.rootSessionId
211
+ || controlled.parentProviderSessionId !== binding.parent_provider_session_id || native?.source !== "subagent"
212
+ || native.childSessionId !== binding.provider_session_id || native.childToolCallId !== binding.tool_call_id
213
+ || typeof update?.toolCallId !== "string" || !update.toolCallId || typeof native.nativeTurnId !== "string" || !native.nativeTurnId)
214
+ return false;
215
+ handleZcodeEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
216
+ }
217
+ else if (binding.native_admission === "assigned_work_start") {
218
+ const harness = binding.source_harness, session = object(payload.session);
219
+ if (payload.daemon_id !== scope.daemonId || (harness === "antigravity-cli" ? payload.conversation_id !== binding.provider_session_id
220
+ || (payload.parent_conversation_id ?? null) !== binding.parent_provider_session_id || payload.event_id !== binding.tool_call_id
221
+ : !session || session.provider_session_id !== binding.provider_session_id || (session.parent_provider_session_id ?? null) !== binding.parent_provider_session_id
222
+ || payload.tool_call_id !== binding.tool_call_id))
223
+ return false;
224
+ const prior = context.nativeHookProof;
225
+ try {
226
+ context.nativeHookProof = { harness, daemonId: scope.daemonId, rootSessionId: scope.rootSessionId, sessionId: binding.provider_session_id,
227
+ toolCallId: binding.tool_call_id };
228
+ const handle = harness === "antigravity-cli" ? handleAgyEvent : harness === "droid-cli" ? handleDroidEvent : handleOpenCodeEvent;
229
+ handle(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
230
+ }
231
+ finally {
232
+ if (prior)
233
+ context.nativeHookProof = prior;
234
+ else
235
+ delete context.nativeHookProof;
236
+ }
237
+ }
238
+ else {
239
+ if (payload.daemon_id !== scope.daemonId || payload.conversation_id !== binding.provider_session_id
240
+ || (payload.parent_conversation_id ?? null) !== binding.parent_provider_session_id || payload.event_id !== binding.tool_call_id)
241
+ return false;
242
+ handleAgyEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
243
+ }
244
+ return true;
245
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { resolveProjectRoot } from "../storage/paths.js";
4
+ import { requireProjectByRoot } from "./projects.js";
5
+ import { readFlowRunExecutionBundle } from "./runs.js";
6
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
7
+ import { readReviewFixSourceIntake, assertReviewFixSourceIntakeCustody } from "./review-fix-intake-custody.js";
8
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
9
+ import { hybridDigest } from "./hybrid-contracts.js";
10
+ import { assertReviewFixStageScope, validateReviewFixStageResult } from "./review-fix-stage-results.js";
11
+ function refuse(reason) {
12
+ throw new AppError("review_fix_handoff_invalid", "Handoff requires accepted scope review and unchanged original user inputs", 2, { reason, effect: "no_effect" });
13
+ }
14
+ /** Prepare an SQL-fenced raw packet for the ordinary Stage settlement writer.
15
+ * No files, RUNs, PRTs or specifications are created. A ready scope is not
16
+ * authorization to formalize; the user must separately request that action.
17
+ */
18
+ export function prepareReviewFixHandoff(context, input) {
19
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
20
+ const scope = { projectId: project.id, runId: input.runId };
21
+ const bundle = readFlowRunExecutionBundle(context, input);
22
+ if (!bundle || bundle.manifest.flow_id !== "review-fix")
23
+ refuse("review_fix_bundle_required");
24
+ const source = readReviewFixSourceIntake(context, input, bundle);
25
+ const read = (reader) => {
26
+ const intake = readAcceptedStageData(reader, { ...scope, stage: "intake" });
27
+ const reviewed = readAcceptedStageData(reader, { ...scope, stage: "scope-review" });
28
+ const users = readPreparedMemoryFlowUserInputs(reader, scope, "review-fix", bundle);
29
+ return { intake, reviewed, users };
30
+ };
31
+ const original = context.db.readSnapshot(() => read(context));
32
+ let prior;
33
+ try {
34
+ prior = JSON.parse(original.intake.bytes.toString("utf8"));
35
+ }
36
+ catch {
37
+ return refuse("accepted_intake_invalid");
38
+ }
39
+ if (original.intake.data_schema_id !== "dd-flow/review-fix-stage-result@1"
40
+ || original.reviewed.data_schema_id !== "dd-flow/review-fix-stage-result@1"
41
+ || prior?.schema_id !== "dd-flow/review-fix-stage-result@1" || prior.flow_id !== "review-fix"
42
+ || prior.run_id !== input.runId || prior.stage !== "intake" || prior.intake_sha256 !== source.sha256
43
+ || prior.input_sha256 !== original.intake.input_sha256 || !original.reviewed.input_sha256)
44
+ refuse("accepted_intake_invalid");
45
+ const reviewed = validateReviewFixStageResult(original.reviewed.bytes, bundle.descriptor.stageBlob("scope-review", "result_schema"), {
46
+ runId: input.runId, stage: "scope-review", inputSha256: original.reviewed.input_sha256, intakeSha256: source.sha256,
47
+ snapshotSha256: prior.snapshot_sha256, userInputsSha256: hybridDigest(original.users),
48
+ findingIds: source.intake.selected_findings.map(finding => finding.id),
49
+ predecessors: [{ stage: "intake", ...original.intake.result }],
50
+ userSources: original.users.sources.map(item => {
51
+ if (!["request", "question", "answer"].includes(item.role))
52
+ return refuse("invalid_user_input_role");
53
+ return { root: item.root, path: item.path, sha256: item.sha256, role: item.role };
54
+ })
55
+ });
56
+ assertReviewFixStageScope(reviewed, prior);
57
+ const disposition = reviewed.details.review.some(item => item.verdict === "revise") ? "needs_scope"
58
+ : reviewed.details.decisions.some(item => item.disposition === "approved") ? "ready"
59
+ : reviewed.details.decisions.some(item => item.disposition === "deferred") ? "deferred" : "declined";
60
+ const bytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/review-fix-handoff@1", run_id: input.runId,
61
+ disposition, formalization_authorized: false, next_entry: disposition === "ready" ? "vnext/start.md" : null,
62
+ source_intake: source.intake, user_inputs: original.users,
63
+ scope: reviewed.details, accepted_review: { ...original.reviewed.result, authority_sha256: original.reviewed.authority_sha256 }
64
+ }));
65
+ // Keep this packet within the existing Stage reader envelope; never truncate
66
+ // original messages or findings to make an oversized handoff appear valid.
67
+ if (bytes.length > 4 * 1024 * 1024)
68
+ refuse("handoff_exceeds_bound");
69
+ return {
70
+ path: `${bundle.descriptor.stage("handoff").directory}/raw-intake.json`, bytes,
71
+ sha256: createHash("sha256").update(bytes).digest("hex"),
72
+ assertCustody(writer) {
73
+ assertReviewFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
74
+ const current = read(writer);
75
+ if (!current.intake.bytes.equals(original.intake.bytes) || !current.reviewed.bytes.equals(original.reviewed.bytes)
76
+ || current.intake.authority_sha256 !== original.intake.authority_sha256
77
+ || current.reviewed.authority_sha256 !== original.reviewed.authority_sha256
78
+ || hybridDigest(current.users) !== hybridDigest(original.users))
79
+ refuse("accepted_handoff_changed");
80
+ }
81
+ };
82
+ }
@@ -0,0 +1,17 @@
1
+ import { assertDiagnosticRepairSourceIntakeCustody, prepareDiagnosticRepairSourceIntake, readDiagnosticRepairSourceIntake } from "./diagnostic-repair-source-intake.js";
2
+ import { readMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
3
+ // Preserve this flow's own capsule identity; shared transport grants no approval.
4
+ export function assertReviewFixSourceIntakeCustody(context, scope, bundle, expectedSha256) {
5
+ return assertDiagnosticRepairSourceIntakeCustody(context, scope, bundle, expectedSha256, "review-fix");
6
+ }
7
+ export function prepareReviewFixSourceIntake(context, input) {
8
+ return prepareDiagnosticRepairSourceIntake(context, input, "review-fix");
9
+ }
10
+ export function readReviewFixSourceIntake(context, input, retainedBundle) {
11
+ return readDiagnosticRepairSourceIntake(context, input, "review-fix", retainedBundle);
12
+ }
13
+ /** Original request and all delivered answers, not an agent's replacement spec.
14
+ * Reading these bytes neither accepts scope nor authorizes SDLC formalization. */
15
+ export function readReviewFixUserInputs(context, input, retainedBundle) {
16
+ return readMemoryFlowUserInputs(context, input, "review-fix", retainedBundle);
17
+ }
@@ -0,0 +1,43 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { reviewFixStages } from "../domain/review-fix-flow.js";
3
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
4
+ import { requireProjectByRoot } from "./projects.js";
5
+ import { resolveProjectRoot } from "../storage/paths.js";
6
+ import { readReviewFixSourceIntake, assertReviewFixSourceIntakeCustody } from "./review-fix-intake-custody.js";
7
+ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
8
+ import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
9
+ import { readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
10
+ import { hybridDigest } from "./hybrid-contracts.js";
11
+ /** Internal family entry. Generic runtime owns attempts, Work and coordinator;
12
+ * this does not register a public flow or authorize a downstream SDLC start. */
13
+ export function readReviewFixStageScope(context, input, historical = false) {
14
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
15
+ const bundle = readFlowRunExecutionBundle(context, input);
16
+ if (!bundle || bundle.manifest.flow_id !== "review-fix" || !reviewFixStages.includes(input.stage))
17
+ throw new AppError("review_fix_stage_invalid", "Review-fix requires its registered three-stage flow and retained intake", 2, { reason: "review_fix_stage_required", effect: "no_effect" });
18
+ bundle.descriptor.stage(input.stage);
19
+ const source = readReviewFixSourceIntake(context, input, bundle);
20
+ const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
21
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
22
+ const replay = historical && JSON.parse(run.index_json).stage_runs.some((stage) => stage.stage === input.stage && stage.status === "done");
23
+ const frozen = bundle.execution.snapshot().scope;
24
+ const snapshotSelection = { projectId: project.id, projectRoot: frozen.projectRoot, sourceRoot: frozen.projectRoot,
25
+ requestId: frozen.requestId, flowId: "review-fix" };
26
+ const snapshot = (replay ? readDiagnosticSnapshotCustody : readDiagnosticSnapshot)(context, snapshotSelection, input.runId);
27
+ if (!snapshot || snapshot.root !== frozen.workspace)
28
+ throw new AppError("review_fix_stage_invalid", "Review-fix requires its unchanged private source snapshot", 2, { reason: "snapshot_required", effect: "no_effect" });
29
+ return { bundle, source, scope, run, replay, snapshot, snapshotSelection,
30
+ assertCustody(writer) {
31
+ assertReviewFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
32
+ if (hybridDigest(readDiagnosticSnapshotCustody(writer, snapshotSelection, input.runId)) !== hybridDigest(snapshot))
33
+ throw new AppError("review_fix_stage_invalid", "Review-fix source snapshot custody changed", 2, { reason: "snapshot_changed", effect: "no_effect" });
34
+ }
35
+ };
36
+ }
37
+ export function startReviewFixStage(context, input) {
38
+ const selected = readReviewFixStageScope(context, input), { source } = selected;
39
+ const fence = selected.assertCustody;
40
+ const start = PreparedDescriptorStageStart.prepare(context, input).apply(context, fence);
41
+ const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, fence);
42
+ return { ...start, ...coordinator, intake_sha256: source.sha256, source_intake: source.reference };
43
+ }
@@ -0,0 +1,176 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { entityReferenceVariants } from "../shared/entity-references.js";
5
+ import { reviewFixStages } from "../domain/review-fix-flow.js";
6
+ import { readReviewFixStageScope, startReviewFixStage } from "./review-fix-stage-entry.js";
7
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
8
+ import { PreparedDescriptorStageSettlement, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
9
+ import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
10
+ import { readAcceptedStageData, prepareStagePacket, acceptStagePacket, prepareStageReport, acceptStageReport, publishStagePacket, publishStageReport, resolveStagePublicationPath } from "./stage-report-renderer.js";
11
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
12
+ import { assertReviewFixStageScope, validateReviewFixStageResult, reviewFixHandoffDisposition } from "./review-fix-stage-results.js";
13
+ import { prepareReviewFixHandoff } from "./review-fix-handoff.js";
14
+ import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
15
+ import { readDiagnosticSnapshot } from "./diagnostic-snapshot.js";
16
+ import { stageLifecycleMatchKey } from "./hooks.js";
17
+ import { hybridDigest } from "./hybrid-contracts.js";
18
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
19
+ function refuse(reason) {
20
+ throw new AppError("review_fix_stage_invalid", "Review-fix requires original scope, independent review and a raw handoff", 2, { reason, effect: "no_effect" });
21
+ }
22
+ export const reviewFixStageHandlers = Object.freeze(reviewFixStages.map(stage => Object.freeze({ flowId: "review-fix", stage,
23
+ entryId: "review-fix-stage-entry", finishId: "review-fix-stage-finish", entry: startReviewFixStage, finish: finishReviewFixStage })));
24
+ /** Read retained raw bytes for terminal replay, never recapture newer messages. */
25
+ function readHandoff(context, scope, attempt, file) {
26
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='handoff' AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [scope.projectId, scope.runId, attempt]);
27
+ if (rows.length > 32)
28
+ refuse("handoff_custody_invalid");
29
+ const packets = rows.map(row => {
30
+ if (Buffer.byteLength(row.payload_json) > 6 * 1024 * 1024)
31
+ refuse("handoff_exceeds_bound");
32
+ return JSON.parse(row.payload_json);
33
+ }).filter(packet => typeof packet.file === "string" && resolveStagePublicationPath(context, scope.projectId, scope.runId, packet.file) === file);
34
+ if (!packets.length)
35
+ return null;
36
+ if (packets.length !== 1 || typeof packets[0].base64 !== "string")
37
+ refuse("accepted_handoff_missing");
38
+ const bytes = Buffer.from(packets[0].base64, "base64");
39
+ if (!bytes.length || bytes.length > 4 * 1024 * 1024 || bytes.toString("base64") !== packets[0].base64 || hash(bytes) !== packets[0].sha256)
40
+ refuse("accepted_handoff_changed");
41
+ return { bytes, sha256: hash(bytes) };
42
+ }
43
+ /** Generic Stage/RUN settlement remains the sole lifecycle owner. This handler
44
+ * writes only retained runtime packets/reports, never project files or SDLC. */
45
+ export function finishReviewFixStage(context, input) {
46
+ if (!input.bytes.length || input.bytes.length > 1024 * 1024)
47
+ refuse("result_exceeds_bound");
48
+ const selected = readReviewFixStageScope(context, input, true), { bundle, source, scope, replay, snapshot } = selected;
49
+ const packet = readDescriptorStageInput(context, scope, bundle, true, true);
50
+ const stage = input.stage;
51
+ const previousStage = stage === "scope-review" ? "intake" : stage === "handoff" ? "scope-review" : null;
52
+ const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
53
+ const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
54
+ if (previous && (packet.input.predecessor?.stage !== previousStage || hybridDigest(packet.input.predecessor.result) !== hybridDigest(previous.result)))
55
+ refuse("predecessor_changed");
56
+ const bytes = Buffer.from(input.bytes);
57
+ let result;
58
+ const users = !replay ? readPreparedMemoryFlowUserInputs(context, scope, "review-fix", bundle) : null;
59
+ if (replay) {
60
+ const accepted = readAcceptedStageData(context, scope);
61
+ if (!accepted.bytes.equals(bytes))
62
+ refuse("accepted_result_changed");
63
+ result = JSON.parse(accepted.bytes.toString("utf8"));
64
+ }
65
+ else {
66
+ result = validateReviewFixStageResult(bytes, bundle.descriptor.stageBlob(stage, "result_schema"), {
67
+ runId: input.runId, stage, inputSha256: packet.inputSha256, intakeSha256: source.sha256,
68
+ snapshotSha256: snapshot.source_sha256, userInputsSha256: hybridDigest(users),
69
+ findingIds: source.intake.selected_findings.map(finding => finding.id), predecessors: previous ? [{ stage: previousStage, ...previous.result }] : [],
70
+ userSources: users.sources.map(item => {
71
+ if (!["request", "question", "answer"].includes(item.role))
72
+ return refuse("invalid_user_input_role");
73
+ return { root: item.root, path: item.path, sha256: item.sha256, role: item.role };
74
+ })
75
+ });
76
+ if (source.intake.limitations.some(item => !result.details.limitations.includes(item)))
77
+ refuse("source_limitations_missing");
78
+ if (prior)
79
+ assertReviewFixStageScope(result, prior);
80
+ }
81
+ const settlement = PreparedDescriptorStageSettlement.prepare(context, input);
82
+ const keys = (name) => entityReferenceVariants(input.runId).map(ref => stageLifecycleMatchKey("stage_finish", ref, name, input.projectRoot));
83
+ const intakeKeys = stage !== "intake" ? keys("intake") : [];
84
+ const reviewKeys = stage === "handoff" ? keys("scope-review") : [];
85
+ const readOwners = (reader) => {
86
+ const physical = (id) => {
87
+ const session = reader.db.get("SELECT harness,provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=?", [scope.projectId, scope.runId, id]);
88
+ if (!session?.harness || !session.provider_session_id)
89
+ refuse("physical_owner_missing");
90
+ return session;
91
+ };
92
+ const intake = stage !== "intake" ? readAcceptedDescriptorStageSettlement(reader, { ...scope, stage: "intake" }, bundle, intakeKeys) : null;
93
+ const review = stage === "handoff" ? readAcceptedDescriptorStageSettlement(reader, { ...scope, stage: "scope-review" }, bundle, reviewKeys) : null;
94
+ if (intake)
95
+ assertDiagnosticSessionIndependence([physical(review ? review.binding.sessionId : settlement.coordinatorSessionId())], [physical(intake.binding.sessionId)]);
96
+ return { intake, review };
97
+ };
98
+ const owners = context.db.readSnapshot(() => readOwners(context));
99
+ const handoff = stage === "handoff" && !replay ? prepareReviewFixHandoff(context, input) : null;
100
+ const reportRoot = path.join(packet.runRoot, bundle.descriptor.stage(stage).directory), handoffFile = path.join(reportRoot, "raw-intake.json");
101
+ const retained = stage === "handoff" && replay ? readHandoff(context, scope, packet.attempt, handoffFile) : null;
102
+ if (stage === "handoff" && replay && !retained)
103
+ refuse("accepted_handoff_missing");
104
+ const raw = handoff ?? retained;
105
+ const rawPacket = handoff ? prepareStagePacket(handoffFile, handoff.bytes) : null;
106
+ const report = stage === "handoff" && raw ? prepareStageReport({ run_id: input.runId, stage, verdict: "completed", summary: result.summary,
107
+ review_fix: { disposition: reviewFixHandoffDisposition(result), formalization_authorized: false,
108
+ source: source.intake.source, source_intake: source.reference, unselected_ids: source.intake.unselected_ids,
109
+ scope: result.details, raw_intake: { root: "run", path: `${bundle.descriptor.stage(stage).directory}/raw-intake.json`, sha256: raw.sha256 } },
110
+ semantic: { next_action: "No repair or SDLC was started. Ready scope requires a separate explicit user formalization through vnext/start.md; deferred/declined/needs_scope remain honest outcomes." }
111
+ }) : null;
112
+ if (report && Buffer.byteLength(JSON.stringify(report.bytes)) > 7 * 1024 * 1024)
113
+ refuse("terminal_report_exceeds_bound");
114
+ if (!replay && hybridDigest(readDiagnosticSnapshot(context, selected.snapshotSelection, input.runId)) !== hybridDigest(snapshot))
115
+ refuse("snapshot_changed");
116
+ const fence = (writer) => {
117
+ selected.assertCustody(writer);
118
+ if (previous && hybridDigest(readAcceptedStageData(writer, { ...scope, stage: previousStage })) !== hybridDigest(previous))
119
+ refuse("predecessor_changed");
120
+ if (hybridDigest(readOwners(writer)) !== hybridDigest(owners))
121
+ refuse("review_owner_changed");
122
+ if (users && hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "review-fix", bundle)) !== hybridDigest(users))
123
+ refuse("delivered_inputs_changed");
124
+ handoff?.assertCustody(writer);
125
+ if (rawPacket && !replay) {
126
+ const existing = readHandoff(writer, scope, packet.attempt, handoffFile);
127
+ if (existing && !existing.bytes.equals(rawPacket.bytes))
128
+ refuse("accepted_handoff_changed");
129
+ if (!existing)
130
+ acceptStagePacket(writer, { ...scope, prepared: rawPacket });
131
+ }
132
+ if (retained && hybridDigest(readHandoff(writer, scope, packet.attempt, handoffFile)) !== hybridDigest(retained))
133
+ refuse("accepted_handoff_changed");
134
+ if (report) {
135
+ const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='handoff' AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, scope.runId, packet.attempt]);
136
+ if (rows.length > 1 || rows.some(row => Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024))
137
+ refuse("terminal_report_custody_invalid");
138
+ if (rows.length) {
139
+ const stored = JSON.parse(rows[0].payload_json);
140
+ if (stored.sha256 !== report.sha256 || hybridDigest(stored.bytes) !== hybridDigest(report.bytes)
141
+ || resolveStagePublicationPath(writer, scope.projectId, scope.runId, stored.root) !== reportRoot)
142
+ refuse("terminal_report_changed");
143
+ }
144
+ else {
145
+ if (replay)
146
+ refuse("accepted_terminal_report_missing");
147
+ acceptStageReport(writer, { ...scope, root: reportRoot, prepared: report });
148
+ }
149
+ }
150
+ };
151
+ let acceptedStage = false;
152
+ try {
153
+ const accepted = settlement.apply(context, fence);
154
+ acceptedStage = true;
155
+ if (stage !== "handoff")
156
+ return accepted;
157
+ const complete = PreparedDescriptorRunSettlement.prepare(context, input).apply(context, fence);
158
+ publishStagePacket(context, scope.projectId, scope.runId, stage, handoffFile);
159
+ publishStageReport(context, scope.projectId, scope.runId, stage, reportRoot);
160
+ return { ...accepted, ...complete, disposition: reviewFixHandoffDisposition(result), formalization_authorized: false,
161
+ handoff: handoffFile, reports: ["json", "md", "html"].map(extension => path.join(reportRoot, `stage-report.${extension}`)) };
162
+ }
163
+ catch (error) {
164
+ if (stage !== "handoff")
165
+ throw error;
166
+ if (!acceptedStage) {
167
+ try {
168
+ acceptedStage = readAcceptedStageData(context, scope).bytes.equals(bytes);
169
+ }
170
+ catch { /* Not committed. */ }
171
+ }
172
+ if (!acceptedStage)
173
+ throw error;
174
+ throw new AppError("review_fix_handoff_pending", "Accepted handoff needs projection or RUN settlement reconciliation", 1, { effect: "committed", run_id: input.runId, next_action: "reconcile_accepted_handoff", cause: error instanceof AppError ? error.code : "handoff_settlement_failed" });
175
+ }
176
+ }