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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,195 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { Ajv } from "ajv/dist/ajv.js";
5
+ import semver from "semver";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
8
+ import { readBoundedReleaseFile } from "./release-source.js";
9
+ import { isCanonicalOnlyMemoryPath } from "./memory-layer.js";
10
+ import { localEnvironmentAllowed } from "./workspace-files.js";
11
+ import { admitBundledFlow } from "./stage-handlers.js";
12
+ import { normalizeFlowContract } from "../domain/flow-contract.js";
13
+ const limit = 1024 * 1024, manifestPath = "project-pack/manifest.json";
14
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+ function refuse(reason) {
16
+ throw new AppError("project_flow_source_invalid", "Project-local flow requires its own valid curated pack; use the supported pack upgrade/repair route", 2, { reason, phase: "prepare", effect: "no_effect", next_action: "inspect_project_pack_and_upgrade_or_repair" });
17
+ }
18
+ function safe(name) {
19
+ return typeof name === "string" && /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(name)
20
+ && name.split("/").every(part => ![".", "..", ".git", "node_modules", ".dd-flow"].includes(part.toLowerCase()) && localEnvironmentAllowed(part));
21
+ }
22
+ function schema(name) { return JSON.parse(fs.readFileSync(new URL(`../schemas/${name}.schema.json`, import.meta.url), "utf8")); }
23
+ function decoded(bytes) { try {
24
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
25
+ }
26
+ catch {
27
+ return refuse("invalid_pack_json");
28
+ } }
29
+ function validate(scope, blobs) {
30
+ if (!scope || Object.keys(scope).sort().join() !== "flowId,projectRoot,workspace" || scope.flowId !== "interactive"
31
+ || [scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !path.isAbsolute(value)))
32
+ refuse("invalid_project_scope");
33
+ const manifest = decoded(blobs.get(manifestPath) ?? Buffer.alloc(0));
34
+ if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("project-flow-pack-manifest"))(manifest)
35
+ || manifest.schema_id !== "dd-flow/project-flow-pack-manifest@2" || !/^[a-f0-9]{40}$/.test(manifest.source_commit)
36
+ || !semver.valid(manifest.canon_version_at_source_commit))
37
+ refuse("invalid_or_legacy_project_manifest");
38
+ if (manifest.included_files.length > 20_000 || manifest.included_files.some(name => !safe(name) || name === manifestPath
39
+ || name.startsWith("project-pack/") || isCanonicalOnlyMemoryPath(`dd-flow/${name}`)))
40
+ refuse("invalid_curated_inventory");
41
+ const names = [manifestPath, ...manifest.included_files], folded = new Set();
42
+ let total = 0;
43
+ if (blobs.size !== names.length)
44
+ refuse("pack_inventory_changed");
45
+ for (const name of names) {
46
+ if (folded.has(name.toLowerCase()))
47
+ refuse("aliased_pack_path");
48
+ folded.add(name.toLowerCase());
49
+ const bytes = blobs.get(name);
50
+ if (!bytes?.length || bytes.length > limit || (total += bytes.length) > 32 * limit)
51
+ refuse("missing_or_unbounded_pack_file");
52
+ if (name !== manifestPath && manifest.file_hashes?.[name] !== undefined && manifest.file_hashes[name] !== hash(bytes))
53
+ refuse("project_pack_file_changed");
54
+ }
55
+ const descriptorPath = `${scope.flowId}/flow-descriptor.json`, descriptor = decoded(blobs.get(descriptorPath) ?? Buffer.alloc(0));
56
+ if (validateHybridShape("flow-descriptor", descriptor).length || descriptor.id !== scope.flowId || descriptor.origin !== "project")
57
+ refuse("project_descriptor_required");
58
+ for (const stage of descriptor.stages)
59
+ for (const kind of ["instruction", "result_schema"]) {
60
+ const ref = stage[kind], bytes = blobs.get(ref.path);
61
+ if (!bytes || ref.path === manifestPath || !manifest.included_files.includes(ref.path) || hash(bytes) !== ref.sha256)
62
+ refuse("stage_source_not_in_project_pack");
63
+ try {
64
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
65
+ }
66
+ catch {
67
+ refuse("invalid_stage_source_encoding");
68
+ }
69
+ if (kind === "result_schema")
70
+ decoded(bytes);
71
+ }
72
+ const compatibility = decoded(blobs.get("compatibility.json") ?? Buffer.alloc(0));
73
+ if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("compatibility"))(compatibility))
74
+ refuse("invalid_project_compatibility");
75
+ const contract = normalizeFlowContract(decoded(blobs.get("flow-contract.json") ?? Buffer.alloc(0)));
76
+ if (contract.id !== manifest.flow_contract || compatibility.dd_flow_cli.flow_contract !== contract.id
77
+ || compatibility.memory_bank_version !== manifest.canon_version_at_source_commit)
78
+ refuse("project_contract_mismatch");
79
+ return { manifest, descriptorPath, compatibility };
80
+ }
81
+ /** Project pack data, not engine/handler admission. Manifest canon paths are
82
+ * provenance hints only: never follow them or fall back to global instructions. */
83
+ export class PreparedProjectFlowSource {
84
+ sha256;
85
+ #scope;
86
+ #blobs;
87
+ #facts;
88
+ #fresh;
89
+ constructor(scope, blobs, fresh) {
90
+ this.#facts = validate(scope, blobs);
91
+ this.#scope = structuredClone(scope);
92
+ this.#blobs = new Map([...blobs].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, bytes]) => [name, Buffer.from(bytes)]));
93
+ this.sha256 = hybridDigest({ scope, inventory: [...this.#blobs].map(([name, bytes]) => ({ path: name, sha256: hash(bytes) })) });
94
+ this.#fresh = fresh;
95
+ Object.freeze(this);
96
+ }
97
+ static fromProject(scope) {
98
+ try {
99
+ for (const root of [scope.projectRoot, scope.workspace])
100
+ if (fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
101
+ refuse("noncanonical_project_root");
102
+ let root = scope.workspace;
103
+ for (const name of [".memory-bank", "dd-flow"]) {
104
+ root = path.join(root, name);
105
+ if (!fs.lstatSync(root).isDirectory())
106
+ refuse("linked_project_pack");
107
+ }
108
+ const read = (logical) => {
109
+ if (!safe(logical))
110
+ refuse("unsafe_pack_path");
111
+ let parent = root;
112
+ for (const part of logical.split("/").slice(0, -1)) {
113
+ parent = path.join(parent, part);
114
+ if (!fs.lstatSync(parent).isDirectory())
115
+ refuse("linked_project_pack");
116
+ }
117
+ return readBoundedReleaseFile(path.join(root, logical), limit);
118
+ };
119
+ const raw = read("manifest.json"), manifest = decoded(raw);
120
+ if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("project-flow-pack-manifest"))(manifest)
121
+ || manifest.schema_id !== "dd-flow/project-flow-pack-manifest@2")
122
+ refuse("invalid_or_legacy_project_manifest");
123
+ const blobs = new Map([[manifestPath, raw]]);
124
+ if (manifest.included_files.length > 20_000)
125
+ refuse("unbounded_project_pack");
126
+ let total = raw.length;
127
+ for (const name of manifest.included_files) {
128
+ if (!safe(name) || isCanonicalOnlyMemoryPath(`dd-flow/${name}`) || name.startsWith("project-pack/"))
129
+ refuse("invalid_curated_inventory");
130
+ const bytes = read(name);
131
+ total += bytes.length;
132
+ if (total > 32 * limit)
133
+ refuse("unbounded_project_pack");
134
+ blobs.set(name, bytes);
135
+ }
136
+ if (!read("manifest.json").equals(raw))
137
+ refuse("manifest_changed_during_capture");
138
+ return new PreparedProjectFlowSource(scope, blobs, true);
139
+ }
140
+ catch (error) {
141
+ if (error instanceof AppError && error.code === "project_flow_source_invalid")
142
+ throw error;
143
+ return refuse("project_pack_unavailable_or_changed");
144
+ }
145
+ }
146
+ snapshot() { return { schema_id: "dd-flow/project-flow-source@1", scope: structuredClone(this.#scope), blobs: [...this.#blobs].map(([name, bytes]) => ({ path: name, base64: bytes.toString("base64") })) }; }
147
+ /** Data restoration only. expectedSha256 must come from independent request/
148
+ * RUN custody. Engine/handler admission is separate and never inferred here. */
149
+ static restoreRetained(value, expectedSha256) {
150
+ if (!value || Object.keys(value).sort().join() !== "blobs,schema_id,scope" || value.schema_id !== "dd-flow/project-flow-source@1"
151
+ || !Array.isArray(value.blobs) || value.blobs.length > 20_001)
152
+ refuse("invalid_retained_project_source");
153
+ const blobs = new Map();
154
+ let total = 0;
155
+ for (const item of value.blobs) {
156
+ if (!item || Object.keys(item).sort().join() !== "base64,path" || !safe(item.path) || typeof item.base64 !== "string" || item.base64.length > 2 * limit || blobs.has(item.path))
157
+ refuse("invalid_retained_project_source");
158
+ const bytes = Buffer.from(item.base64, "base64");
159
+ total += bytes.length;
160
+ if (!bytes.length || bytes.length > limit || total > 32 * limit || bytes.toString("base64") !== item.base64)
161
+ refuse("invalid_retained_project_source");
162
+ blobs.set(item.path, bytes);
163
+ }
164
+ const result = new PreparedProjectFlowSource(value.scope, blobs, false);
165
+ if (result.sha256 !== expectedSha256)
166
+ refuse("retained_project_source_changed");
167
+ return result;
168
+ }
169
+ blobs() { return new Map([...this.#blobs].map(([name, bytes]) => [name, Buffer.from(bytes)])); }
170
+ scope() { return structuredClone(this.#scope); }
171
+ descriptorReference() { const name = this.#facts.descriptorPath; return { path: name, sha256: hash(this.#blobs.get(name)) }; }
172
+ projectPackProvenance() { return { sha256: hybridDigest([...this.#blobs].map(([name, bytes]) => ({ path: name === manifestPath ? "dd-flow/manifest.json" : `dd-flow/${name}`, sha256: hash(bytes) })).sort((a, b) => a.path.localeCompare(b.path, "en"))), source_commit: this.#facts.manifest.source_commit }; }
173
+ canonSource() { return { version: this.#facts.manifest.canon_version_at_source_commit, commit: this.#facts.manifest.source_commit }; }
174
+ /** Declared project base, not a claim that customized instructions are bytes
175
+ * from an installed canonical archive. Actual data identity is the pack hash. */
176
+ canonProvenance() { return { ...this.canonSource(), archive_sha256: null }; }
177
+ resolveRegisteredFlow() { const ref = this.descriptorReference(); return admitBundledFlow(this.#blobs.get(ref.path), ref.sha256, this.#blobs); }
178
+ assertEngineCompatibility(engine) {
179
+ const value = this.#facts.compatibility;
180
+ if (engine.package_name !== value.engine.package_name || engine.package_name !== value.dd_flow_cli.package_name
181
+ || !semver.valid(engine.package_version) || !semver.valid(engine.engine_version)
182
+ || !semver.validRange(value.engine.version_range) || !semver.valid(value.dd_flow_cli.min_version)
183
+ || !semver.satisfies(engine.engine_version, value.engine.version_range, { includePrerelease: true })
184
+ || !semver.gte(engine.package_version, value.dd_flow_cli.min_version))
185
+ refuse("project_pack_engine_incompatible");
186
+ }
187
+ revalidate() {
188
+ if (!this.#fresh) {
189
+ validate(this.#scope, this.#blobs);
190
+ return;
191
+ }
192
+ if (PreparedProjectFlowSource.fromProject(this.#scope).sha256 !== this.sha256)
193
+ refuse("project_source_changed_before_retention");
194
+ }
195
+ }
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { hybridDigest } from "./hybrid-contracts.js";
7
+ import { interactiveWorkPathAllowed } from "./interactive-work-batch.js";
8
+ import { ObservedMemorySources } from "./memory-source-observations.js";
9
+ import { memorySourceKey } from "./memory-stage-results.js";
10
+ import { assertMemoryGitRemovalSources } from "./memory-git-integration.js";
11
+ function refuse(reason) {
12
+ throw new AppError("project_git_integration_invalid", "Reviewed project files lack exact local Git integration", 2, { reason, effect: "no_effect" });
13
+ }
14
+ export function assertReviewedProjectGitInventory(references, removals) {
15
+ const valid = (ref) => ref?.root === "workspace" && interactiveWorkPathAllowed(ref.path)
16
+ && /^[a-f0-9]{64}$/.test(ref.sha256) && (Object.keys(ref).sort().join() === "path,root,sha256"
17
+ || Object.keys(ref).sort().join() === "git_mode,path,root,sha256" && ["100644", "100755"].includes(ref.git_mode));
18
+ if (!Array.isArray(references) || !Array.isArray(removals) || !references.length && !removals.length
19
+ || references.length + removals.length > 4096 || [...references, ...removals].some(ref => !valid(ref))
20
+ || new Set([...references, ...removals].map(ref => ref.path)).size !== references.length + removals.length)
21
+ refuse("invalid_reviewed_project_inventory");
22
+ }
23
+ /** Physical selected-file readback only, not review, policy or commit authority.
24
+ * Unrelated dirty files/index entries are deliberately neither committed nor
25
+ * treated as failures. The terminal owner must admit the original dirty input. */
26
+ export function observeReviewedProjectGitIntegration(input, environment, references, removals = []) {
27
+ assertReviewedProjectGitInventory(references, removals);
28
+ for (const root of [input.projectRoot, input.workspaceRoot]) {
29
+ if (!path.isAbsolute(root) || fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
30
+ refuse("canonical_repository_root_required");
31
+ }
32
+ if (!path.isAbsolute(input.gitExecutable) || typeof input.integrationBranch !== "string" || !input.integrationBranch)
33
+ refuse("explicit_git_selection_required");
34
+ const env = { ...Object.fromEntries(Object.entries(environment).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" };
35
+ const git = (root, args) => {
36
+ const result = spawnSync(input.gitExecutable, ["-c", "core.fsmonitor=false", "-C", root, ...args], { env, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
37
+ if (result.error || result.signal || result.status !== 0)
38
+ refuse("git_readback_failed");
39
+ return result.stdout;
40
+ };
41
+ const text = (root, args) => git(root, args).toString("utf8").trim();
42
+ const source = input.workspaceRoot, target = input.projectRoot, integrationRef = `refs/heads/${input.integrationBranch}`;
43
+ git(target, ["check-ref-format", integrationRef]);
44
+ const common = (root) => fs.realpathSync(path.resolve(root, text(root, ["rev-parse", "--git-common-dir"])));
45
+ const repository = common(source);
46
+ if (common(target) !== repository || [source, target].some(root => text(root, ["rev-parse", "--show-toplevel"]) !== root))
47
+ refuse("foreign_repository");
48
+ const workspaceBranch = text(source, ["symbolic-ref", "--short", "HEAD"]);
49
+ if (text(target, ["symbolic-ref", "--short", "HEAD"]) !== input.integrationBranch)
50
+ refuse("integration_branch_changed");
51
+ const head = text(source, ["rev-parse", "--verify", "HEAD^{commit}"]), integrated = text(target, ["rev-parse", "--verify", `${integrationRef}^{commit}`]);
52
+ git(target, ["merge-base", "--is-ancestor", head, integrated]);
53
+ const paths = [...references, ...removals].map(ref => ref.path);
54
+ const observations = [...new Set([source, target])].map(root => {
55
+ const visibility = git(root, ["ls-files", "-v", "-z", "--", ...paths]).toString("utf8");
56
+ if (visibility.split("\0").filter(Boolean).some(entry => !entry.startsWith("H ")))
57
+ refuse("selected_index_visibility_override");
58
+ git(root, ["diff", "--cached", "--exit-code", "--", ...paths]);
59
+ assertMemoryGitRemovalSources(root, removals);
60
+ const observed = ObservedMemorySources.capture({ workspace: root }, references.map(({ root, path }) => ({ root, path })));
61
+ const hashes = observed.hashes();
62
+ for (const ref of references) {
63
+ if (hashes.get(memorySourceKey("workspace", ref.path)) !== ref.sha256)
64
+ refuse("reviewed_workspace_bytes_changed");
65
+ if (process.platform !== "win32" && ((fs.lstatSync(path.join(root, ref.path)).mode & 0o111) !== 0) !== (ref.git_mode === "100755"))
66
+ refuse("reviewed_workspace_mode_changed");
67
+ }
68
+ return observed;
69
+ });
70
+ // ponytail: at most 4096 selected paths; batch Git reads if real interactive
71
+ // inventories make per-file subprocess latency material.
72
+ for (const [root, revision] of [[source, head], [target, integrated]]) {
73
+ for (const ref of references) {
74
+ const entry = git(root, ["ls-tree", "-z", revision, "--", ref.path]).toString("utf8");
75
+ if (!entry.startsWith(`${ref.git_mode ?? "100644"} blob `) || !entry.endsWith(`\t${ref.path}\0`))
76
+ refuse("reviewed_regular_git_blob_required");
77
+ if (createHash("sha256").update(git(root, ["cat-file", "blob", `${revision}:${ref.path}`])).digest("hex") !== ref.sha256)
78
+ refuse("reviewed_git_bytes_changed");
79
+ }
80
+ for (const ref of removals)
81
+ if (git(root, ["ls-tree", "-z", revision, "--", ref.path]).length)
82
+ refuse("removed_file_still_committed");
83
+ }
84
+ for (const observation of observations)
85
+ observation.revalidate();
86
+ if (text(source, ["rev-parse", "HEAD"]) !== head || text(target, ["rev-parse", integrationRef]) !== integrated)
87
+ refuse("integration_changed_during_observation");
88
+ return { schema_id: "dd-flow/project-git-integration-observation@1", project_root: target, workspace_root: source,
89
+ repository, workspace_branch: workspaceBranch, integration_ref: integrationRef, workspace_head: head, integration_head: integrated,
90
+ reviewed_files_sha256: hybridDigest([...references].sort((a, b) => a.path.localeCompare(b.path, "en"))),
91
+ removed_files_sha256: hybridDigest([...removals].sort((a, b) => a.path.localeCompare(b.path, "en"))) };
92
+ }
@@ -2,15 +2,10 @@ import crypto from "node:crypto";
2
2
  import { stageAttempt, stageCycle, bindStageWork } from "./stage-work-graph.js";
3
3
  import { assertRunMutationAllowed } from "./run-recovery.js";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { assertSourceRepairEligible } from "./code-checks.js";
5
+ import { assertCheckDiagnosisEligible } from "./code-checks.js";
6
6
  /** Checks finish before entering this synchronous registration transaction. */
7
7
  export function ensureAggregateRepair(context, input, create) {
8
- if (!input.failures.length)
9
- throw new AppError("repair_intent_invalid", "Repair requires candidate-source-backed failed receipts", 1);
10
- const workspace = context.db.get("SELECT workspace_root FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
11
- if (!workspace?.workspace_root && input.failures.some(receipt => receipt.candidate_library_inputs?.length))
12
- throw new AppError("repair_intent_invalid", "Library-backed repair requires the current RUN workspace", 1);
13
- assertSourceRepairEligible(input.failures, workspace?.workspace_root);
8
+ assertCheckDiagnosisEligible(input.failures);
14
9
  const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.fingerprint_scope === "check_inputs" ? receipt.before_fingerprint : receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
15
10
  return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
16
11
  }
@@ -0,0 +1,274 @@
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
+ const diagnostics = new WeakMap();
16
+ async function regularJson(file) {
17
+ try {
18
+ const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
19
+ try {
20
+ if (!(await handle.stat()).isFile())
21
+ return null;
22
+ return object(JSON.parse(await handle.readFile("utf8")));
23
+ }
24
+ finally {
25
+ await handle.close();
26
+ }
27
+ }
28
+ catch (error) {
29
+ if (["ENOENT", "ELOOP", "ENOTDIR"].includes(error.code ?? "") || error instanceof SyntaxError)
30
+ return null;
31
+ throw error;
32
+ }
33
+ }
34
+ async function realDirectory(directory) {
35
+ try {
36
+ const info = await lstat(directory);
37
+ return info.isDirectory() && !info.isSymbolicLink();
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return false;
42
+ throw error;
43
+ }
44
+ }
45
+ function payloadCommand(binding) {
46
+ const payload = object(binding.native_payload);
47
+ if (!payload)
48
+ return undefined;
49
+ if (binding.source_harness === "codex-desktop")
50
+ return lifecycleCommandFromPayload(payload) ?? undefined;
51
+ if (binding.source_harness === "grok-acp") {
52
+ const native = object(payload.tool_input ?? payload.toolInput);
53
+ const command = native?.command ?? native?.cmd ?? payload.command;
54
+ return typeof command === "string" ? command : undefined;
55
+ }
56
+ if (binding.source_harness === "zcode-acp") {
57
+ const update = object(object(payload.params)?.update), native = object(update?.rawInput);
58
+ const command = native?.command ?? native?.cmd;
59
+ return typeof command === "string" ? command : undefined;
60
+ }
61
+ const input = object(payload.input);
62
+ const command = input?.command ?? input?.cmd ?? input?.CommandLine;
63
+ return typeof command === "string" ? command : undefined;
64
+ }
65
+ /** The directory is only a locator. The owning daemon must attest the exact
66
+ * binding from its verified native-delivery map before it becomes a receipt. */
67
+ export async function reconcileRetainedNativeHook(context, input) {
68
+ const { scope, invocationId } = input;
69
+ const diagnostic = (phase, error, binding) => {
70
+ if (!input.onDiagnostic)
71
+ return;
72
+ const supplied = error instanceof Error || object(error) ? error.code : undefined;
73
+ const code = typeof supplied === "string" && /^[A-Za-z][A-Za-z0-9_]{0,95}$/.test(supplied) ? supplied : "native_hook_verification_unavailable";
74
+ // Diagnostics never become execution authority or activity. Keep identifiers
75
+ // and a typed cause, not native messages/configs that can contain credentials.
76
+ const value = { phase, code, invocation_id: invocationId,
77
+ ...(typeof binding?.request_id === "string" ? { request_id: binding.request_id } : {}),
78
+ ...(typeof binding?.operation_id === "string" ? { operation_id: binding.operation_id } : {}) };
79
+ const seen = diagnostics.get(context) ?? new Set(), key = JSON.stringify(value);
80
+ if (seen.has(key))
81
+ return;
82
+ seen.add(key);
83
+ diagnostics.set(context, seen);
84
+ input.onDiagnostic(value);
85
+ };
86
+ if (!scope.runId || context.env.DD_FLOW_DAEMON_ID !== scope.daemonId)
87
+ return false;
88
+ let owner = null;
89
+ try {
90
+ owner = object(JSON.parse(context.env.DD_FLOW_RUNTIME_OWNER ?? "null"));
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ 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;
96
+ if (typeof stateDir !== "string" || !path.isAbsolute(stateDir) || !await realDirectory(stateDir))
97
+ return false;
98
+ // This is an optional additional observation source. A missing, retiring or
99
+ // unbound daemon must leave the existing hook path intact, never grant it
100
+ // authority and never turn an otherwise valid legacy delivery into a failure.
101
+ let owned;
102
+ let metadata;
103
+ try {
104
+ const state = readNativeDaemonState(stateDir);
105
+ if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId) {
106
+ diagnostic("owner", { code: "daemon_owner_mismatch" });
107
+ return false;
108
+ }
109
+ const project = requireProjectByRoot(context, scope.projectRoot);
110
+ owned = requireManagedDaemonBinding(context, { stateDir, projectId: project.id, runId: scope.runId });
111
+ metadata = JSON.parse(owned.process.metadata_json);
112
+ }
113
+ catch (error) {
114
+ diagnostic("owner", error);
115
+ return false;
116
+ }
117
+ if (metadata.run_generation !== scope.generation) {
118
+ diagnostic("owner", { code: "run_generation_mismatch" });
119
+ return false;
120
+ }
121
+ const operations = path.join(stateDir, "operations");
122
+ if (!await realDirectory(operations))
123
+ return false;
124
+ const candidates = [];
125
+ for (const entry of await readdir(operations, { withFileTypes: true })) {
126
+ if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name))
127
+ continue;
128
+ const directory = path.join(operations, entry.name), hooks = path.join(directory, "hooks");
129
+ if (!await realDirectory(hooks))
130
+ continue;
131
+ const requested = await regularJson(path.join(directory, "requested.json"));
132
+ if (!requested || requested.schema_id !== "dd-flow/daemon-operation@1" || typeof requested.operation_id !== "string"
133
+ || sha256(requested.operation_id) !== entry.name || requested.daemon_id !== scope.daemonId
134
+ || requested.owner_id !== owned.process.owner_id || !["session.create", "session.start", "session.prompt"].includes(String(requested.operation))
135
+ || requested.session_id && requested.session_id !== scope.rootSessionId)
136
+ continue;
137
+ for (const hook of await readdir(hooks, { withFileTypes: true })) {
138
+ if (!hook.isFile() || !/^[A-Za-z0-9-]{1,128}\.json$/.test(hook.name))
139
+ continue;
140
+ const binding = await regularJson(path.join(hooks, hook.name));
141
+ if (!binding || binding.schema_id !== "dd-flow/hook-request@1" || binding.request_id !== hook.name.slice(0, -5)
142
+ || binding.daemon_id !== scope.daemonId || binding.operation_id !== requested.operation_id
143
+ || binding.root_provider_session_id !== scope.rootSessionId || !["codex-desktop", "antigravity-cli", "grok-acp", "zcode-acp", "opencode-server", "droid-cli"].includes(String(binding.source_harness))
144
+ || typeof binding.provider_session_id !== "string" || !binding.provider_session_id
145
+ || !(binding.parent_provider_session_id === null || typeof binding.parent_provider_session_id === "string")
146
+ || typeof binding.tool_call_id !== "string" || !binding.tool_call_id || binding.event !== "PreToolUse"
147
+ || typeof binding.native_command !== "string" || binding.command_sha256 !== sha256(binding.native_command)
148
+ || payloadCommand(binding) !== binding.native_command || !matchesIssuedNativeCommand(context, invocationId, binding.native_command))
149
+ continue;
150
+ candidates.push(binding);
151
+ }
152
+ }
153
+ const verified = [];
154
+ for (const binding of candidates) {
155
+ try {
156
+ const module = binding.source_harness === "codex-desktop"
157
+ ? await import("../harness-runtime/lib/dd-codex-daemon.mjs")
158
+ : binding.source_harness === "grok-acp" ? await import("../harness-runtime/lib/dd-grok-daemon.mjs")
159
+ : binding.source_harness === "zcode-acp" ? await import("../harness-runtime/lib/dd-zcode-daemon.mjs")
160
+ // @ts-expect-error Executable adapter assets have no duplicate typed API.
161
+ : binding.source_harness === "opencode-server" ? await import("../harness-runtime/lib/dd-opencode-daemon.mjs")
162
+ // @ts-expect-error Executable adapter assets have no duplicate typed API.
163
+ : binding.source_harness === "droid-cli" ? await import("../harness-runtime/lib/dd-droid-daemon.mjs")
164
+ : await import("../harness-runtime/lib/dd-agy-daemon.mjs");
165
+ const proof = await withDaemonProgressObserver(progress => {
166
+ if (typeof progress.native_request_id === 'string' && typeof progress.observed_at === 'string')
167
+ input.onProgress?.({ cursor: `${binding.request_id}:${progress.native_request_id}`, observed_at: Date.parse(progress.observed_at) });
168
+ }, () => module.callDaemon(stateDir, "hook.verify", { daemonId: scope.daemonId, requestId: binding.request_id, binding }, 5_000));
169
+ if (binding.native_admission === "assigned_work_start") {
170
+ const resolved = object(proof.nativeHookBinding);
171
+ if (proof.verified !== true || JSON.stringify(proof.registeredHookBinding) !== JSON.stringify(binding) || !resolved
172
+ || !["antigravity-cli", "opencode-server", "droid-cli"].includes(String(binding.source_harness))
173
+ || resolved.native_ancestry !== "confirmed" || typeof resolved.native_cwd !== "string" || !path.isAbsolute(resolved.native_cwd)
174
+ || !(resolved.parent_provider_session_id === null || typeof resolved.parent_provider_session_id === "string")
175
+ || ["request_id", "operation_id", "daemon_id", "turn_generation", "source_harness", "root_provider_session_id", "provider_session_id",
176
+ "tool_call_id", "event", "native_command", "command_sha256", "native_creator", "native_admission"].some(key => resolved[key] !== binding[key])
177
+ || payloadCommand(resolved) !== binding.native_command) {
178
+ diagnostic("verify", { code: "native_hook_unproven" }, binding);
179
+ continue;
180
+ }
181
+ verified.push(resolved);
182
+ }
183
+ else if (proof.verified === true && JSON.stringify(proof.nativeHookBinding) === JSON.stringify(binding))
184
+ verified.push(binding);
185
+ else
186
+ diagnostic("verify", { code: "native_hook_unproven" }, binding);
187
+ }
188
+ catch (error) {
189
+ diagnostic("verify", error, binding);
190
+ }
191
+ }
192
+ // Exact old tool delivery belongs to its immutable prior attempt. A public
193
+ // command may be identical in a fresh no-effect successor; do not repeatedly
194
+ // select its predecessor and starve the new native callback.
195
+ const eligible = verified.filter(binding => observedLifecycleInvocation(context, binding.native_command, scope, { identity: {
196
+ daemonId: scope.daemonId, rootSessionId: scope.rootSessionId, sessionId: binding.provider_session_id,
197
+ parentSessionId: binding.parent_provider_session_id, toolCallId: binding.tool_call_id
198
+ } })?.id === invocationId);
199
+ if (!eligible.length)
200
+ return false;
201
+ const identity = (binding) => JSON.stringify([binding.source_harness, binding.daemon_id, binding.root_provider_session_id,
202
+ binding.provider_session_id, binding.parent_provider_session_id, binding.native_cwd]);
203
+ if (new Set(eligible.map(identity)).size !== 1)
204
+ throw new AppError("invocation_ambiguous", "More than one native executor supplied this issued Work command", 1, { effect: "no_effect", invocation_id: invocationId });
205
+ const binding = eligible[0], payload = object(binding.native_payload);
206
+ if (binding.source_harness === "codex-desktop") {
207
+ const proof = object(binding.codex_proof);
208
+ if (!proof || proof.daemonId !== scope.daemonId || proof.rootSessionId !== scope.rootSessionId
209
+ || proof.sessionId !== binding.provider_session_id || proof.parentSessionId !== binding.parent_provider_session_id
210
+ || proof.toolCallId !== binding.tool_call_id || typeof proof.projectRoot !== "string"
211
+ || canonicalPath(proof.projectRoot) !== canonicalPath(scope.projectRoot) || typeof proof.ddFlowHome !== "string"
212
+ || canonicalPath(proof.ddFlowHome) !== canonicalPath(context.ddFlowHome))
213
+ return false;
214
+ const prior = context.codexHookProof;
215
+ try {
216
+ context.codexHookProof = { daemonId: scope.daemonId, rootSessionId: scope.rootSessionId,
217
+ sessionId: binding.provider_session_id, parentSessionId: binding.parent_provider_session_id,
218
+ toolCallId: binding.tool_call_id, projectRoot: proof.projectRoot, command: binding.native_command };
219
+ handleCodexHook(context, { event: "PreToolUse", projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
220
+ }
221
+ finally {
222
+ if (prior)
223
+ context.codexHookProof = prior;
224
+ else
225
+ delete context.codexHookProof;
226
+ }
227
+ }
228
+ else if (binding.source_harness === "grok-acp") {
229
+ const identity = object(object(payload._meta)?.ddGrok);
230
+ if (!identity || identity.daemonId !== scope.daemonId || identity.rootProviderSessionId !== scope.rootSessionId
231
+ || identity.parentProviderSessionId !== binding.parent_provider_session_id
232
+ || (payload.session_id ?? payload.sessionId) !== binding.provider_session_id
233
+ || (payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id) !== binding.tool_call_id)
234
+ return false;
235
+ handleGrokEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
236
+ }
237
+ else if (binding.source_harness === "zcode-acp") {
238
+ const controlled = object(object(payload._meta)?.ddZcode), update = object(object(payload.params)?.update), native = object(object(update?._meta)?.zcodeRuntime);
239
+ if (!controlled || controlled.daemonId !== scope.daemonId || controlled.rootProviderSessionId !== scope.rootSessionId
240
+ || controlled.parentProviderSessionId !== binding.parent_provider_session_id || native?.source !== "subagent"
241
+ || native.childSessionId !== binding.provider_session_id || native.childToolCallId !== binding.tool_call_id
242
+ || typeof update?.toolCallId !== "string" || !update.toolCallId || typeof native.nativeTurnId !== "string" || !native.nativeTurnId)
243
+ return false;
244
+ handleZcodeEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
245
+ }
246
+ else if (binding.native_admission === "assigned_work_start") {
247
+ const harness = binding.source_harness, session = object(payload.session);
248
+ if (payload.daemon_id !== scope.daemonId || (harness === "antigravity-cli" ? payload.conversation_id !== binding.provider_session_id
249
+ || (payload.parent_conversation_id ?? null) !== binding.parent_provider_session_id || payload.event_id !== binding.tool_call_id
250
+ : !session || session.provider_session_id !== binding.provider_session_id || (session.parent_provider_session_id ?? null) !== binding.parent_provider_session_id
251
+ || payload.tool_call_id !== binding.tool_call_id))
252
+ return false;
253
+ const prior = context.nativeHookProof;
254
+ try {
255
+ context.nativeHookProof = { harness, daemonId: scope.daemonId, rootSessionId: scope.rootSessionId, sessionId: binding.provider_session_id,
256
+ toolCallId: binding.tool_call_id };
257
+ const handle = harness === "antigravity-cli" ? handleAgyEvent : harness === "droid-cli" ? handleDroidEvent : handleOpenCodeEvent;
258
+ handle(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
259
+ }
260
+ finally {
261
+ if (prior)
262
+ context.nativeHookProof = prior;
263
+ else
264
+ delete context.nativeHookProof;
265
+ }
266
+ }
267
+ else {
268
+ if (payload.daemon_id !== scope.daemonId || payload.conversation_id !== binding.provider_session_id
269
+ || (payload.parent_conversation_id ?? null) !== binding.parent_provider_session_id || payload.event_id !== binding.tool_call_id)
270
+ return false;
271
+ handleAgyEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
272
+ }
273
+ return true;
274
+ }