@lannguyensi/harness 0.64.0 → 1.0.0

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 (710) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +46 -42
  3. package/dist/cli/add/index.js +17 -21
  4. package/dist/cli/add/index.js.map +1 -1
  5. package/dist/cli/adopt/derive.d.ts +2 -2
  6. package/dist/cli/adopt/derive.js +2 -19
  7. package/dist/cli/adopt/derive.js.map +1 -1
  8. package/dist/cli/adopt/index.d.ts +1 -1
  9. package/dist/cli/adopt/index.js +1 -12
  10. package/dist/cli/adopt/index.js.map +1 -1
  11. package/dist/cli/apply/apply.d.ts +20 -35
  12. package/dist/cli/apply/apply.js +61 -136
  13. package/dist/cli/apply/apply.js.map +1 -1
  14. package/dist/cli/apply/generate-codex-config.js +10 -58
  15. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  16. package/dist/cli/apply/generate-memory-index.js +1 -2
  17. package/dist/cli/apply/generate-memory-index.js.map +1 -1
  18. package/dist/cli/apply/generate-opencode-config.d.ts +2 -10
  19. package/dist/cli/apply/generate-opencode-config.js +74 -113
  20. package/dist/cli/apply/generate-opencode-config.js.map +1 -1
  21. package/dist/cli/apply/generate-settings.d.ts +11 -64
  22. package/dist/cli/apply/generate-settings.js +27 -132
  23. package/dist/cli/apply/generate-settings.js.map +1 -1
  24. package/dist/cli/apply/index.d.ts +1 -1
  25. package/dist/cli/apply/index.js +1 -1
  26. package/dist/cli/apply/index.js.map +1 -1
  27. package/dist/cli/apply/install-codex-config.d.ts +153 -1
  28. package/dist/cli/apply/install-codex-config.js +445 -70
  29. package/dist/cli/apply/install-codex-config.js.map +1 -1
  30. package/dist/cli/apply/next-steps.d.ts +7 -0
  31. package/dist/cli/apply/next-steps.js +13 -1
  32. package/dist/cli/apply/next-steps.js.map +1 -1
  33. package/dist/cli/bounded-stdin.d.ts +0 -23
  34. package/dist/cli/bounded-stdin.js +2 -49
  35. package/dist/cli/bounded-stdin.js.map +1 -1
  36. package/dist/cli/describe.d.ts +1 -1
  37. package/dist/cli/describe.js +0 -3
  38. package/dist/cli/describe.js.map +1 -1
  39. package/dist/cli/diff/engine.d.ts +0 -11
  40. package/dist/cli/diff/engine.js +1 -17
  41. package/dist/cli/diff/engine.js.map +1 -1
  42. package/dist/cli/diff/index.js +14 -25
  43. package/dist/cli/diff/index.js.map +1 -1
  44. package/dist/cli/doctor/claude-mcp.d.ts +4 -19
  45. package/dist/cli/doctor/claude-mcp.js +11 -22
  46. package/dist/cli/doctor/claude-mcp.js.map +1 -1
  47. package/dist/cli/doctor/codex.d.ts +12 -14
  48. package/dist/cli/doctor/codex.js +31 -144
  49. package/dist/cli/doctor/codex.js.map +1 -1
  50. package/dist/cli/doctor/format.js +23 -345
  51. package/dist/cli/doctor/format.js.map +1 -1
  52. package/dist/cli/doctor/hook-liveness.d.ts +56 -0
  53. package/dist/cli/doctor/hook-liveness.js +455 -0
  54. package/dist/cli/doctor/hook-liveness.js.map +1 -0
  55. package/dist/cli/doctor/index.d.ts +13 -87
  56. package/dist/cli/doctor/index.js +57 -713
  57. package/dist/cli/doctor/index.js.map +1 -1
  58. package/dist/cli/doctor/opencode.d.ts +2 -1
  59. package/dist/cli/doctor/opencode.js.map +1 -1
  60. package/dist/cli/doctor/types.d.ts +21 -387
  61. package/dist/cli/doctor/types.js.map +1 -1
  62. package/dist/cli/drain-stdio.d.ts +31 -0
  63. package/dist/cli/drain-stdio.js +76 -0
  64. package/dist/cli/drain-stdio.js.map +1 -0
  65. package/dist/cli/export.d.ts +3 -1
  66. package/dist/cli/export.js +7 -16
  67. package/dist/cli/export.js.map +1 -1
  68. package/dist/cli/gate/codex-disable.d.ts +29 -0
  69. package/dist/cli/gate/codex-disable.js +195 -0
  70. package/dist/cli/gate/codex-disable.js.map +1 -0
  71. package/dist/cli/gate/codex-enable.d.ts +41 -0
  72. package/dist/cli/gate/codex-enable.js +127 -0
  73. package/dist/cli/gate/codex-enable.js.map +1 -0
  74. package/dist/cli/gate/codex-snapshot.d.ts +208 -0
  75. package/dist/cli/gate/codex-snapshot.js +402 -0
  76. package/dist/cli/gate/codex-snapshot.js.map +1 -0
  77. package/dist/cli/gate/disable.d.ts +9 -0
  78. package/dist/cli/gate/disable.js +263 -28
  79. package/dist/cli/gate/disable.js.map +1 -1
  80. package/dist/cli/gate/enable.d.ts +26 -6
  81. package/dist/cli/gate/enable.js +152 -80
  82. package/dist/cli/gate/enable.js.map +1 -1
  83. package/dist/cli/gate/link-chain.d.ts +53 -0
  84. package/dist/cli/gate/link-chain.js +122 -0
  85. package/dist/cli/gate/link-chain.js.map +1 -0
  86. package/dist/cli/gate/snapshot.d.ts +289 -12
  87. package/dist/cli/gate/snapshot.js +481 -22
  88. package/dist/cli/gate/snapshot.js.map +1 -1
  89. package/dist/cli/index.d.ts +0 -9
  90. package/dist/cli/index.js +9 -2131
  91. package/dist/cli/index.js.map +1 -1
  92. package/dist/cli/init/composer.d.ts +3 -6
  93. package/dist/cli/init/composer.js +6 -370
  94. package/dist/cli/init/composer.js.map +1 -1
  95. package/dist/cli/init/dependencies.d.ts +1 -2
  96. package/dist/cli/init/dependencies.js +4 -80
  97. package/dist/cli/init/dependencies.js.map +1 -1
  98. package/dist/cli/init/detect.js +5 -4
  99. package/dist/cli/init/detect.js.map +1 -1
  100. package/dist/cli/init/interactive.d.ts +1 -23
  101. package/dist/cli/init/interactive.js +33 -247
  102. package/dist/cli/init/interactive.js.map +1 -1
  103. package/dist/cli/init/profiles.d.ts +2 -2
  104. package/dist/cli/init/profiles.js +51 -245
  105. package/dist/cli/init/profiles.js.map +1 -1
  106. package/dist/cli/init/templates.d.ts +2 -37
  107. package/dist/cli/init/templates.js +25 -1237
  108. package/dist/cli/init/templates.js.map +1 -1
  109. package/dist/cli/list.d.ts +1 -1
  110. package/dist/cli/list.js +1 -38
  111. package/dist/cli/list.js.map +1 -1
  112. package/dist/cli/loader.d.ts +15 -1
  113. package/dist/cli/loader.js +56 -39
  114. package/dist/cli/loader.js.map +1 -1
  115. package/dist/cli/main.js +7 -2
  116. package/dist/cli/main.js.map +1 -1
  117. package/dist/cli/migrate-home/index.js +2 -2
  118. package/dist/cli/migrate-home/index.js.map +1 -1
  119. package/dist/cli/pack/add.js +1 -1
  120. package/dist/cli/pack/add.js.map +1 -1
  121. package/dist/cli/pack/hook-bootstrap.d.ts +5 -164
  122. package/dist/cli/pack/hook-bootstrap.js +15 -291
  123. package/dist/cli/pack/hook-bootstrap.js.map +1 -1
  124. package/dist/cli/pack/hook-branch-protection.d.ts +28 -21
  125. package/dist/cli/pack/hook-branch-protection.js +365 -340
  126. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  127. package/dist/cli/pack/index.d.ts +0 -1
  128. package/dist/cli/pack/index.js +0 -1
  129. package/dist/cli/pack/index.js.map +1 -1
  130. package/dist/cli/pack/list.js +1 -6
  131. package/dist/cli/pack/list.js.map +1 -1
  132. package/dist/cli/pack/mutate.d.ts +7 -9
  133. package/dist/cli/pack/mutate.js +6 -9
  134. package/dist/cli/pack/mutate.js.map +1 -1
  135. package/dist/cli/pack/reseed.d.ts +2 -2
  136. package/dist/cli/pack/reseed.js +16 -22
  137. package/dist/cli/pack/reseed.js.map +1 -1
  138. package/dist/cli/{delegate/register.d.ts → register-gate-group.d.ts} +1 -1
  139. package/dist/cli/register-gate-group.js +348 -0
  140. package/dist/cli/register-gate-group.js.map +1 -0
  141. package/dist/cli/register-inspect-group.d.ts +6 -0
  142. package/dist/cli/register-inspect-group.js +189 -0
  143. package/dist/cli/register-inspect-group.js.map +1 -0
  144. package/dist/cli/register-operator-lifecycle.d.ts +1 -1
  145. package/dist/cli/register-operator-lifecycle.js +3 -100
  146. package/dist/cli/register-operator-lifecycle.js.map +1 -1
  147. package/dist/cli/register-pack-group.d.ts +5 -0
  148. package/dist/cli/register-pack-group.js +134 -0
  149. package/dist/cli/register-pack-group.js.map +1 -0
  150. package/dist/cli/{register-audit-group.d.ts → register-setup-group.d.ts} +1 -1
  151. package/dist/cli/register-setup-group.js +495 -0
  152. package/dist/cli/register-setup-group.js.map +1 -0
  153. package/dist/cli/remove/index.d.ts +0 -14
  154. package/dist/cli/remove/index.js +2 -115
  155. package/dist/cli/remove/index.js.map +1 -1
  156. package/dist/cli/remove/mutate.d.ts +0 -2
  157. package/dist/cli/remove/mutate.js +1 -19
  158. package/dist/cli/remove/mutate.js.map +1 -1
  159. package/dist/cli/remove/register.js +2 -19
  160. package/dist/cli/remove/register.js.map +1 -1
  161. package/dist/cli/shell-quote.d.ts +7 -0
  162. package/dist/cli/shell-quote.js +17 -0
  163. package/dist/cli/shell-quote.js.map +1 -0
  164. package/dist/cli/uninstall/index.d.ts +4 -2
  165. package/dist/cli/uninstall/index.js +15 -7
  166. package/dist/cli/uninstall/index.js.map +1 -1
  167. package/dist/cli/uninstall/register.js +6 -4
  168. package/dist/cli/uninstall/register.js.map +1 -1
  169. package/dist/cli/uninstall/snapshot.d.ts +15 -0
  170. package/dist/cli/uninstall/snapshot.js +38 -0
  171. package/dist/cli/uninstall/snapshot.js.map +1 -1
  172. package/dist/cli/validate/checks.d.ts +0 -77
  173. package/dist/cli/validate/checks.js +3 -1049
  174. package/dist/cli/validate/checks.js.map +1 -1
  175. package/dist/cli/validate/index.js +19 -15
  176. package/dist/cli/validate/index.js.map +1 -1
  177. package/dist/index.d.ts +0 -3
  178. package/dist/index.js +0 -3
  179. package/dist/index.js.map +1 -1
  180. package/dist/io/claude-mcp.d.ts +1 -1
  181. package/dist/io/claude-mcp.js +27 -14
  182. package/dist/io/claude-mcp.js.map +1 -1
  183. package/dist/io/harness-lock.js +2 -2
  184. package/dist/io/harness-lock.js.map +1 -1
  185. package/dist/io/lock.d.ts +0 -26
  186. package/dist/io/lock.js +10 -27
  187. package/dist/io/lock.js.map +1 -1
  188. package/dist/io/merge-settings.js +6 -4
  189. package/dist/io/merge-settings.js.map +1 -1
  190. package/dist/io/project-name.d.ts +22 -39
  191. package/dist/io/project-name.js +25 -42
  192. package/dist/io/project-name.js.map +1 -1
  193. package/dist/io/read-regular-file.d.ts +103 -40
  194. package/dist/io/read-regular-file.js +196 -35
  195. package/dist/io/read-regular-file.js.map +1 -1
  196. package/dist/io/restart-hints.js +3 -6
  197. package/dist/io/restart-hints.js.map +1 -1
  198. package/dist/io/version-compare.d.ts +1 -3
  199. package/dist/io/version-compare.js +1 -3
  200. package/dist/io/version-compare.js.map +1 -1
  201. package/dist/io/write-regular-file.d.ts +30 -0
  202. package/dist/io/write-regular-file.js +85 -0
  203. package/dist/io/write-regular-file.js.map +1 -0
  204. package/dist/overrides/index.d.ts +1 -0
  205. package/dist/overrides/index.js +1 -0
  206. package/dist/overrides/index.js.map +1 -1
  207. package/dist/overrides/merge-layers.d.ts +43 -0
  208. package/dist/overrides/merge-layers.js +76 -0
  209. package/dist/overrides/merge-layers.js.map +1 -0
  210. package/dist/overrides/merge.d.ts +16 -0
  211. package/dist/overrides/merge.js +21 -8
  212. package/dist/overrides/merge.js.map +1 -1
  213. package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -69
  214. package/dist/policy-packs/builtin/branch-protection-runtime.js +12 -84
  215. package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
  216. package/dist/policy-packs/builtin/branch-protection.d.ts +7 -30
  217. package/dist/policy-packs/builtin/branch-protection.js +56 -89
  218. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  219. package/dist/policy-packs/config-check.d.ts +7 -5
  220. package/dist/policy-packs/config-check.js +2 -2
  221. package/dist/policy-packs/config-check.js.map +1 -1
  222. package/dist/policy-packs/expand.d.ts +1 -3
  223. package/dist/policy-packs/expand.js +5 -42
  224. package/dist/policy-packs/expand.js.map +1 -1
  225. package/dist/policy-packs/index.d.ts +3 -3
  226. package/dist/policy-packs/index.js +2 -2
  227. package/dist/policy-packs/index.js.map +1 -1
  228. package/dist/policy-packs/registry.d.ts +9 -38
  229. package/dist/policy-packs/registry.js +5 -65
  230. package/dist/policy-packs/registry.js.map +1 -1
  231. package/dist/policy-packs/runtime.d.ts +0 -4
  232. package/dist/policy-packs/runtime.js +9 -22
  233. package/dist/policy-packs/runtime.js.map +1 -1
  234. package/dist/policy-packs/types.d.ts +0 -19
  235. package/dist/policy-packs/ux-compare.d.ts +1 -11
  236. package/dist/policy-packs/ux-compare.js +6 -42
  237. package/dist/policy-packs/ux-compare.js.map +1 -1
  238. package/dist/policy-packs/ux-drift-check.d.ts +5 -5
  239. package/dist/policy-packs/ux-drift-check.js +17 -22
  240. package/dist/policy-packs/ux-drift-check.js.map +1 -1
  241. package/dist/policy-packs/version-check.d.ts +5 -26
  242. package/dist/policy-packs/version-check.js +19 -74
  243. package/dist/policy-packs/version-check.js.map +1 -1
  244. package/dist/probes/memory.d.ts +1 -1
  245. package/dist/probes/memory.js +1 -1
  246. package/dist/probes/memory.js.map +1 -1
  247. package/dist/runtime/agent-facing.d.ts +9 -0
  248. package/dist/runtime/agent-facing.js +28 -14
  249. package/dist/runtime/agent-facing.js.map +1 -1
  250. package/dist/runtime/bounded-text.d.ts +15 -0
  251. package/dist/runtime/bounded-text.js +27 -0
  252. package/dist/runtime/bounded-text.js.map +1 -0
  253. package/dist/runtime/git-branch.d.ts +125 -0
  254. package/dist/runtime/git-branch.js +381 -0
  255. package/dist/runtime/git-branch.js.map +1 -0
  256. package/dist/runtime/hermetic-spawn-guard.d.ts +2 -2
  257. package/dist/runtime/hermetic-spawn-guard.js +24 -34
  258. package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
  259. package/dist/runtime/home-dir.js +3 -2
  260. package/dist/runtime/home-dir.js.map +1 -1
  261. package/dist/runtime/tool-name-aliases.d.ts +0 -6
  262. package/dist/runtime/tool-name-aliases.js +5 -18
  263. package/dist/runtime/tool-name-aliases.js.map +1 -1
  264. package/dist/schema/grounding.d.ts +7 -54
  265. package/dist/schema/grounding.js +10 -12
  266. package/dist/schema/grounding.js.map +1 -1
  267. package/dist/schema/hooks.d.ts +39 -136
  268. package/dist/schema/hooks.js +3 -6
  269. package/dist/schema/hooks.js.map +1 -1
  270. package/dist/schema/index.d.ts +114 -3828
  271. package/dist/schema/index.js +41 -80
  272. package/dist/schema/index.js.map +1 -1
  273. package/dist/schema/memory.d.ts +54 -131
  274. package/dist/schema/memory.js +2 -2
  275. package/dist/schema/memory.js.map +1 -1
  276. package/dist/schema/policy-packs.d.ts +3 -45
  277. package/dist/schema/policy-packs.js +7 -13
  278. package/dist/schema/policy-packs.js.map +1 -1
  279. package/dist/schema/policy-ux.d.ts +7 -0
  280. package/dist/schema/policy-ux.js +18 -0
  281. package/dist/schema/policy-ux.js.map +1 -0
  282. package/dist/schema/removed-keys.d.ts +185 -0
  283. package/dist/schema/removed-keys.js +531 -0
  284. package/dist/schema/removed-keys.js.map +1 -0
  285. package/dist/schema/tools.d.ts +30 -268
  286. package/dist/schema/tools.js +4 -4
  287. package/dist/schema/tools.js.map +1 -1
  288. package/package.json +5 -16
  289. package/dist/cli/approve/branch-protection.d.ts +0 -69
  290. package/dist/cli/approve/branch-protection.js +0 -144
  291. package/dist/cli/approve/branch-protection.js.map +0 -1
  292. package/dist/cli/approve/risk.d.ts +0 -112
  293. package/dist/cli/approve/risk.js +0 -197
  294. package/dist/cli/approve/risk.js.map +0 -1
  295. package/dist/cli/approve/stdin-report.d.ts +0 -57
  296. package/dist/cli/approve/stdin-report.js +0 -145
  297. package/dist/cli/approve/stdin-report.js.map +0 -1
  298. package/dist/cli/approve/understanding.d.ts +0 -410
  299. package/dist/cli/approve/understanding.js +0 -996
  300. package/dist/cli/approve/understanding.js.map +0 -1
  301. package/dist/cli/audit.d.ts +0 -80
  302. package/dist/cli/audit.js +0 -252
  303. package/dist/cli/audit.js.map +0 -1
  304. package/dist/cli/delegate/index.d.ts +0 -76
  305. package/dist/cli/delegate/index.js +0 -444
  306. package/dist/cli/delegate/index.js.map +0 -1
  307. package/dist/cli/delegate/register.js +0 -90
  308. package/dist/cli/delegate/register.js.map +0 -1
  309. package/dist/cli/doctor/auto-approve-mode.d.ts +0 -37
  310. package/dist/cli/doctor/auto-approve-mode.js +0 -80
  311. package/dist/cli/doctor/auto-approve-mode.js.map +0 -1
  312. package/dist/cli/doctor/bypass-without-auto-approve.d.ts +0 -38
  313. package/dist/cli/doctor/bypass-without-auto-approve.js +0 -95
  314. package/dist/cli/doctor/bypass-without-auto-approve.js.map +0 -1
  315. package/dist/cli/doctor/codex-config-drift.d.ts +0 -28
  316. package/dist/cli/doctor/codex-config-drift.js +0 -240
  317. package/dist/cli/doctor/codex-config-drift.js.map +0 -1
  318. package/dist/cli/doctor/expire-on-tool-match.d.ts +0 -15
  319. package/dist/cli/doctor/expire-on-tool-match.js +0 -58
  320. package/dist/cli/doctor/expire-on-tool-match.js.map +0 -1
  321. package/dist/cli/doctor/rogue-ledger.d.ts +0 -53
  322. package/dist/cli/doctor/rogue-ledger.js +0 -153
  323. package/dist/cli/doctor/rogue-ledger.js.map +0 -1
  324. package/dist/cli/doctor/session-start-preflight-setup-version.d.ts +0 -83
  325. package/dist/cli/doctor/session-start-preflight-setup-version.js +0 -114
  326. package/dist/cli/doctor/session-start-preflight-setup-version.js.map +0 -1
  327. package/dist/cli/doctor/settings-drift.d.ts +0 -25
  328. package/dist/cli/doctor/settings-drift.js +0 -213
  329. package/dist/cli/doctor/settings-drift.js.map +0 -1
  330. package/dist/cli/doctor/toolchain-parity.d.ts +0 -63
  331. package/dist/cli/doctor/toolchain-parity.js +0 -154
  332. package/dist/cli/doctor/toolchain-parity.js.map +0 -1
  333. package/dist/cli/doctor/ug-auto-approvals.d.ts +0 -40
  334. package/dist/cli/doctor/ug-auto-approvals.js +0 -91
  335. package/dist/cli/doctor/ug-auto-approvals.js.map +0 -1
  336. package/dist/cli/doctor/ug-delegations.d.ts +0 -26
  337. package/dist/cli/doctor/ug-delegations.js +0 -121
  338. package/dist/cli/doctor/ug-delegations.js.map +0 -1
  339. package/dist/cli/doctor/ug-inflight.d.ts +0 -35
  340. package/dist/cli/doctor/ug-inflight.js +0 -57
  341. package/dist/cli/doctor/ug-inflight.js.map +0 -1
  342. package/dist/cli/doctor/understanding-mode-env.d.ts +0 -51
  343. package/dist/cli/doctor/understanding-mode-env.js +0 -98
  344. package/dist/cli/doctor/understanding-mode-env.js.map +0 -1
  345. package/dist/cli/dry-run.d.ts +0 -58
  346. package/dist/cli/dry-run.js +0 -283
  347. package/dist/cli/dry-run.js.map +0 -1
  348. package/dist/cli/enriched-event.d.ts +0 -35
  349. package/dist/cli/enriched-event.js +0 -60
  350. package/dist/cli/enriched-event.js.map +0 -1
  351. package/dist/cli/event-input.d.ts +0 -28
  352. package/dist/cli/event-input.js +0 -73
  353. package/dist/cli/event-input.js.map +0 -1
  354. package/dist/cli/explain-action.d.ts +0 -25
  355. package/dist/cli/explain-action.js +0 -57
  356. package/dist/cli/explain-action.js.map +0 -1
  357. package/dist/cli/explain-policy.d.ts +0 -147
  358. package/dist/cli/explain-policy.js +0 -248
  359. package/dist/cli/explain-policy.js.map +0 -1
  360. package/dist/cli/explain.d.ts +0 -31
  361. package/dist/cli/explain.js +0 -159
  362. package/dist/cli/explain.js.map +0 -1
  363. package/dist/cli/gc/index.d.ts +0 -63
  364. package/dist/cli/gc/index.js +0 -566
  365. package/dist/cli/gc/index.js.map +0 -1
  366. package/dist/cli/pack/approve-escape.d.ts +0 -19
  367. package/dist/cli/pack/approve-escape.js +0 -190
  368. package/dist/cli/pack/approve-escape.js.map +0 -1
  369. package/dist/cli/pack/auto-approve-path.d.ts +0 -146
  370. package/dist/cli/pack/auto-approve-path.js +0 -462
  371. package/dist/cli/pack/auto-approve-path.js.map +0 -1
  372. package/dist/cli/pack/hook-codex-post-tool-use.d.ts +0 -25
  373. package/dist/cli/pack/hook-codex-post-tool-use.js +0 -160
  374. package/dist/cli/pack/hook-codex-post-tool-use.js.map +0 -1
  375. package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +0 -48
  376. package/dist/cli/pack/hook-codex-pre-tool-use.js +0 -474
  377. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +0 -1
  378. package/dist/cli/pack/hook-codex-stop.d.ts +0 -35
  379. package/dist/cli/pack/hook-codex-stop.js +0 -329
  380. package/dist/cli/pack/hook-codex-stop.js.map +0 -1
  381. package/dist/cli/pack/hook-codex-user-prompt-submit.d.ts +0 -24
  382. package/dist/cli/pack/hook-codex-user-prompt-submit.js +0 -93
  383. package/dist/cli/pack/hook-codex-user-prompt-submit.js.map +0 -1
  384. package/dist/cli/pack/hook-post-merge-gate-record.d.ts +0 -37
  385. package/dist/cli/pack/hook-post-merge-gate-record.js +0 -190
  386. package/dist/cli/pack/hook-post-merge-gate-record.js.map +0 -1
  387. package/dist/cli/pack/hook-post-merge-gate.d.ts +0 -34
  388. package/dist/cli/pack/hook-post-merge-gate.js +0 -277
  389. package/dist/cli/pack/hook-post-merge-gate.js.map +0 -1
  390. package/dist/cli/pack/hook-post-tool-use.d.ts +0 -45
  391. package/dist/cli/pack/hook-post-tool-use.js +0 -137
  392. package/dist/cli/pack/hook-post-tool-use.js.map +0 -1
  393. package/dist/cli/pack/hook-pre-tool-use.d.ts +0 -92
  394. package/dist/cli/pack/hook-pre-tool-use.js +0 -1328
  395. package/dist/cli/pack/hook-pre-tool-use.js.map +0 -1
  396. package/dist/cli/pack/hook-runtime-reality.d.ts +0 -63
  397. package/dist/cli/pack/hook-runtime-reality.js +0 -210
  398. package/dist/cli/pack/hook-runtime-reality.js.map +0 -1
  399. package/dist/cli/pack/hook-solution-acceptance-writeguard.d.ts +0 -32
  400. package/dist/cli/pack/hook-solution-acceptance-writeguard.js +0 -438
  401. package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +0 -1
  402. package/dist/cli/pack/hook-solution-acceptance.d.ts +0 -157
  403. package/dist/cli/pack/hook-solution-acceptance.js +0 -530
  404. package/dist/cli/pack/hook-solution-acceptance.js.map +0 -1
  405. package/dist/cli/pack/hook-stay-in-scope.d.ts +0 -37
  406. package/dist/cli/pack/hook-stay-in-scope.js +0 -128
  407. package/dist/cli/pack/hook-stay-in-scope.js.map +0 -1
  408. package/dist/cli/pack/hook-subagent-start.d.ts +0 -23
  409. package/dist/cli/pack/hook-subagent-start.js +0 -88
  410. package/dist/cli/pack/hook-subagent-start.js.map +0 -1
  411. package/dist/cli/pack/hook-subagent-stop.d.ts +0 -19
  412. package/dist/cli/pack/hook-subagent-stop.js +0 -41
  413. package/dist/cli/pack/hook-subagent-stop.js.map +0 -1
  414. package/dist/cli/pack/hook-track-active-claim.d.ts +0 -27
  415. package/dist/cli/pack/hook-track-active-claim.js +0 -275
  416. package/dist/cli/pack/hook-track-active-claim.js.map +0 -1
  417. package/dist/cli/pack/transcript-report-scan.d.ts +0 -71
  418. package/dist/cli/pack/transcript-report-scan.js +0 -359
  419. package/dist/cli/pack/transcript-report-scan.js.map +0 -1
  420. package/dist/cli/pack/understanding-report-schema-hint.d.ts +0 -26
  421. package/dist/cli/pack/understanding-report-schema-hint.js +0 -120
  422. package/dist/cli/pack/understanding-report-schema-hint.js.map +0 -1
  423. package/dist/cli/pack/upgrade.d.ts +0 -32
  424. package/dist/cli/pack/upgrade.js +0 -273
  425. package/dist/cli/pack/upgrade.js.map +0 -1
  426. package/dist/cli/pause/index.d.ts +0 -113
  427. package/dist/cli/pause/index.js +0 -251
  428. package/dist/cli/pause/index.js.map +0 -1
  429. package/dist/cli/pause-check.d.ts +0 -22
  430. package/dist/cli/pause-check.js +0 -54
  431. package/dist/cli/pause-check.js.map +0 -1
  432. package/dist/cli/policy/intercept.d.ts +0 -233
  433. package/dist/cli/policy/intercept.js +0 -1022
  434. package/dist/cli/policy/intercept.js.map +0 -1
  435. package/dist/cli/policy/risk-envelope-enrichment.d.ts +0 -40
  436. package/dist/cli/policy/risk-envelope-enrichment.js +0 -110
  437. package/dist/cli/policy/risk-envelope-enrichment.js.map +0 -1
  438. package/dist/cli/record/index.d.ts +0 -81
  439. package/dist/cli/record/index.js +0 -348
  440. package/dist/cli/record/index.js.map +0 -1
  441. package/dist/cli/register-audit-group.js +0 -91
  442. package/dist/cli/register-audit-group.js.map +0 -1
  443. package/dist/cli/resolve-env.d.ts +0 -32
  444. package/dist/cli/resolve-env.js +0 -46
  445. package/dist/cli/resolve-env.js.map +0 -1
  446. package/dist/cli/session-export/index.d.ts +0 -46
  447. package/dist/cli/session-export/index.js +0 -169
  448. package/dist/cli/session-export/index.js.map +0 -1
  449. package/dist/cli/session-export/redact.d.ts +0 -22
  450. package/dist/cli/session-export/redact.js +0 -47
  451. package/dist/cli/session-export/redact.js.map +0 -1
  452. package/dist/cli/session-export/transcript.d.ts +0 -24
  453. package/dist/cli/session-export/transcript.js +0 -164
  454. package/dist/cli/session-export/transcript.js.map +0 -1
  455. package/dist/cli/session-start/branch-check.d.ts +0 -18
  456. package/dist/cli/session-start/branch-check.js +0 -140
  457. package/dist/cli/session-start/branch-check.js.map +0 -1
  458. package/dist/cli/session-start/index.d.ts +0 -121
  459. package/dist/cli/session-start/index.js +0 -591
  460. package/dist/cli/session-start/index.js.map +0 -1
  461. package/dist/cli/session-start/shared-options.d.ts +0 -103
  462. package/dist/cli/session-start/shared-options.js +0 -107
  463. package/dist/cli/session-start/shared-options.js.map +0 -1
  464. package/dist/cli/session-start/stale-base-check.d.ts +0 -46
  465. package/dist/cli/session-start/stale-base-check.js +0 -352
  466. package/dist/cli/session-start/stale-base-check.js.map +0 -1
  467. package/dist/cli/session-start/toolchain-parity.d.ts +0 -267
  468. package/dist/cli/session-start/toolchain-parity.js +0 -797
  469. package/dist/cli/session-start/toolchain-parity.js.map +0 -1
  470. package/dist/cli/smoke/assertions.d.ts +0 -46
  471. package/dist/cli/smoke/assertions.js +0 -170
  472. package/dist/cli/smoke/assertions.js.map +0 -1
  473. package/dist/cli/smoke/index.d.ts +0 -76
  474. package/dist/cli/smoke/index.js +0 -245
  475. package/dist/cli/smoke/index.js.map +0 -1
  476. package/dist/cli/smoke/runner.d.ts +0 -55
  477. package/dist/cli/smoke/runner.js +0 -134
  478. package/dist/cli/smoke/runner.js.map +0 -1
  479. package/dist/cli/smoke/stream-parser.d.ts +0 -65
  480. package/dist/cli/smoke/stream-parser.js +0 -115
  481. package/dist/cli/smoke/stream-parser.js.map +0 -1
  482. package/dist/cli/test-risk.d.ts +0 -27
  483. package/dist/cli/test-risk.js +0 -38
  484. package/dist/cli/test-risk.js.map +0 -1
  485. package/dist/io/display-path.d.ts +0 -11
  486. package/dist/io/display-path.js +0 -29
  487. package/dist/io/display-path.js.map +0 -1
  488. package/dist/io/duration.d.ts +0 -5
  489. package/dist/io/duration.js +0 -50
  490. package/dist/io/duration.js.map +0 -1
  491. package/dist/io/extract.d.ts +0 -79
  492. package/dist/io/extract.js +0 -226
  493. package/dist/io/extract.js.map +0 -1
  494. package/dist/io/ledger-record.d.ts +0 -83
  495. package/dist/io/ledger-record.js +0 -301
  496. package/dist/io/ledger-record.js.map +0 -1
  497. package/dist/io/read-json-dir-entries.d.ts +0 -36
  498. package/dist/io/read-json-dir-entries.js +0 -48
  499. package/dist/io/read-json-dir-entries.js.map +0 -1
  500. package/dist/io/safe-json-parse.d.ts +0 -5
  501. package/dist/io/safe-json-parse.js +0 -21
  502. package/dist/io/safe-json-parse.js.map +0 -1
  503. package/dist/policies/index.d.ts +0 -5
  504. package/dist/policies/index.js +0 -6
  505. package/dist/policies/index.js.map +0 -1
  506. package/dist/policies/ledger-client.d.ts +0 -113
  507. package/dist/policies/ledger-client.js +0 -500
  508. package/dist/policies/ledger-client.js.map +0 -1
  509. package/dist/policies/producers.d.ts +0 -12
  510. package/dist/policies/producers.js +0 -61
  511. package/dist/policies/producers.js.map +0 -1
  512. package/dist/policies/requires.d.ts +0 -75
  513. package/dist/policies/requires.js +0 -240
  514. package/dist/policies/requires.js.map +0 -1
  515. package/dist/policies/timestamp.d.ts +0 -14
  516. package/dist/policies/timestamp.js +0 -36
  517. package/dist/policies/timestamp.js.map +0 -1
  518. package/dist/policy-packs/builtin/permission-profiles.d.ts +0 -11
  519. package/dist/policy-packs/builtin/permission-profiles.js +0 -74
  520. package/dist/policy-packs/builtin/permission-profiles.js.map +0 -1
  521. package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +0 -286
  522. package/dist/policy-packs/builtin/post-merge-gate-runtime.js +0 -397
  523. package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +0 -1
  524. package/dist/policy-packs/builtin/post-merge-gate.d.ts +0 -56
  525. package/dist/policy-packs/builtin/post-merge-gate.js +0 -347
  526. package/dist/policy-packs/builtin/post-merge-gate.js.map +0 -1
  527. package/dist/policy-packs/builtin/solution-acceptance-reconnect.d.ts +0 -50
  528. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js +0 -151
  529. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js.map +0 -1
  530. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +0 -323
  531. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +0 -672
  532. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +0 -1
  533. package/dist/policy-packs/builtin/solution-acceptance.d.ts +0 -45
  534. package/dist/policy-packs/builtin/solution-acceptance.js +0 -226
  535. package/dist/policy-packs/builtin/solution-acceptance.js.map +0 -1
  536. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +0 -25
  537. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +0 -94
  538. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +0 -1
  539. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.d.ts +0 -35
  540. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js +0 -86
  541. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js.map +0 -1
  542. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.d.ts +0 -210
  543. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js +0 -361
  544. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js.map +0 -1
  545. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.d.ts +0 -304
  546. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +0 -734
  547. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +0 -1
  548. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.d.ts +0 -36
  549. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js +0 -73
  550. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js.map +0 -1
  551. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +0 -19
  552. package/dist/policy-packs/builtin/understanding-before-execution/index.js +0 -83
  553. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +0 -1
  554. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.d.ts +0 -135
  555. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js +0 -643
  556. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js.map +0 -1
  557. package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +0 -25
  558. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +0 -56
  559. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +0 -1
  560. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +0 -20
  561. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +0 -106
  562. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +0 -1
  563. package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +0 -126
  564. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +0 -245
  565. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +0 -1
  566. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.d.ts +0 -29
  567. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js +0 -82
  568. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js.map +0 -1
  569. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.d.ts +0 -71
  570. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js +0 -175
  571. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js.map +0 -1
  572. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +0 -625
  573. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +0 -1076
  574. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +0 -1
  575. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +0 -128
  576. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +0 -213
  577. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +0 -1
  578. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.d.ts +0 -129
  579. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js +0 -166
  580. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js.map +0 -1
  581. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +0 -164
  582. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +0 -275
  583. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +0 -1
  584. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +0 -1
  585. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +0 -10
  586. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +0 -1
  587. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +0 -526
  588. package/dist/policy-packs/builtin/understanding-before-execution.js +0 -1078
  589. package/dist/policy-packs/builtin/understanding-before-execution.js.map +0 -1
  590. package/dist/policy-packs/permission-translator.d.ts +0 -9
  591. package/dist/policy-packs/permission-translator.js +0 -76
  592. package/dist/policy-packs/permission-translator.js.map +0 -1
  593. package/dist/runtime/action-envelope.d.ts +0 -64
  594. package/dist/runtime/action-envelope.js +0 -46
  595. package/dist/runtime/action-envelope.js.map +0 -1
  596. package/dist/runtime/approval-signing.d.ts +0 -111
  597. package/dist/runtime/approval-signing.js +0 -384
  598. package/dist/runtime/approval-signing.js.map +0 -1
  599. package/dist/runtime/bash-match-facts.d.ts +0 -60
  600. package/dist/runtime/bash-match-facts.js +0 -196
  601. package/dist/runtime/bash-match-facts.js.map +0 -1
  602. package/dist/runtime/bash-match-registry.d.ts +0 -112
  603. package/dist/runtime/bash-match-registry.js +0 -406
  604. package/dist/runtime/bash-match-registry.js.map +0 -1
  605. package/dist/runtime/bash-prefix-parse.d.ts +0 -31
  606. package/dist/runtime/bash-prefix-parse.js +0 -576
  607. package/dist/runtime/bash-prefix-parse.js.map +0 -1
  608. package/dist/runtime/command-normalize.d.ts +0 -658
  609. package/dist/runtime/command-normalize.js +0 -2410
  610. package/dist/runtime/command-normalize.js.map +0 -1
  611. package/dist/runtime/deletion-target-resolve.d.ts +0 -28
  612. package/dist/runtime/deletion-target-resolve.js +0 -1078
  613. package/dist/runtime/deletion-target-resolve.js.map +0 -1
  614. package/dist/runtime/destructive-shell-floor.d.ts +0 -19
  615. package/dist/runtime/destructive-shell-floor.js +0 -611
  616. package/dist/runtime/destructive-shell-floor.js.map +0 -1
  617. package/dist/runtime/environment-resolver.d.ts +0 -36
  618. package/dist/runtime/environment-resolver.js +0 -138
  619. package/dist/runtime/environment-resolver.js.map +0 -1
  620. package/dist/runtime/git-context.d.ts +0 -201
  621. package/dist/runtime/git-context.js +0 -422
  622. package/dist/runtime/git-context.js.map +0 -1
  623. package/dist/runtime/index.d.ts +0 -12
  624. package/dist/runtime/index.js +0 -13
  625. package/dist/runtime/index.js.map +0 -1
  626. package/dist/runtime/intercept.d.ts +0 -623
  627. package/dist/runtime/intercept.js +0 -1384
  628. package/dist/runtime/intercept.js.map +0 -1
  629. package/dist/runtime/kube-context.d.ts +0 -16
  630. package/dist/runtime/kube-context.js +0 -63
  631. package/dist/runtime/kube-context.js.map +0 -1
  632. package/dist/runtime/kubectl-target-parse.d.ts +0 -31
  633. package/dist/runtime/kubectl-target-parse.js +0 -249
  634. package/dist/runtime/kubectl-target-parse.js.map +0 -1
  635. package/dist/runtime/ledger-add.d.ts +0 -16
  636. package/dist/runtime/ledger-add.js +0 -150
  637. package/dist/runtime/ledger-add.js.map +0 -1
  638. package/dist/runtime/ledger-writer.d.ts +0 -27
  639. package/dist/runtime/ledger-writer.js +0 -46
  640. package/dist/runtime/ledger-writer.js.map +0 -1
  641. package/dist/runtime/pause-sentinel.d.ts +0 -60
  642. package/dist/runtime/pause-sentinel.js +0 -156
  643. package/dist/runtime/pause-sentinel.js.map +0 -1
  644. package/dist/runtime/pending-approval.d.ts +0 -23
  645. package/dist/runtime/pending-approval.js +0 -67
  646. package/dist/runtime/pending-approval.js.map +0 -1
  647. package/dist/runtime/read-only-bash.d.ts +0 -141
  648. package/dist/runtime/read-only-bash.js +0 -2204
  649. package/dist/runtime/read-only-bash.js.map +0 -1
  650. package/dist/runtime/recovery-git-commit.d.ts +0 -22
  651. package/dist/runtime/recovery-git-commit.js +0 -284
  652. package/dist/runtime/recovery-git-commit.js.map +0 -1
  653. package/dist/runtime/reject-malformed-session-id.d.ts +0 -8
  654. package/dist/runtime/reject-malformed-session-id.js +0 -16
  655. package/dist/runtime/reject-malformed-session-id.js.map +0 -1
  656. package/dist/runtime/risk-classifier.d.ts +0 -38
  657. package/dist/runtime/risk-classifier.js +0 -311
  658. package/dist/runtime/risk-classifier.js.map +0 -1
  659. package/dist/runtime/session-id.d.ts +0 -113
  660. package/dist/runtime/session-id.js +0 -193
  661. package/dist/runtime/session-id.js.map +0 -1
  662. package/dist/runtime/shell-word.d.ts +0 -62
  663. package/dist/runtime/shell-word.js +0 -410
  664. package/dist/runtime/shell-word.js.map +0 -1
  665. package/dist/runtime/task-providers/agent-tasks.d.ts +0 -105
  666. package/dist/runtime/task-providers/agent-tasks.js +0 -247
  667. package/dist/runtime/task-providers/agent-tasks.js.map +0 -1
  668. package/dist/runtime/when-eval.d.ts +0 -62
  669. package/dist/runtime/when-eval.js +0 -212
  670. package/dist/runtime/when-eval.js.map +0 -1
  671. package/dist/runtime/workflow-policies.d.ts +0 -207
  672. package/dist/runtime/workflow-policies.js +0 -596
  673. package/dist/runtime/workflow-policies.js.map +0 -1
  674. package/dist/schema/audit.d.ts +0 -71
  675. package/dist/schema/audit.js +0 -32
  676. package/dist/schema/audit.js.map +0 -1
  677. package/dist/schema/doctor.d.ts +0 -25
  678. package/dist/schema/doctor.js +0 -23
  679. package/dist/schema/doctor.js.map +0 -1
  680. package/dist/schema/environments.d.ts +0 -215
  681. package/dist/schema/environments.js +0 -101
  682. package/dist/schema/environments.js.map +0 -1
  683. package/dist/schema/extract.d.ts +0 -9
  684. package/dist/schema/extract.js +0 -86
  685. package/dist/schema/extract.js.map +0 -1
  686. package/dist/schema/permission-profiles.d.ts +0 -2292
  687. package/dist/schema/permission-profiles.js +0 -60
  688. package/dist/schema/permission-profiles.js.map +0 -1
  689. package/dist/schema/policies.d.ts +0 -974
  690. package/dist/schema/policies.js +0 -298
  691. package/dist/schema/policies.js.map +0 -1
  692. package/dist/schema/requires.d.ts +0 -131
  693. package/dist/schema/requires.js +0 -88
  694. package/dist/schema/requires.js.map +0 -1
  695. package/dist/schema/risk.d.ts +0 -144
  696. package/dist/schema/risk.js +0 -194
  697. package/dist/schema/risk.js.map +0 -1
  698. package/dist/schema/session-start-preflight.d.ts +0 -10
  699. package/dist/schema/session-start-preflight.js +0 -106
  700. package/dist/schema/session-start-preflight.js.map +0 -1
  701. package/dist/schema/stale-base-check.d.ts +0 -18
  702. package/dist/schema/stale-base-check.js +0 -37
  703. package/dist/schema/stale-base-check.js.map +0 -1
  704. package/dist/schema/toolchain-parity.d.ts +0 -21
  705. package/dist/schema/toolchain-parity.js +0 -48
  706. package/dist/schema/toolchain-parity.js.map +0 -1
  707. package/dist/schema/workflows.d.ts +0 -519
  708. package/dist/schema/workflows.js +0 -81
  709. package/dist/schema/workflows.js.map +0 -1
  710. package/scripts/runtime-reality-docker-probe.mjs +0 -71
@@ -1,1076 +0,0 @@
1
- // Persisted JSON report handling (`.understanding-gate/reports/`), split
2
- // out of the former monolithic understanding-before-execution-runtime.ts
3
- // (structural concentration slice 2, agent-tasks 348a4d42). Pure move: see
4
- // src/policy-packs/builtin/understanding-before-execution/index.ts for
5
- // the re-exported public surface.
6
- //
7
- // `safeJsonParse` used to be defined and exported here (module-private in
8
- // the monolith, then exported so markers.ts could reuse it for
9
- // marker-body parsing). It has since moved to src/io/safe-json-parse.ts
10
- // (task 9bc0d546) so both call sites import a shared helper instead of one
11
- // importing it from the other -- import/export mechanics only, no
12
- // behavior change.
13
- import * as fs from "node:fs";
14
- import * as path from "node:path";
15
- import { atomicWriteFile } from "../../../io/atomic-write.js";
16
- import { safeJsonParse } from "../../../io/safe-json-parse.js";
17
- import { sha256Hex } from "../../../runtime/approval-signing.js";
18
- const DEFAULT_REPORTS_DIRNAME = ".understanding-gate";
19
- const REPORTS_SUBDIR = "reports";
20
- /**
21
- * Env var the persisted-report directory can be set from. Honored by
22
- * harness (`defaultReportsDir` below + emitted by `harness apply` onto
23
- * the pack-contributed hook commands) AND by `@lannguyensi/understanding-gate`
24
- * (its `core/persistence.js:resolveReportDir` reads the same name), so
25
- * the three actors that touch the directory — Stop hook (package),
26
- * PreToolUse blocker (harness), `harness approve understanding` — can
27
- * agree on the path regardless of each process's cwd.
28
- */
29
- export const REPORTS_DIR_ENV = "UNDERSTANDING_GATE_REPORT_DIR";
30
- /**
31
- * Resolve the persisted-report directory. Precedence:
32
- * 1. `UNDERSTANDING_GATE_REPORT_DIR` (taken verbatim — apply emits an
33
- * absolute path, operator-exported values are shell-expanded before
34
- * we see them).
35
- * 2. `<cwd>/.understanding-gate/reports` — backward-compat fallback.
36
- * Callers that have a stable anchor (the manifest directory) pass
37
- * it as `cwd` so the fallback agrees with whatever path apply
38
- * baked into the hook commands.
39
- */
40
- export function defaultReportsDir(cwd = process.cwd()) {
41
- const fromEnv = process.env[REPORTS_DIR_ENV];
42
- if (typeof fromEnv === "string" && fromEnv.length > 0)
43
- return fromEnv;
44
- return path.join(cwd, DEFAULT_REPORTS_DIRNAME, REPORTS_SUBDIR);
45
- }
46
- /**
47
- * Project root anchor for the reports directory: `<dir-of-manifest>/.understanding-gate/reports`.
48
- * Used by `harness apply` to bake an absolute, manifest-anchored value into
49
- * the pack-contributed hook commands' env, and by `harness approve` as the
50
- * fallback when `UNDERSTANDING_GATE_REPORT_DIR` is unset.
51
- */
52
- export function reportsDirForManifest(manifestPath) {
53
- return path.join(path.dirname(manifestPath), DEFAULT_REPORTS_DIRNAME, REPORTS_SUBDIR);
54
- }
55
- /**
56
- * Parse the ISO prefix of a producer filename
57
- * (`2026-05-24T06-16-39-409Z-<slug>-<hash>.json`) into epoch ms. The
58
- * producer flattens `:` and `.` to `-` for filesystem safety; undo that
59
- * before `Date.parse`. null when the name does not carry the prefix.
60
- */
61
- function parseFilenameIsoMs(name) {
62
- const m = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/.exec(name);
63
- if (!m)
64
- return null;
65
- const ms = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`);
66
- return Number.isNaN(ms) ? null : ms;
67
- }
68
- function readPersistedReport(filePath, mtimeMs, boundedRaw) {
69
- let raw;
70
- try {
71
- raw = boundedRaw ?? fs.readFileSync(filePath, "utf8");
72
- }
73
- catch {
74
- return null;
75
- }
76
- const parsed = safeJsonParse(raw);
77
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
78
- return null;
79
- const obj = parsed;
80
- const createdAt = typeof obj["createdAt"] === "string" ? obj["createdAt"] : null;
81
- const createdAtJsonMs = createdAt !== null ? Date.parse(createdAt) : Number.NaN;
82
- const createdAtMs = !Number.isNaN(createdAtJsonMs)
83
- ? createdAtJsonMs
84
- : (parseFilenameIsoMs(path.basename(filePath)) ?? mtimeMs);
85
- return {
86
- filePath,
87
- sessionId: typeof obj["sessionId"] === "string" ? obj["sessionId"] : null,
88
- approvalStatus: typeof obj["approvalStatus"] === "string" ? obj["approvalStatus"] : null,
89
- approvedAt: typeof obj["approvedAt"] === "string" ? obj["approvedAt"] : null,
90
- expiredBy: typeof obj["expiredBy"] === "string" ? obj["expiredBy"] : null,
91
- expiredAt: typeof obj["expiredAt"] === "string" ? obj["expiredAt"] : null,
92
- createdAt,
93
- createdAtMs,
94
- };
95
- }
96
- /**
97
- * List persisted reports under `dir`, newest-first by creation time
98
- * (JSON `createdAt`, then the filename ISO prefix, then mtime). Missing
99
- * directory returns []; an I/O error on a single file skips it. Reads each
100
- * regular file IN FULL, so it must not list the agent-writable reports
101
- * directory (use {@link listPersistedReportsBoundedWithSkips}); no `src`
102
- * caller remains, it stays for its tests and the pack's export surface.
103
- *
104
- * Creation time, NOT mtime, is the sort key: `harness approve
105
- * understanding` rewrites the report it flips, which bumps mtime and
106
- * made an old just-approved report sort as the freshest
107
- * (harness-discovery C1). mtime is the last-resort fallback.
108
- */
109
- export function listPersistedReports(dir) {
110
- let names;
111
- try {
112
- names = fs.readdirSync(dir);
113
- }
114
- catch {
115
- return [];
116
- }
117
- const reports = [];
118
- for (const name of names) {
119
- if (!name.endsWith(".json"))
120
- continue;
121
- const full = path.join(dir, name);
122
- let stat;
123
- try {
124
- stat = fs.statSync(full);
125
- }
126
- catch {
127
- continue;
128
- }
129
- if (!stat.isFile())
130
- continue;
131
- const report = readPersistedReport(full, stat.mtimeMs);
132
- if (!report)
133
- continue;
134
- reports.push(report);
135
- }
136
- reports.sort((a, b) => b.createdAtMs - a.createdAtMs);
137
- return reports;
138
- }
139
- /**
140
- * Maximum age a sessionId-null report may have for the tolerant
141
- * fallback to adopt it on the `harness approve understanding` path.
142
- * Sized for the real flow (Stop hook persists at turn end, operator
143
- * approves within minutes) with slack for a slow read-through. Live
144
- * repro that motivated it: a 17-day-old pending report got adopted,
145
- * validated, and stamped for a fresh session because the producer had
146
- * silently failed to persist the fresh report (harness-discovery C1,
147
- * friction-log #67).
148
- */
149
- export const TOLERANT_FALLBACK_MAX_AGE_MS = 15 * 60_000;
150
- /**
151
- * Tolerance for a sessionId-less candidate whose `createdAt` lies in
152
- * the FUTURE relative to the approve-time clock. A future creation
153
- * time is suspect either way: a forged `createdAt` (the producer's
154
- * Metadata block lets the agent author it) or serious clock skew.
155
- * Beyond this skew the fallback rejects the candidate just like a
156
- * stale one rather than trusting a timestamp that cannot be right.
157
- */
158
- export const TOLERANT_FALLBACK_FUTURE_SKEW_MS = 5 * 60_000;
159
- /**
160
- * Select the freshest report for a given session_id, or the freshest
161
- * applicable report when the persisted file lacks a sessionId field
162
- * (older package versions). `report: null` when nothing matches.
163
- *
164
- * The strict (sessionId-equals) match always wins. The tolerant
165
- * fallback's appetite is controlled by `opts.tolerantFallback` and
166
- * `opts.maxFallbackAgeMs` — see `FindReportOptions`. The selection
167
- * result carries enough context (`fallbackAdopted`, `staleRejected`)
168
- * for the caller to be loud about non-strict adoptions.
169
- */
170
- export function selectReportForSession(reports, sessionId, opts = {}) {
171
- // Strict match first.
172
- for (const r of reports) {
173
- if (r.sessionId === sessionId) {
174
- return { report: r, fallbackAdopted: false, staleRejected: [] };
175
- }
176
- }
177
- // Tolerant fallback: a report without sessionId is treated as
178
- // applicable to whichever session is asking. Only kicks in when no
179
- // matching sessionId-tagged report exists — which includes the case
180
- // where the producer Stop hook silently failed to persist the live
181
- // session's report, so the candidates here may be entirely unrelated
182
- // leftovers. `maxFallbackAgeMs` is the guard against adopting those.
183
- const mode = opts.tolerantFallback ?? "any";
184
- const nowMs = (opts.now ?? new Date()).getTime();
185
- const staleRejected = [];
186
- for (const r of reports) {
187
- if (r.sessionId !== null)
188
- continue;
189
- if (mode === "uncompleted" &&
190
- (r.approvalStatus === "approved" || r.approvalStatus === "expired")) {
191
- // A completed-cycle report from another session/task; skipping it
192
- // here is what stops `harness approve understanding` from binding
193
- // the live session to a stale, unrelated report.
194
- continue;
195
- }
196
- if (opts.maxFallbackAgeMs !== undefined) {
197
- const ageMs = nowMs - r.createdAtMs;
198
- if (ageMs > opts.maxFallbackAgeMs || ageMs < -TOLERANT_FALLBACK_FUTURE_SKEW_MS) {
199
- staleRejected.push(r);
200
- continue;
201
- }
202
- }
203
- return { report: r, fallbackAdopted: true, staleRejected };
204
- }
205
- return { report: null, fallbackAdopted: false, staleRejected };
206
- }
207
- /**
208
- * The NEWEST report whose `sessionId` strictly equals `sessionId`, or
209
- * `null` when the newest-first list has none. Deliberately NOT a thin
210
- * wrapper over {@link selectReportForSession}: it stops at the first
211
- * strict match and never consults the sessionId-null tolerant fallback.
212
- *
213
- * Written for the PreToolUse hook's auto-approval path
214
- * (agent-tasks/74b4b17d, ADR
215
- * docs/decisions/2026-08-27-ug-auto-mode-approval.md, Option A condition
216
- * 3), where both of the fallback's properties are unacceptable:
217
- *
218
- * - A sessionId-null report is adopted by whichever session asks, so
219
- * the fallback would let a report bound to no session at all mint a
220
- * marker for THIS one. The auto path has no operator to confirm that
221
- * adoption the way `harness approve understanding` does.
222
- * - Eligibility is evaluated on the newest report ONLY. The caller
223
- * checks `approvalStatus === "pending"` on whatever this returns; if
224
- * the newest strict-session report was already consumed, the session
225
- * is ineligible even when an older `pending` report is still on disk.
226
- * Scanning further down the list for some other pending report is
227
- * exactly the re-mint this design forbids.
228
- */
229
- export function selectNewestStrictSessionReport(reports, sessionId) {
230
- for (const r of reports) {
231
- if (r.sessionId === sessionId)
232
- return r;
233
- }
234
- return null;
235
- }
236
- /**
237
- * Back-compat wrapper around `selectReportForSession` for callers that
238
- * only need the report (the gate read and expiry paths).
239
- */
240
- export function findLatestReportForSession(reports, sessionId, opts = {}) {
241
- return selectReportForSession(reports, sessionId, opts).report;
242
- }
243
- /**
244
- * Distinct, greppable audit phrase for the case where the persisted report
245
- * on disk says `approvalStatus: "approved"` but no validly-signed approval
246
- * marker backs it (task 7402301d). The counterpart of the marker path's
247
- * `forged/unsigned marker rejected` phrase: both PreToolUse hooks embed
248
- * `PersistedReportEvidence.detail` in their block reason, so this phrase
249
- * is what an operator or auditor greps for to tell "someone dropped an
250
- * approved-looking report into the reports directory" (or: a report was
251
- * approved through a path that never wrote the signed marker, e.g. the
252
- * standalone `understanding-gate approve` CLI) apart from the routine
253
- * "never approved" case. Module-private on purpose: the tests that pin
254
- * the phrase spell it out literally, so a change here turns them red
255
- * instead of silently following the constant.
256
- */
257
- const UNSIGNED_REPORT_APPROVAL_REJECTED = "unsigned persisted-report approval rejected";
258
- /** Cap applied to each interpolated value in `checkPersistedReport`'s `detail`. */
259
- const DETAIL_VALUE_MAX_LENGTH = 120;
260
- /**
261
- * `detail` is not an inert log line: both PreToolUse hooks embed it
262
- * verbatim into their block `reason`, and the Codex hook additionally
263
- * writes it to stderr, the block-reason surface an operator reads
264
- * directly. The report file's basename and its `approvedAt` field both
265
- * come from a JSON file whose content the gated agent controls (it is
266
- * only unsigned evidence, task 7402301d), so neither is trusted
267
- * verbatim here: control characters (including newlines, which could
268
- * forge extra `reason:`-looking lines) are replaced with a space and
269
- * the result is capped so one field cannot blow out the surface.
270
- */
271
- export function sanitizeDetailValue(value) {
272
- // Deliberately strips C0/DEL control characters (including newline,
273
- // which could otherwise forge an extra `reason:`-looking stderr line).
274
- const flattened = value.replace(/[\x00-\x1f\x7f]/g, " ");
275
- return flattened.length > DETAIL_VALUE_MAX_LENGTH
276
- ? `${flattened.slice(0, DETAIL_VALUE_MAX_LENGTH)}...`
277
- : flattened;
278
- }
279
- /**
280
- * Flip the latest matching persisted report's approvalStatus to
281
- * `expired` so the audit record agrees with the cleared marker
282
- * (harness/1ee26e77 follow-up: post-tool-use expiry was marker-only; the
283
- * persisted report at .understanding-gate/reports/ silently kept
284
- * satisfying the gate even after task_finish deleted the marker. Since
285
- * task 7402301d the report carries no gate authority at all, so this flip
286
- * is audit hygiene rather than a second gate closure).
287
- *
288
- * Atomic rewrite. Preserves the rest of the report body so the audit
289
- * trail (the operator's actual Understanding text + previous approval
290
- * timestamps) stays intact; only the status fields change.
291
- *
292
- * Returns `{ ok: true, filePath, previousStatus }` on success,
293
- * `{ ok: false, reason }` when no matching report exists or rewrite
294
- * failed. Non-throwing: caller (post-tool-use hook) uses this as a
295
- * best-effort cleanup so a missing report dir or unrelated I/O issue
296
- * does not escalate into a session-breaking hook failure.
297
- */
298
- export function expirePersistedReport(reportsDir, sessionId, now = new Date(), trigger) {
299
- const reports = listPersistedReportsBounded(reportsDir);
300
- if (reports.length === 0) {
301
- return { ok: false, reason: `no reports under ${reportsDir}` };
302
- }
303
- const latest = findLatestReportForSession(reports, sessionId);
304
- if (!latest) {
305
- return {
306
- ok: false,
307
- reason: `no report matched session_id=${sessionId} (${reports.length} report(s) for other sessions)`,
308
- };
309
- }
310
- if (latest.approvalStatus !== "approved") {
311
- return {
312
- ok: false,
313
- reason: `latest report ${sanitizeDetailValue(path.basename(latest.filePath))} already has approvalStatus=${sanitizeDetailValue(latest.approvalStatus ?? "<missing>")}, nothing to expire`,
314
- };
315
- }
316
- // Bounded like the listing: the file may have been swapped since.
317
- const read = readReportFileBounded(latest.filePath);
318
- if (!read.ok) {
319
- return { ok: false, reason: `failed to read ${latest.filePath}: ${read.detail}` };
320
- }
321
- const raw = read.raw;
322
- let parsed;
323
- try {
324
- parsed = JSON.parse(raw);
325
- }
326
- catch (err) {
327
- return { ok: false, reason: `failed to parse ${latest.filePath}: ${err.message}` };
328
- }
329
- const previousStatus = typeof parsed["approvalStatus"] === "string" ? parsed["approvalStatus"] : null;
330
- parsed["approvalStatus"] = "expired";
331
- parsed["expiredAt"] = now.toISOString();
332
- if (trigger !== undefined && trigger !== "") {
333
- parsed["expiredBy"] = trigger;
334
- }
335
- else {
336
- // No event to record: drop a stale one from an earlier expiry.
337
- delete parsed["expiredBy"];
338
- }
339
- try {
340
- atomicWriteFile(latest.filePath, `${JSON.stringify(parsed, null, 2)}\n`);
341
- }
342
- catch (err) {
343
- return {
344
- ok: false,
345
- reason: `failed to rewrite ${latest.filePath}: ${err.message}`,
346
- };
347
- }
348
- return { ok: true, filePath: latest.filePath, previousStatus };
349
- }
350
- /**
351
- * Gate-side EVIDENCE probe of the persisted report (task 7402301d). Both
352
- * PreToolUse hooks call this after the signed-marker check has NOT
353
- * matched, purely to (a) put a precise reason into the block diagnostic
354
- * and (b) tell "no report at all" (`report: null`, which gates the
355
- * parse-error lookup) apart from "a report exists but is pending".
356
- *
357
- * It never returns an approval. The selection still uses the tolerant
358
- * sessionId-null fallback (`"any"`, no age limit) so the diagnostic keeps
359
- * naming a legacy session's own sessionId-less report; that leniency is
360
- * harmless now because nothing here can open the gate. A report whose
361
- * on-disk status is `approved` yields `claimsApproved: true` with the
362
- * `UNSIGNED_REPORT_APPROVAL_REJECTED` phrase in `detail`, the distinct
363
- * audit signal for a report-side forgery attempt (or an approval that
364
- * bypassed `harness approve understanding`, e.g. the standalone
365
- * `understanding-gate approve` CLI, which writes no signed marker).
366
- */
367
- export function checkPersistedReport(reportsDir, sessionId) {
368
- // Bounded by entry count: the directory is agent-writable, and a read of
369
- // hundreds of thousands of planted entries runs the hook past its budget,
370
- // which the runtime treats as an allow. Past the bound nothing is opened and
371
- // the answer is no evidence (never an approval, so the block stands).
372
- const listing = listPersistedReportsBoundedWithSkips(reportsDir, {
373
- maxEntries: MAX_HOOK_LISTING_ENTRIES,
374
- });
375
- if (listing.truncated) {
376
- return {
377
- claimsApproved: false,
378
- detail: `no report evidence read: ${reportsDir} ${listing.truncatedDetail}`,
379
- report: null,
380
- };
381
- }
382
- const reports = listing.reports;
383
- if (reports.length === 0) {
384
- return {
385
- claimsApproved: false,
386
- detail: `no reports found at ${reportsDir}`,
387
- report: null,
388
- };
389
- }
390
- const latest = findLatestReportForSession(reports, sessionId);
391
- if (!latest) {
392
- return {
393
- claimsApproved: false,
394
- detail: `no report matched session_id=${sessionId} (${reports.length} report(s) for other sessions)`,
395
- report: null,
396
- };
397
- }
398
- const safeFileName = sanitizeDetailValue(path.basename(latest.filePath));
399
- if (latest.approvalStatus !== "approved") {
400
- const boundaryExpiry = describeBoundaryExpiry(latest);
401
- const expirySuffix = boundaryExpiry !== undefined ? `; ${boundaryExpiry}` : "";
402
- return {
403
- claimsApproved: false,
404
- detail: `latest report ${safeFileName} has approvalStatus=${sanitizeDetailValue(latest.approvalStatus ?? "<missing>")}${expirySuffix}`,
405
- report: latest,
406
- };
407
- }
408
- const safeApprovedAt = latest.approvedAt ? sanitizeDetailValue(latest.approvedAt) : null;
409
- return {
410
- claimsApproved: true,
411
- detail: `${UNSIGNED_REPORT_APPROVAL_REJECTED}: report ${safeFileName} has ` +
412
- `approvalStatus=approved${safeApprovedAt ? ` (approved at ${safeApprovedAt})` : ""} ` +
413
- `but the persisted report is evidence, not authority; the gate opens only on a ` +
414
- `validly-signed approval marker written by \`harness approve understanding\``,
415
- report: latest,
416
- };
417
- }
418
- /**
419
- * `approval expired because <event> at <time>` for a report a PostToolUse
420
- * boundary expired (`expirePersistedReport` stamps `expiredBy` and
421
- * `expiredAt`), `undefined` for any other report, including an older one or
422
- * one a package producer wrote that carries no `expiredBy`. Both values come
423
- * from a JSON file the gated agent can write, so both pass
424
- * `sanitizeDetailValue`.
425
- */
426
- export function describeBoundaryExpiry(report) {
427
- if (report.approvalStatus !== "expired")
428
- return undefined;
429
- if (report.expiredBy === null || report.expiredBy === "")
430
- return undefined;
431
- const when = report.expiredAt !== null && report.expiredAt !== ""
432
- ? ` at ${sanitizeDetailValue(report.expiredAt)}`
433
- : "";
434
- return `approval expired because ${sanitizeDetailValue(report.expiredBy)}${when}`;
435
- }
436
- /**
437
- * Report fields the approval and expiry lifecycles rewrite. They are left
438
- * out of {@link canonicalReportHash}: `rewriteReportApproved` flips
439
- * `approvalStatus`/`approvedAt`/`approvedBy`, may stamp `sessionId`, and
440
- * drops `expiredAt`/`expiredBy`; `expirePersistedReport` later sets
441
- * `approvalStatus`/`expiredAt`/`expiredBy` again. The hash must survive all
442
- * of that for an untouched report, so it covers only the content the
443
- * operator actually reviewed.
444
- */
445
- const LIFECYCLE_REPORT_FIELDS = new Set([
446
- "approvalStatus",
447
- "approvedAt",
448
- "approvedBy",
449
- "expiredAt",
450
- "expiredBy",
451
- "sessionId",
452
- ]);
453
- /**
454
- * Deepest array/object nesting {@link canonicalReportHash} serialises (the
455
- * report object itself is level 1). `sortKeysDeep` and `JSON.stringify`
456
- * recurse once per level, and any agent-written `*.json` file in the reports
457
- * directory reaches them through the gate-read scan, so without a bound a
458
- * file nested a few thousand levels deep overflows the stack and the hook
459
- * process dies instead of deciding. A real report nests a handful of levels.
460
- */
461
- const MAX_CANONICAL_REPORT_DEPTH = 64;
462
- /**
463
- * Largest report file, in bytes, the canonical hash reads from disk (1 MiB).
464
- * Every `*.json` entry of the reports directory reaches the hash through the
465
- * gate-read scan, and the agent can write that directory, so without a bound
466
- * a planted file of a few hundred megabytes made the hook run out of heap or
467
- * past its time budget instead of deciding (a hook that dies or overruns is a
468
- * non-blocking error for the runtime). A real report is a few kilobytes, far
469
- * below the cap. A file over the cap matches nothing at gate read, and both
470
- * producers refuse to approve such a report.
471
- */
472
- export const MAX_HASHED_REPORT_BYTES = 1024 * 1024;
473
- /**
474
- * Budget the gate-read hash scan spends before it gives up (32 MiB), charged
475
- * per `*.json` entry as the larger of the bytes read and
476
- * {@link MIN_SCAN_ENTRY_COST_BYTES}. The per-file cap bounds one file, not
477
- * their number: 160 planted 1 MiB key-heavy files took 15.9 to 17.6 s of scan
478
- * time, past the 15 s PreToolUse budget, and a hook past its budget is a
479
- * non-blocking error for the runtime (an allow). Charging bytes alone left the
480
- * entry count open: entries that cost no bytes (tiny files, directories named
481
- * `*.json`, files over the cap, unreadable ones) still cost an open each, and
482
- * 400k tiny files took about 22 s of CPU. A scan that spent the budget without
483
- * finding every wanted hash stops and the check fails closed with the mismatch
484
- * reason. With the floor the scan opens at most 8192 entries and reads about
485
- * 32 MiB (the last file read may add up to the 1 MiB per-file cap), whichever
486
- * the entries are. Real reports are a few kilobytes, so the budget covers
487
- * thousands of them. The scan reads the newest file first, which is where the
488
- * approved report usually is.
489
- *
490
- * The other full-directory reads on the PreToolUse path spend the same
491
- * budget with the same floor, each its own (a fresh {@link ReadBudget} per
492
- * listing, not one shared across the hook call): the evidence read, the
493
- * auto-approval precondition listing, the subagent-delegation lookup and the
494
- * parse-error log lookup each read at most about 32 MiB, and none opens an
495
- * entry once the directory holds more than {@link MAX_HOOK_LISTING_ENTRIES}.
496
- */
497
- export const MAX_HASH_SCAN_BYTES = 32 * 1024 * 1024;
498
- /**
499
- * Least a `*.json` entry is charged against {@link MAX_HASH_SCAN_BYTES}
500
- * whatever the read returned (4 KiB, about one real report), so entries that
501
- * read nothing or little still spend the budget at a rate that bounds their
502
- * count: 32 MiB / 4 KiB = 8192 entries.
503
- */
504
- export const MIN_SCAN_ENTRY_COST_BYTES = 4096;
505
- /**
506
- * Most entries (8192, the hash scan's budget divided by its per-entry floor)
507
- * a PreToolUse-path reader accepts in one agent-writable directory. The hash
508
- * scan stops at its budget, but four more readers on the same hook path used
509
- * to open every entry (the evidence read, the auto-approval precondition
510
- * listing, the subagent-delegation lookup, the parse-error log lookup), so a
511
- * directory of hundreds of thousands of planted entries took the hook past the
512
- * 15 s PreToolUse budget, which the runtime treats as an allow (measured at
513
- * 400k planted entries: 408195 opens and 28.9 s of CPU in the Claude Code
514
- * hook). The count alone left each entry readable up to the 1 MiB per-file
515
- * cap, so every one of those readers also charges the entries it reads against
516
- * {@link MAX_HASH_SCAN_BYTES} (see {@link ReadBudget}).
517
- *
518
- * A reader does not take a newest window of such a directory: a partial view
519
- * would let a hidden newer entry change a selection (an auto-approval would
520
- * adopt an older pending report while a newer, hidden one is already
521
- * approved). Past either bound (more entries, or more report data than the
522
- * byte budget) it returns nothing and reports the directory as too large to
523
- * read, and each reader treats that as no evidence: the evidence read
524
- * reports no report, the auto-approval path declines, the delegation lookup
525
- * captures nothing and the parse-error lookup finds nothing. None of them
526
- * allows. The directory listing stops as soon as the bound is crossed. See
527
- * {@link listDirNamesBounded}.
528
- */
529
- export const MAX_HOOK_LISTING_ENTRIES = MAX_HASH_SCAN_BYTES / MIN_SCAN_ENTRY_COST_BYTES;
530
- /**
531
- * Most entries (matching or not) {@link listDirNamesBounded} iterates in one
532
- * directory, as a multiple of `maxEntries`. The reports and parse-errors
533
- * directories hold nothing but their `*.json` and `*.log` entries, so a
534
- * directory with many more names than that is planted, not real.
535
- */
536
- const HOOK_LISTING_SCAN_FACTOR = 2;
537
- const TRUNCATED_REMEDY = "remove non-report or stale *.json entries from it by hand (`harness gc` removes only aged approved or expired reports)";
538
- /** Clause a past-the-entry-bound listing reports; the caller supplies the subject. */
539
- const ENTRIES_TRUNCATED_DETAIL = `holds more than ${MAX_HOOK_LISTING_ENTRIES} *.json entries, or more than ${MAX_HOOK_LISTING_ENTRIES * HOOK_LISTING_SCAN_FACTOR} entries of any name, more than the gate reads; ${TRUNCATED_REMEDY}`;
540
- /** Clause a past-the-byte-budget listing reports; the caller supplies the subject. */
541
- const BYTES_TRUNCATED_DETAIL = `holds more than ${MAX_HASH_SCAN_BYTES / (1024 * 1024)} MiB of report data, more than the gate reads; ${TRUNCATED_REMEDY}`;
542
- /**
543
- * Names under `dir` ending in `suffix`, or none and `truncated: true` when
544
- * there are more than `maxEntries` of them. The one listing the bounded
545
- * PreToolUse-path readers share, so the bound lives in one place; a missing or
546
- * unreadable directory lists nothing and is not `truncated`.
547
- *
548
- * The listing reads the directory one entry at a time and stops as soon as
549
- * `maxEntries + 1` matching names have been seen, or `maxScanned` entries of
550
- * any name (default: {@link HOOK_LISTING_SCAN_FACTOR} times `maxEntries`),
551
- * so millions of planted names, matching or not, cost a bounded number of
552
- * directory reads and no allocation of their names. The directory handle is
553
- * always closed. An unbounded `maxEntries` reads the whole directory. The
554
- * names come back in ascending byte order (the `fs.readdirSync` order), not
555
- * in the filesystem's iteration order.
556
- */
557
- export function listDirNamesBounded(dir, suffix, maxEntries, maxScanned = maxEntries * HOOK_LISTING_SCAN_FACTOR) {
558
- let handle;
559
- try {
560
- handle = fs.opendirSync(dir);
561
- }
562
- catch {
563
- return { names: [], truncated: false };
564
- }
565
- const names = [];
566
- let scanned = 0;
567
- try {
568
- for (let entry = handle.readSync(); entry !== null; entry = handle.readSync()) {
569
- scanned += 1;
570
- if (scanned > maxScanned)
571
- return { names: [], truncated: true };
572
- if (!entry.name.endsWith(suffix))
573
- continue;
574
- names.push(entry.name);
575
- if (names.length > maxEntries)
576
- return { names: [], truncated: true };
577
- }
578
- }
579
- catch {
580
- return { names: [], truncated: false };
581
- }
582
- finally {
583
- try {
584
- handle.closeSync();
585
- }
586
- catch {
587
- // Already gone; nothing left to release.
588
- }
589
- }
590
- return { names: sortNamesByBytes(names), truncated: false };
591
- }
592
- /**
593
- * `names` in ascending byte order of their UTF-8 encoding: the order
594
- * `fs.readdirSync` returns (libuv sorts its scandir with `strcmp`), so the
595
- * readers that took names from it before {@link listDirNamesBounded} read
596
- * the directory one entry at a time keep the same order, and ties in their
597
- * own newest-first sort stay deterministic instead of following the
598
- * filesystem's iteration order.
599
- */
600
- function sortNamesByBytes(names) {
601
- return names
602
- .map((name) => ({ name, key: Buffer.from(name, "utf8") }))
603
- .sort((a, b) => Buffer.compare(a.key, b.key))
604
- .map((entry) => entry.name);
605
- }
606
- /**
607
- * Iterative walk over the arrays and objects of `value` (the value itself is
608
- * level 1) that stops at the first one nested deeper than `max` levels.
609
- * Iterative, so the walk itself cannot overflow the stack. Only arrays and
610
- * objects are pushed: a scalar child (string, number, boolean, null) cannot
611
- * nest, so a wide array of scalars costs no stack slot per element, and
612
- * `visited` counts the root plus the containers below it. Exported for tests.
613
- */
614
- export function walkContainerDepth(value, max) {
615
- const nodes = [value];
616
- const depths = [1];
617
- let visited = 0;
618
- while (nodes.length > 0) {
619
- const node = nodes.pop();
620
- const depth = depths.pop() ?? 1;
621
- visited += 1;
622
- if (node === null || typeof node !== "object")
623
- continue;
624
- if (depth > max)
625
- return { tooDeep: true, visited };
626
- for (const child of Array.isArray(node) ? node : Object.values(node)) {
627
- if (child !== null && typeof child === "object") {
628
- nodes.push(child);
629
- depths.push(depth + 1);
630
- }
631
- }
632
- }
633
- return { tooDeep: false, visited };
634
- }
635
- /** True when `value` nests arrays/objects deeper than `max` levels. */
636
- function nestsDeeperThan(value, max) {
637
- return walkContainerDepth(value, max).tooDeep;
638
- }
639
- function sortKeysDeep(value) {
640
- if (Array.isArray(value))
641
- return value.map(sortKeysDeep);
642
- if (value !== null && typeof value === "object") {
643
- const entries = Object.entries(value)
644
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
645
- .map(([k, v]) => [k, sortKeysDeep(v)]);
646
- // `Object.fromEntries` defines own properties, so a report key named
647
- // `__proto__` stays data instead of re-parenting the copy.
648
- return Object.fromEntries(entries);
649
- }
650
- return value;
651
- }
652
- /**
653
- * The hash an approval marker signs as its `reportContentHash`, and the one
654
- * the gate recomputes at read time (task fa423e9b): sha256 over a key-sorted
655
- * JSON serialisation of the parsed report with the lifecycle fields in
656
- * `LIFECYCLE_REPORT_FIELDS` removed. A raw-bytes hash cannot do this job: the
657
- * producers hash before `rewriteReportApproved` re-serialises the file and
658
- * `expirePersistedReport` rewrites it again, so the raw bytes of an untouched
659
- * report never match what the marker signed. One function on purpose, shared
660
- * by both producers (`harness approve understanding`, the auto-approval path)
661
- * and the verifier.
662
- *
663
- * Total: returns null instead of throwing for a report nested deeper than
664
- * `MAX_CANONICAL_REPORT_DEPTH`. The verifier counts such a file as one that
665
- * matches nothing; both producers refuse to approve such a report, so no
666
- * marker is ever signed without a content binding because of it. Report
667
- * files reach it only after {@link readReportFileBounded} bounded their type
668
- * and size (the gate-read scan and `harness approve understanding` through
669
- * {@link hashReportFile}, the auto-approval path directly).
670
- */
671
- export function canonicalReportHash(report) {
672
- // `Object.fromEntries` (not `kept[key] = value`): a report key named
673
- // `__proto__` must stay hashed data, not re-parent the copy and vanish.
674
- const kept = Object.fromEntries(Object.entries(report).filter(([key]) => !LIFECYCLE_REPORT_FIELDS.has(key)));
675
- if (nestsDeeperThan(kept, MAX_CANONICAL_REPORT_DEPTH))
676
- return null;
677
- return sha256Hex(JSON.stringify(sortKeysDeep(kept)));
678
- }
679
- /** What `readReportFileBounded` returns for a symbolic link when `noFollow` is set. */
680
- const SYMLINK_REFUSED = {
681
- ok: false,
682
- reason: "not-regular",
683
- detail: "a symbolic link, not a regular file",
684
- };
685
- /** What `readReportFileBounded` returns once its `budget` is spent; nothing is opened. */
686
- const OVER_BUDGET = {
687
- ok: false,
688
- reason: "over-budget",
689
- detail: "not read: the reader's byte budget is spent",
690
- };
691
- function errorCode(err) {
692
- const code = err?.code;
693
- return typeof code === "string" ? code : String(err);
694
- }
695
- /**
696
- * Read a report file for hashing, bounded by type and size. This is the one
697
- * read every canonical report hash goes through: the gate-read scan (via
698
- * {@link hashReportFile}) and both producers. The path is opened once,
699
- * read-only and non-blocking (`O_NONBLOCK`: opening a FIFO with no writer
700
- * returns at once instead of waiting for one); its type and size come from
701
- * `fstat` on that descriptor, and the content is read through the same
702
- * descriptor, so nothing can be swapped in between (no separate stat of the
703
- * path, which a symlink flipped to a FIFO used to race). Refused without
704
- * reading: anything that is not a regular file, and a file larger than
705
- * `MAX_HASHED_REPORT_BYTES`. The read stops one byte past the fstat size
706
- * (never more than the cap plus one byte); a file that grows into that byte
707
- * while being read is refused too. The descriptor is always closed. Never
708
- * throws.
709
- *
710
- * `opts.noFollow` adds `O_NOFOLLOW` to the open: a path that is a symbolic
711
- * link (whatever it points at, a dangling link included) is refused as
712
- * not-regular by the open itself (`ELOOP`), with no earlier `lstat` for a
713
- * swap to race. Off by default, so every hook read keeps following links
714
- * exactly as before; `harness approve understanding` turns it on.
715
- *
716
- * `opts.budget` charges the read against {@link MAX_HASH_SCAN_BYTES} the way
717
- * the hash scan does (the larger of the `fstat` size and
718
- * {@link MIN_SCAN_ENTRY_COST_BYTES} per entry, taken before the read); once
719
- * the budget is spent the call opens nothing and returns `over-budget`.
720
- */
721
- export function readReportFileBounded(filePath, opts = {}) {
722
- const budget = opts.budget;
723
- if (budget !== undefined && budget.spent >= MAX_HASH_SCAN_BYTES)
724
- return OVER_BUDGET;
725
- let fd;
726
- try {
727
- fd = fs.openSync(filePath, fs.constants.O_RDONLY |
728
- fs.constants.O_NONBLOCK |
729
- (opts.noFollow === true ? fs.constants.O_NOFOLLOW : 0));
730
- }
731
- catch (err) {
732
- if (budget !== undefined)
733
- budget.spent += MIN_SCAN_ENTRY_COST_BYTES;
734
- if (opts.noFollow === true && errorCode(err) === "ELOOP")
735
- return SYMLINK_REFUSED;
736
- return { ok: false, reason: "unreadable", detail: `could not be opened (${errorCode(err)})` };
737
- }
738
- try {
739
- const stat = fs.fstatSync(fd);
740
- // Charged from the descriptor's size BEFORE the read, so the bytes read
741
- // never run past the budget by more than the one file in flight: the
742
- // larger of the size and the per-entry floor for a file about to be read,
743
- // the floor for one that is refused unread.
744
- if (budget !== undefined) {
745
- budget.spent +=
746
- stat.isFile() && stat.size <= MAX_HASHED_REPORT_BYTES
747
- ? Math.max(stat.size, MIN_SCAN_ENTRY_COST_BYTES)
748
- : MIN_SCAN_ENTRY_COST_BYTES;
749
- }
750
- if (!stat.isFile())
751
- return { ok: false, reason: "not-regular", detail: "not a regular file" };
752
- if (stat.size > MAX_HASHED_REPORT_BYTES) {
753
- return {
754
- ok: false,
755
- reason: "too-large",
756
- detail: `${stat.size} bytes, over the ${MAX_HASHED_REPORT_BYTES}-byte cap for hashing its content`,
757
- };
758
- }
759
- const buf = Buffer.allocUnsafe(stat.size + 1);
760
- let total = 0;
761
- while (total < buf.length) {
762
- const n = fs.readSync(fd, buf, total, buf.length - total, null);
763
- if (n === 0)
764
- break;
765
- total += n;
766
- }
767
- if (total > stat.size)
768
- return { ok: false, reason: "grew", detail: "grew while being read" };
769
- return { ok: true, raw: buf.toString("utf8", 0, total), mtimeMs: stat.mtimeMs };
770
- }
771
- catch (err) {
772
- return { ok: false, reason: "unreadable", detail: `could not be read (${errorCode(err)})` };
773
- }
774
- finally {
775
- try {
776
- fs.closeSync(fd);
777
- }
778
- catch {
779
- // Already gone; nothing left to release.
780
- }
781
- }
782
- }
783
- /**
784
- * Canonical hash of the report file at `filePath`, read through
785
- * {@link readReportFileBounded}, or why it has none: not a regular file, over
786
- * `MAX_HASHED_REPORT_BYTES`, unreadable, grown while read, not a JSON object,
787
- * or nested too deeply. `harness approve understanding` refuses a report on
788
- * any of these (the `detail` is its message); the gate-read scan counts such
789
- * a file as one that matches nothing. Never throws.
790
- */
791
- export function hashReportFile(filePath, opts = {}) {
792
- const read = readReportFileBounded(filePath, opts);
793
- if (!read.ok)
794
- return read;
795
- return hashReportText(read.raw);
796
- }
797
- /** {@link hashReportFile} for text already read through {@link readReportFileBounded}. */
798
- function hashReportText(raw) {
799
- const parsed = safeJsonParse(raw);
800
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
801
- return { ok: false, reason: "not-json-object", detail: "not a JSON object" };
802
- }
803
- const hash = canonicalReportHash(parsed);
804
- if (hash === null) {
805
- return { ok: false, reason: "too-deep", detail: "nested too deeply to hash its content" };
806
- }
807
- return { ok: true, hash };
808
- }
809
- /** Canonical hash of the report file at `filePath` ({@link hashReportFile}); null when it has none. */
810
- export function canonicalReportHashOfFile(filePath, opts = {}) {
811
- const hashed = hashReportFile(filePath, opts);
812
- return hashed.ok ? hashed.hash : null;
813
- }
814
- /**
815
- * Hash every `*.json` entry of `dir` (any session, any `approvalStatus`)
816
- * through {@link readReportFileBounded} and report which of the `wanted`
817
- * hashes occur. Stops as soon as every wanted hash was found. Every entry
818
- * counts as a report file; one that is not a regular file, is larger than
819
- * `MAX_HASHED_REPORT_BYTES`, cannot be opened or read, grows while read, is
820
- * not a JSON object, or nests too deeply matches nothing. Entries are taken
821
- * in descending name order, newest first (report names start with a
822
- * timestamp), so the approved report is usually the first file read. Every
823
- * entry, whatever its read returned, is charged the larger of the bytes read
824
- * and `MIN_SCAN_ENTRY_COST_BYTES`; once the charges reach `MAX_HASH_SCAN_BYTES`
825
- * with a wanted hash still unfound the scan stops and sets `budgetExhausted`,
826
- * so the number of entries opened and the bytes read (about 32 MiB, the last
827
- * file read may add up to the per-file cap) are both bounded by the budget.
828
- *
829
- * The directory is listed through {@link listDirNamesBounded}, one entry at a
830
- * time, so the listing is bounded too: it stops as soon as it has seen more
831
- * than {@link MAX_HOOK_LISTING_ENTRIES} `*.json` names, or twice that many
832
- * entries of any name, and then no entry is opened and the scan sets
833
- * `truncated` (the caller denies: fail closed). Within those bounds the
834
- * selection is unchanged: every `*.json` name, newest first (descending byte
835
- * order), within the byte budget. Planted names that sort after the report
836
- * names are read first, so they spend the budget before the real report is
837
- * reached once they and the other entries add up to 32 MiB of charges; past
838
- * the entry bound the scan does not start. Either way it denies, never
839
- * allows.
840
- */
841
- function scanReportHashes(dir, wanted) {
842
- const scan = {
843
- files: 0,
844
- matched: new Set(),
845
- budgetExhausted: false,
846
- truncated: false,
847
- };
848
- const listed = listDirNamesBounded(dir, ".json", MAX_HOOK_LISTING_ENTRIES);
849
- if (listed.truncated) {
850
- scan.truncated = true;
851
- return scan;
852
- }
853
- // Ascending byte order from the listing; the scan takes the newest name first.
854
- const names = listed.names.reverse();
855
- let hashedBytes = 0; // budget charged so far, see the charge below
856
- for (const name of names) {
857
- scan.files += 1;
858
- if (hashedBytes >= MAX_HASH_SCAN_BYTES) {
859
- scan.budgetExhausted = true;
860
- break;
861
- }
862
- const read = readReportFileBounded(path.join(dir, name));
863
- hashedBytes += Math.max(read.ok ? Buffer.byteLength(read.raw) : 0, MIN_SCAN_ENTRY_COST_BYTES);
864
- if (!read.ok)
865
- continue;
866
- const hashed = hashReportText(read.raw);
867
- if (hashed.ok && wanted.has(hashed.hash)) {
868
- scan.matched.add(hashed.hash);
869
- if (scan.matched.size === wanted.size)
870
- break;
871
- }
872
- }
873
- return scan;
874
- }
875
- /**
876
- * Gate-read cross-check of a matched approval marker against the persisted
877
- * reports (task fa423e9b). Both PreToolUse hooks call it after
878
- * `checkOperatorApprovalMarkers` matched, through `verifyMatchedMarkerReport`.
879
- *
880
- * Rule: a marker with a non-null signed `reportContentHash` is valid iff at
881
- * least one parseable report file in `reportsDir` (any session, any
882
- * `approvalStatus`) has a canonical hash equal to it. The hash already
883
- * leaves the lifecycle fields out, so approving, expiring or re-approving
884
- * the report never changes it. The signed hash therefore proves one thing:
885
- * the content the operator approved is still on disk unmodified. Editing
886
- * that file (in place, by swap, by symlink, by an unparseable rewrite)
887
- * removes the only match and the marker stops opening the gate. Comparing
888
- * against the session's newest approved report instead would deny
889
- * legitimate approvals (a newer report approved for the same claim, two
890
- * sessions on one claim) and would let a swap pass by moving the swapped
891
- * report out of the selection.
892
- *
893
- * Bounded input: only a regular file of at most `MAX_HASHED_REPORT_BYTES`
894
- * (1 MiB) that parses as a JSON object nested at most 64 levels deep has a
895
- * canonical hash, read through {@link readReportFileBounded} (opened
896
- * non-blocking, typed and sized by `fstat` on the open descriptor, read
897
- * through it). Every other `*.json` entry (a FIFO, a directory, a device, a
898
- * symlink to one of them, a file over the cap, an unreadable, unparseable or
899
- * too deeply nested file) still counts as a report file but matches nothing,
900
- * so it can neither open the gate nor make the scan block, run out of
901
- * memory or die instead of deciding.
902
- *
903
- * Residuals: (1) the check proves only that some file carries the approved
904
- * content. Any regular `*.json` file within the size cap whose content
905
- * equals it keeps a match, whether it was kept before the edit (a copy) or
906
- * re-created after it (the agent wrote that content, so it can write it
907
- * again), and the marker still opens the gate. (2) A reports directory that
908
- * is empty, unreadable or holds no `*.json` entry at all (every report
909
- * deleted, the directory made unreadable, the reports renamed) falls back to
910
- * the "no report file" allow, the same as before this check existed. The
911
- * marker and the operator's approval itself are untouched in both cases;
912
- * only the audit trail loses its claim to be the approved content. (3) The
913
- * cap bounds each file, not their number, so the scan charges every `*.json`
914
- * entry at least `MIN_SCAN_ENTRY_COST_BYTES` against `MAX_HASH_SCAN_BYTES`
915
- * (bytes read or the floor, whichever is larger, whatever the read returned):
916
- * past the budget the check denies with the mismatch reason (fail closed).
917
- * That bounds the entries opened (8192) and the bytes read (about 32 MiB, the
918
- * last file read may add up to the 1 MiB per-file cap). The directory listing
919
- * is bounded as well ({@link listDirNamesBounded}, one entry at a time): past
920
- * {@link MAX_HOOK_LISTING_ENTRIES} `*.json` entries, or twice that many entries
921
- * of any name, the check opens nothing and denies with a reason that names the
922
- * bound and the remedy (fail closed), so the listing cost is bounded by that
923
- * count however many names are planted. Two costs remain. A legitimate
924
- * approval whose report sits behind that much newer report data (by name,
925
- * descending) is denied; re-approving writes a new, newest report and recovers
926
- * only when the newer entries are real reports: planted `*.json` names that
927
- * sort after the timestamped report names (for example names starting with a
928
- * letter) stay newer than any new report and keep denying until removed (the
929
- * same holds past the entry bound, where the scan does not start at all).
930
- * A reports directory with more entries than the bound is denied even when the
931
- * approved report is among them, until entries are removed by hand. The hash
932
- * scan is not the whole hook: the other full-directory reads on it (the
933
- * evidence read, the auto-approval listing, the delegation lookup and the
934
- * parse-error lookup) list the directory entry by entry and stop once
935
- * {@link MAX_HOOK_LISTING_ENTRIES} entries are crossed, and charge the entries
936
- * they read against the same byte budget; past either bound they open or read
937
- * nothing more and fail closed.
938
- *
939
- * Removing the approved report while other report files remain is a deny:
940
- * nothing on disk carries the signed content any more. `harness gc --apply`
941
- * can do that legitimately: it ages approved reports out by their createdAt
942
- * but markers by their file mtime, so a report written long before its
943
- * approval can go while the marker stays. Re-approving recovers.
944
- *
945
- * `primary` is the marker that satisfied the gate (task-scoped first),
946
- * `fallback` a session marker that matched behind a task marker. The
947
- * fallback is consulted only when the primary fails the check, so a task
948
- * marker signed for another report does not hide a session marker that
949
- * verifies. Unchanged behaviour: a null hash on the accepted marker (no
950
- * report resolved, or `--force` over a report nested too deeply to hash,
951
- * never one over the size cap) and a reports directory with no
952
- * `*.json` entry at all both allow. A forced approval of a hashable report
953
- * signs its hash and is checked like any other.
954
- *
955
- * The `detail` is the block reason. It names the failing marker kind and the
956
- * one-command fix, which is also the single migration step for a marker
957
- * written before the canonical hash existed. It carries no report-derived
958
- * value, so nothing in it needs sanitising.
959
- */
960
- export function verifyApprovedReportHash(reportsDir, primary, fallback = null) {
961
- if (primary.reportContentHash === null)
962
- return { ok: true, kind: primary.kind };
963
- if (fallback !== null && fallback.reportContentHash === null) {
964
- return { ok: true, kind: fallback.kind };
965
- }
966
- const wanted = new Set([primary.reportContentHash]);
967
- if (fallback !== null && fallback.reportContentHash !== null) {
968
- wanted.add(fallback.reportContentHash);
969
- }
970
- const scan = scanReportHashes(reportsDir, wanted);
971
- if (scan.truncated) {
972
- // Checked before the "no report file" allow below: a directory too large
973
- // to list holds report files the gate did not read, so it is not empty.
974
- const truncatedKinds = fallback !== null
975
- ? `${primary.kind} and ${fallback.kind} approval markers were`
976
- : `${primary.kind} approval marker was`;
977
- return {
978
- ok: false,
979
- detail: `no report in the reports directory could be checked against the content the ${truncatedKinds} signed for ` +
980
- `(the reports directory ${reportsDir} ${ENTRIES_TRUNCATED_DETAIL})`,
981
- };
982
- }
983
- if (scan.files === 0 || scan.matched.has(primary.reportContentHash)) {
984
- return { ok: true, kind: primary.kind };
985
- }
986
- if (fallback !== null &&
987
- fallback.reportContentHash !== null &&
988
- scan.matched.has(fallback.reportContentHash)) {
989
- return { ok: true, kind: fallback.kind };
990
- }
991
- const kinds = fallback !== null
992
- ? `${primary.kind} and ${fallback.kind} approval markers were`
993
- : `${primary.kind} approval marker was`;
994
- return {
995
- ok: false,
996
- detail: `no report in the reports directory matches the content the ${kinds} signed for ` +
997
- `(the approved report was changed or removed after approval); re-run \`harness approve understanding\`` +
998
- (scan.budgetExhausted
999
- ? " (the reports directory holds more report data than the gate-read scan budget covers)"
1000
- : ""),
1001
- };
1002
- }
1003
- /**
1004
- * {@link listPersistedReports} for every read a PreToolUse hook (or the
1005
- * PostToolUse boundary) makes of the reports directory. The directory holds
1006
- * files the gated agent can write, so every entry is opened once through
1007
- * {@link readReportFileBounded}: non-blocking, typed and sized by `fstat` on
1008
- * the open descriptor, read through it. A `*.json` entry that is not a
1009
- * regular file (a FIFO, a directory, a device, a symlink to one of them), is
1010
- * over `MAX_HASHED_REPORT_BYTES`, cannot be opened or read, or grows while it
1011
- * is read is left out of `reports`, as if it were not there, and recorded in
1012
- * `skipped`: it can neither block the hook nor run it out of memory. There is
1013
- * no separate stat of the path (a symlink flipped to a FIFO between that stat
1014
- * and the read used to hang the read), and the modification time comes from
1015
- * the same descriptor.
1016
- *
1017
- * A caller that must not let the selection fall back to an older report when
1018
- * a newer one is unreadable (the auto-approval precondition) declines when
1019
- * `skipped` is not empty. The operator commands read through this listing as
1020
- * well, so a planted oversized or non-regular entry cannot crash or hang
1021
- * them, and they still see it: `harness approve understanding` refuses
1022
- * while `skipped` is not empty (it passes `refuseSymlinks`, which also
1023
- * refuses a symbolic link at the open, whatever it points at), and
1024
- * `harness gc` reports each skipped entry and leaves it in place.
1025
- *
1026
- * Placement: this block sits at the end of the file, and
1027
- * {@link readPersistedReport} takes the already-read text (`boundedRaw`), so
1028
- * the line numbers the decision record
1029
- * docs/decisions/2026-08-27-ug-auto-mode-approval.md cites in this file stay
1030
- * put (tests/decisions-citations-resolve.test.ts pins them).
1031
- */
1032
- export function listPersistedReportsBoundedWithSkips(dir, opts = {}) {
1033
- // `maxEntries` is set by the PreToolUse-path readers (see
1034
- // `MAX_HOOK_LISTING_ENTRIES`); the operator commands leave it unset. Setting
1035
- // it applies both bounds of one hook read: the entry count, and the byte
1036
- // budget every entry read is charged against (see `MAX_HASH_SCAN_BYTES`).
1037
- const bounded = opts.maxEntries !== undefined;
1038
- const listed = listDirNamesBounded(dir, ".json", opts.maxEntries ?? Number.POSITIVE_INFINITY);
1039
- if (listed.truncated) {
1040
- return { reports: [], skipped: [], truncated: true, truncatedDetail: ENTRIES_TRUNCATED_DETAIL };
1041
- }
1042
- const budget = bounded ? { spent: 0 } : undefined;
1043
- const reports = [];
1044
- const skipped = [];
1045
- for (const name of listed.names) {
1046
- const full = path.join(dir, name);
1047
- // `refuseSymlinks` opens with O_NOFOLLOW, so a link is recorded as skipped
1048
- // by the open itself and no earlier lstat can race a swap.
1049
- const read = readReportFileBounded(full, { noFollow: opts.refuseSymlinks === true, budget });
1050
- // All or nothing: a listing that ran out of budget cannot say which
1051
- // report is the newest, so it returns no report at all.
1052
- if (!read.ok && read.reason === "over-budget") {
1053
- return { reports: [], skipped: [], truncated: true, truncatedDetail: BYTES_TRUNCATED_DETAIL };
1054
- }
1055
- if (!read.ok) {
1056
- skipped.push({ filePath: full, reason: read.reason, detail: read.detail });
1057
- continue;
1058
- }
1059
- const report = readPersistedReport(full, read.mtimeMs, read.raw);
1060
- if (report !== null)
1061
- reports.push(report);
1062
- }
1063
- reports.sort((a, b) => b.createdAtMs - a.createdAtMs);
1064
- return { reports, skipped, truncated: false, truncatedDetail: "" };
1065
- }
1066
- /**
1067
- * {@link listPersistedReportsBoundedWithSkips} without the skipped entries,
1068
- * for the callers that treat a skipped entry as no evidence
1069
- * ({@link checkPersistedReport}, {@link expirePersistedReport}). Expiry acts
1070
- * on the newest report that was within the cap, so an oversized newest
1071
- * approved report is left as it is and the next in-cap one is expired.
1072
- */
1073
- export function listPersistedReportsBounded(dir) {
1074
- return listPersistedReportsBoundedWithSkips(dir).reports;
1075
- }
1076
- //# sourceMappingURL=persisted-reports.js.map