failproofai 0.0.14-beta.2 → 0.0.15-beta.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 (269) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +2 -2
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/invite/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/status/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  47. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  49. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/index.html +1 -1
  52. package/.next/standalone/.next/server/app/index.rsc +15 -15
  53. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  54. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  55. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  56. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  57. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  59. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/page.js +2 -2
  61. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  64. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  65. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  66. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  69. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  70. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -4
  71. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  74. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  75. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  76. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  77. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  80. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  81. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  82. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0hv08mx._.js → [root-of-the-server]__0-dh5jy._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0obaznk._.js → [root-of-the-server]__0h3lfmp._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xvsnvt._.js → [root-of-the-server]__0l4vuqm._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +4 -4
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +6 -4
  94. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1hkdkmz._.js → [root-of-the-server]__10oa-v9._.js} +1 -1
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1i9rknc._.js → [root-of-the-server]__18d-9oi._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1is3glr._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +6 -6
  102. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0hdsupo._.js → [root-of-the-server]__0114ewg._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01od0m6._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s89hoe._.js → [root-of-the-server]__02r5bgf._.js} +3 -3
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09v9cwb._.js → [root-of-the-server]__04o-kw7._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qeuy9c._.js → [root-of-the-server]__0e8sjqm._.js} +3 -3
  110. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tkjqka._.js → [root-of-the-server]__0lkzqax._.js} +3 -3
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lpvwqm._.js +21 -0
  112. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ynf7tx._.js → [root-of-the-server]__0qucxyj._.js} +3 -3
  113. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__03lv-pe._.js → [root-of-the-server]__0u02miy._.js} +3 -3
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +8 -8
  116. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00cz9as._.js → [root-of-the-server]__1apb7k_._.js} +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +4 -0
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1i21qhv._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1py795m._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__015_i4t._.js → [root-of-the-server]__1zqz4v8._.js} +3 -3
  123. package/.next/standalone/.next/server/chunks/ssr/_077y5np._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/app_audit_loading_tsx_1j1kc6j._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_06xsmte.js → node_modules_html-to-image_es_index_0w82veb.js} +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  137. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  138. package/.next/standalone/.next/server/pages/404.html +1 -1
  139. package/.next/standalone/.next/server/pages/500.html +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  142. package/.next/standalone/.next/static/chunks/0hn30_j22wpp_.js +6 -0
  143. package/.next/standalone/.next/static/chunks/{2llkdafnaejw_.js → 1_20f96f1253b.js} +1 -1
  144. package/.next/standalone/.next/static/chunks/1hdhteui932bq.js +1 -0
  145. package/.next/standalone/.next/static/chunks/{33u59vf_8xpd-.js → 1j2xm011_aoae.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/{388hgfqye67xa.js → 1qzsbft-jtnf8.js} +1 -1
  147. package/.next/standalone/.next/static/chunks/{3-ogr79xqbw0h.js → 2mazwb1qmur1p.js} +1 -1
  148. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +2 -0
  149. package/.next/standalone/.next/static/chunks/2xbkqlma_-61k.js +1 -0
  150. package/.next/standalone/.next/static/chunks/{29zkom4s9de2d.js → 2y7acsw0--8xf.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/3060a-0fayz96.js +1 -0
  152. package/.next/standalone/.next/static/chunks/312ihvhxeuoi_.js +1 -0
  153. package/.next/standalone/.next/static/chunks/31c084gsop1bb.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{1ua5armwfph8o.js → 31fmsp1soxi07.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{02jcotghcxrbu.js → 3kmwfieo15mr2.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{1gg-8e9q_m__9.js → 3q9y67-fobgpc.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{turbopack-10hj8t5pf33tu.js → turbopack-1lsv92x7klwrn.js} +1 -1
  158. package/.next/standalone/app/actions/get-hooks-config.ts +85 -0
  159. package/.next/standalone/app/components/log-viewer/entry-row.tsx +8 -3
  160. package/.next/standalone/app/components/project-list.tsx +229 -42
  161. package/.next/standalone/app/components/raw-log-viewer.tsx +33 -15
  162. package/.next/standalone/app/policies/hooks-client.tsx +113 -15
  163. package/.next/standalone/app/project/[name]/page.tsx +12 -2
  164. package/.next/standalone/integration-suite/README.md +43 -1
  165. package/.next/standalone/integration-suite/ci-entrypoint.sh +21 -6
  166. package/.next/standalone/integration-suite/install-clis.sh +58 -13
  167. package/.next/standalone/integration-suite/probe-cli.sh +51 -11
  168. package/.next/standalone/integration-suite/report.js +52 -6
  169. package/.next/standalone/integration-suite/run.sh +55 -3
  170. package/.next/standalone/lib/entry-keys.ts +36 -0
  171. package/.next/standalone/lib/hermes-profiles.ts +156 -0
  172. package/.next/standalone/lib/hermes-projects.ts +91 -25
  173. package/.next/standalone/lib/hermes-sessions.ts +65 -10
  174. package/.next/standalone/lib/openclaw-projects.ts +111 -18
  175. package/.next/standalone/lib/openclaw-sessions.ts +20 -0
  176. package/.next/standalone/lib/paths.ts +36 -0
  177. package/.next/standalone/lib/project-tree.ts +193 -0
  178. package/.next/standalone/lib/projects.ts +22 -0
  179. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  180. package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +3 -114
  181. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  182. package/.next/standalone/node_modules/next/dist/cli/next-test.js +2 -1
  183. package/.next/standalone/node_modules/next/dist/compiled/http-proxy/index.js +5 -5
  184. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +11 -10
  185. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -10
  186. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
  187. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +9 -9
  188. package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +1 -1
  189. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  190. package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
  191. package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +1 -1
  192. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
  193. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
  194. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +97 -56
  195. package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +129 -34
  196. package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +52 -4
  197. package/.next/standalone/node_modules/next/dist/server/config-schema.js +1 -1
  198. package/.next/standalone/node_modules/next/dist/server/config-shared.js +1 -2
  199. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  200. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  201. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  202. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +8 -27
  203. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  204. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +90 -53
  205. package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +15 -2
  206. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +4 -0
  207. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  208. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +1 -1
  209. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  210. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +2 -1
  211. package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +12 -0
  212. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  213. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  214. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  215. package/.next/standalone/node_modules/next/package.json +10 -10
  216. package/.next/standalone/package.json +9 -7
  217. package/.next/standalone/pi-extension/index.ts +38 -5
  218. package/.next/standalone/server.js +1 -1
  219. package/dist/cli.mjs +623 -308
  220. package/lib/entry-keys.ts +36 -0
  221. package/lib/hermes-profiles.ts +156 -0
  222. package/lib/hermes-projects.ts +91 -25
  223. package/lib/hermes-sessions.ts +65 -10
  224. package/lib/openclaw-projects.ts +111 -18
  225. package/lib/openclaw-sessions.ts +20 -0
  226. package/lib/paths.ts +36 -0
  227. package/lib/project-tree.ts +193 -0
  228. package/lib/projects.ts +22 -0
  229. package/package.json +9 -7
  230. package/pi-extension/index.ts +38 -5
  231. package/scripts/launch.ts +7 -0
  232. package/src/audit/cli-adapters/hermes.ts +6 -3
  233. package/src/audit/cli-adapters/openclaw.ts +17 -13
  234. package/src/hooks/configure-wizard.ts +24 -2
  235. package/src/hooks/custom-hooks-loader.ts +58 -4
  236. package/src/hooks/enforcement-capability.ts +266 -0
  237. package/src/hooks/handler.ts +12 -1
  238. package/src/hooks/hook-activity-store.ts +14 -0
  239. package/src/hooks/hooks-config.ts +59 -1
  240. package/src/hooks/integrations.ts +123 -19
  241. package/src/hooks/manager.ts +93 -27
  242. package/src/hooks/policy-evaluator.ts +59 -7
  243. package/src/hooks/policy-types.ts +26 -0
  244. package/src/hooks/types.ts +50 -2
  245. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0twi4fj._.js +0 -3
  253. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +0 -4
  254. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +0 -3
  255. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fax1sl._.js +0 -19
  256. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +0 -3
  260. package/.next/standalone/.next/static/chunks/0h-6ymo9v86b0.js +0 -6
  261. package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +0 -1
  262. package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +0 -1
  263. package/.next/standalone/.next/static/chunks/1bg_g3g359c_d.js +0 -1
  264. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +0 -2
  265. package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +0 -1
  266. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +0 -1
  267. /package/.next/standalone/.next/static/{YKxI22hC-F_nHp7MEKfb1 → QFmU1xD4R9N_HVfwccP6A}/_buildManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{YKxI22hC-F_nHp7MEKfb1 → QFmU1xD4R9N_HVfwccP6A}/_clientMiddlewareManifest.js +0 -0
  269. /package/.next/standalone/.next/static/{YKxI22hC-F_nHp7MEKfb1 → QFmU1xD4R9N_HVfwccP6A}/_ssgManifest.js +0 -0
@@ -7,13 +7,13 @@
7
7
  * shared LogEntry[] shape, so `logEntriesToEvents` handles the rest.
8
8
  *
9
9
  * Gateway sessions run in the container workspace, not a host repo, so they
10
- * group by agentId (`openclaw:<agentId>`) and contribute nothing to a
11
- * cwd-scoped audit.
10
+ * group by (agentId, channel) — `openclaw:<agentId>:<channel>` — and contribute
11
+ * nothing to a cwd-scoped audit. That is the SAME key the dashboard groups by;
12
+ * the two used to disagree (adapter by agent, panel by channel), so a report and
13
+ * the projects panel sliced identical data on different axes.
12
14
  */
13
- import {
14
- listOpenClawTranscripts,
15
- getOpenClawSessionLog,
16
- } from "../../../lib/openclaw-sessions";
15
+ import { getOpenClawSessions, openClawProjectPath } from "../../../lib/openclaw-projects";
16
+ import { getOpenClawSessionLog } from "../../../lib/openclaw-sessions";
17
17
  import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
18
18
  import type { ListOpts } from "./claude";
19
19
  import { logEntriesToEvents } from "./shared";
@@ -28,15 +28,19 @@ export async function listOpenClawTranscriptMetadata(
28
28
 
29
29
  const sinceMs = opts.sinceMs ?? 0;
30
30
  const out: TranscriptMetadata[] = [];
31
- for (const t of listOpenClawTranscripts()) {
32
- if (t.mtimeMs < sinceMs) continue;
31
+ // getOpenClawSessions layers the per-agent sessions.json index over the same
32
+ // transcript walk, which is what carries `channel`. It reads one small index
33
+ // file per AGENT (not per session) and is memoized, so grouping by channel
34
+ // costs no meaningful extra IO on the audit path.
35
+ for (const s of await getOpenClawSessions()) {
36
+ if (s.mtimeMs < sinceMs) continue;
33
37
  out.push({
34
38
  cli: "openclaw",
35
- projectName: `openclaw:${t.agentId}`,
36
- sessionId: t.sessionId,
37
- transcriptPath: t.transcriptPath,
38
- mtimeMs: t.mtimeMs,
39
- sizeBytes: t.sizeBytes,
39
+ projectName: openClawProjectPath(s.agentId, s.channel),
40
+ sessionId: s.sessionId,
41
+ transcriptPath: s.transcriptPath,
42
+ mtimeMs: s.mtimeMs,
43
+ sizeBytes: s.sizeBytes,
40
44
  });
41
45
  }
42
46
  return out;
@@ -29,6 +29,8 @@ import {
29
29
  import {
30
30
  detectInstalledClis,
31
31
  getIntegration,
32
+ settingsPathsFor,
33
+ unhookedHermesProfiles,
32
34
  } from "./integrations";
33
35
  import { INTEGRATION_TYPES, type IntegrationType, type HookScope } from "./types";
34
36
  import { installHooks } from "./manager";
@@ -126,12 +128,28 @@ export function buildAgentChoices(scope: HookScope, cwd: string): MultiChoice<In
126
128
  } catch {
127
129
  installedHere = false;
128
130
  }
131
+ // Hermes reports installed only when EVERY profile is hooked, so a profile
132
+ // added after install flips it to false. Say which ones, otherwise a mostly
133
+ // configured gateway just reads as "not configured".
134
+ let partialHint: string | undefined;
135
+ if (id === "hermes" && !installedHere) {
136
+ try {
137
+ const unhooked = unhookedHermesProfiles();
138
+ if (unhooked.length > 0) {
139
+ partialHint = `${unhooked.length} unhooked profile(s): ${unhooked.join(", ")}`;
140
+ }
141
+ } catch {
142
+ // Profile discovery is best-effort — never block the wizard.
143
+ }
144
+ }
129
145
  return {
130
146
  label: integration.displayName,
131
147
  value: id,
132
148
  checked: isDetected || installedHere,
133
149
  section: isDetected ? "Detected" : "Not installed · set up ahead of time",
134
- hint: installedHere ? "already configured" : isDetected ? undefined : "not on PATH",
150
+ hint: installedHere
151
+ ? "already configured"
152
+ : (partialHint ?? (isDetected ? undefined : "not on PATH")),
135
153
  };
136
154
  });
137
155
  }
@@ -322,7 +340,11 @@ export function reviewLines(state: {
322
340
  lines.push(" This will update:");
323
341
  for (const cli of clis) {
324
342
  const integration = getIntegration(cli);
325
- lines.push(` ${homeify(integration.getSettingsPath(scope, cwd))} ${integration.displayName} hooks`);
343
+ // Usually one path; Hermes lists one per profile so the operator sees every
344
+ // home dir that is about to be written.
345
+ for (const p of settingsPathsFor(integration, scope, cwd)) {
346
+ lines.push(` ${homeify(p)} ${integration.displayName} hooks`);
347
+ }
326
348
  }
327
349
  lines.push(` ${homeify(getConfigPathForScope(scope, cwd))} ${policies.length} policies`);
328
350
  return lines;
@@ -27,6 +27,14 @@ const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
27
27
  /** Regex matching convention policy filenames: *policies.{js,mjs,ts} */
28
28
  const CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
29
29
 
30
+ /**
31
+ * Monotonic counter used to cache-bust each dynamic import. Loading N files
32
+ * creates N module instances rather than reusing one; every caller here is
33
+ * either a one-shot CLI command or a short-lived hook process, so the
34
+ * lifetime cost is bounded by the process, not by uptime.
35
+ */
36
+ let loadSequence = 0;
37
+
30
38
  /** Script extensions we could load, used to spot near-miss filenames. */
31
39
  const LOADABLE_EXT_RE = /\.(js|mjs|ts)$/;
32
40
 
@@ -96,7 +104,19 @@ async function loadSingleFile(
96
104
  tmpFiles = await rewriteFileTree(absPath, distUrl, distIndex);
97
105
 
98
106
  const entryTmp = absPath + TMP_SUFFIX;
99
- const fileUrl = pathToFileURL(entryTmp).href;
107
+ // Cache-bust the specifier. NOTE this works under Node but NOT under Bun,
108
+ // which caches by resolved path and ignores the query — and the shipped
109
+ // binary runs under Bun. It is kept because it makes repeat loads behave
110
+ // the same on both runtimes where it can, but nothing may DEPEND on a
111
+ // repeat load re-executing; callers dedupe paths instead (see above).
112
+ // The temp path is deterministic, so a second
113
+ // load of the same file inside one process hit the ESM module cache, the
114
+ // module body never re-ran, no `customPolicies.add` fired, and the caller
115
+ // saw zero hooks — indistinguishable from a broken policy file. That is
116
+ // exactly what `failproofai policies` rendered as "failed to load" for
117
+ // every file when it walked a shared project/user directory twice.
118
+ // A distinct query string makes it a distinct module to the loader.
119
+ const fileUrl = `${pathToFileURL(entryTmp).href}?v=${++loadSequence}`;
100
120
  await import(/* webpackIgnore: true */ fileUrl);
101
121
  } catch (err) {
102
122
  const msg = err instanceof Error ? err.message : String(err);
@@ -201,12 +221,20 @@ export async function loadAllCustomHooks(
201
221
  // purpose, so switching off *discovery* shouldn't silently drop it too.
202
222
  const conventionEnabled = opts?.customPoliciesEnabled !== false;
203
223
 
224
+ // Every file already imported this call, by resolved absolute path. Loading
225
+ // one twice is never right: `customPolicies.add` is an unconditional push, so
226
+ // the second import registers every hook a second time and the policy runs
227
+ // twice per event. Seeded by the explicit path below and consulted by both
228
+ // convention passes.
229
+ const loadedPaths = new Set<string>();
230
+
204
231
  // 1. Explicit customPoliciesPath (existing behavior)
205
232
  if (customPoliciesPath) {
206
233
  const absPath = isAbsolute(customPoliciesPath)
207
234
  ? customPoliciesPath
208
235
  : resolve(projectRoot, customPoliciesPath);
209
236
  if (existsSync(absPath)) {
237
+ loadedPaths.add(absPath);
210
238
  await loadSingleFile(absPath);
211
239
  } else {
212
240
  hookLogWarn(`customPoliciesPath not found: ${absPath}`);
@@ -216,10 +244,19 @@ export async function loadAllCustomHooks(
216
244
  const hooksBeforeConvention = getCustomHooks().length;
217
245
 
218
246
  // 2. Project convention: {projectRoot}/.failproofai/policies/*policies.{js,mjs,ts}
247
+ //
248
+ // A `customPoliciesPath` pointing INTO this directory at a file whose name
249
+ // also matches the convention (`*policies.{js,mjs,ts}`) is an ordinary setup —
250
+ // `failproofai policies -i -c .failproofai/policies/my-policies.mjs` produces
251
+ // exactly that — and it is discovered here as well. Skip what step 1 loaded,
252
+ // or the file is imported twice and every hook in it fires twice per event.
219
253
  const projectDir = resolve(projectRoot, ".failproofai", "policies");
220
254
  if (conventionEnabled) warnSkippedPolicyFiles(projectDir, "project");
221
- const projectFiles = conventionEnabled ? discoverPolicyFiles(projectDir) : [];
255
+ const projectFiles = conventionEnabled
256
+ ? discoverPolicyFiles(projectDir).filter((f) => !loadedPaths.has(f))
257
+ : [];
222
258
  for (const file of projectFiles) {
259
+ loadedPaths.add(file);
223
260
  const hooksBefore = getCustomHooks().length;
224
261
  await loadSingleFile(file, { conventionScope: "project" });
225
262
  const newHooks = getCustomHooks().slice(hooksBefore);
@@ -233,10 +270,27 @@ export async function loadAllCustomHooks(
233
270
  }
234
271
 
235
272
  // 3. User convention: ~/.failproofai/policies/*policies.{js,mjs,ts}
273
+ //
274
+ // Skipped entirely when it resolves to the directory already loaded above —
275
+ // which happens whenever the project root IS the home directory, a normal
276
+ // setup for a gateway. Without this, every file is loaded a second time and
277
+ // `customPolicies.add` (an unconditional push) registers each hook twice, so
278
+ // every policy fires twice per event. For a counting policy that means the
279
+ // count doubles and its ceiling trips at half the real number.
280
+ //
281
+ // Today that is masked by the runtime rather than prevented: Bun caches
282
+ // dynamic imports by resolved path and IGNORES the `?v=` cache-buster below,
283
+ // so the second import is a no-op — but Node honours the query and would
284
+ // double-register. The binary runs under Bun and the tests under Node, so the
285
+ // bug is invisible from both sides. Do not rely on that; dedupe the paths.
236
286
  const userDir = resolve(homedir(), ".failproofai", "policies");
237
- if (conventionEnabled) warnSkippedPolicyFiles(userDir, "user");
238
- const userFiles = conventionEnabled ? discoverPolicyFiles(userDir) : [];
287
+ if (conventionEnabled && userDir !== projectDir) warnSkippedPolicyFiles(userDir, "user");
288
+ const userFiles =
289
+ conventionEnabled && userDir !== projectDir
290
+ ? discoverPolicyFiles(userDir).filter((f) => !loadedPaths.has(f))
291
+ : [];
239
292
  for (const file of userFiles) {
293
+ loadedPaths.add(file);
240
294
  const hooksBefore = getCustomHooks().length;
241
295
  await loadSingleFile(file, { conventionScope: "user" });
242
296
  const newHooks = getCustomHooks().slice(hooksBefore);
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Does a policy DENY on this (cli, event) pair actually change the agent's
3
+ * behaviour, given the wire shape failproofai emits TODAY?
4
+ *
5
+ * This exists because the same claim used to live in prose, and prose drifted:
6
+ * CLAUDE.md documented Hermes `subagent_stop` as a working gate while upstream
7
+ * discarded its return, so every customer policy on that event enforced
8
+ * nothing, silently, for months. A table a test can assert against cannot rot
9
+ * that way.
10
+ *
11
+ * ABSENT MEANS UNKNOWN, NOT "block". Only rows traced to a consuming call site,
12
+ * an official contract, or a recorded live probe appear here. Callers must
13
+ * treat a missing entry as "say nothing" — a hedge rendered in the UI is still
14
+ * a claim, and an unverified claim is what this file exists to prevent.
15
+ */
16
+ // src/hooks/enforcement-capability.ts
17
+ //
18
+ // Does a policy DENY on this (cli, event) pair actually change the agent's
19
+ // behaviour, given the wire shape failproofai emits TODAY?
20
+ //
21
+ // "block" — the verdict is read at a call site that prevents the action
22
+ // or forces the agent to continue/retry.
23
+ // "observe" — the action proceeds regardless. Either the CLI discards the
24
+ // verdict, or the tool already ran, or the CLI *could* block but
25
+ // our shape is inert (each such line says so).
26
+ // ABSENT — NOT VERIFIED. The UI must stay silent, never assume "block".
27
+ //
28
+ // Only SOURCE / VENDOR_DOCS / LIVE_PROBE rows appear here. Every line carries
29
+ // its evidence. Versions probed: claude 2.1.220 · codex fe01054a · copilot
30
+ // 1.0.71 (some sites re-read in 1.0.68) · cursor-agent 2026.07.16-899851b ·
31
+ // opencode 7565e035 (v1.18.9, re-checked at v1.14.33) · pi 0.80.10 (source
32
+ // 0.82.1) · hermes-agent 5771a6e · openclaw f8ed8ecf (v2026.7.2) · droid
33
+ // 0.175.1 · devin 3000.2.17 · agy 1.1.8 · goose 1.43.0.
34
+ import type { HookEventType, IntegrationType } from "./types";
35
+
36
+ export type EnforcementCapability = "block" | "observe";
37
+
38
+ export const ENFORCEMENT_CAPABILITY: Record<
39
+ IntegrationType,
40
+ Partial<Record<HookEventType, EnforcementCapability>>
41
+ > = {
42
+ // ── claude ────────────────────────────────────────────────────────────────
43
+ // Closed source, but the shipped launcher embeds its minified JS as plaintext.
44
+ // Offsets are BYTE offsets into the shipped claude 2.1.220 launcher bundle.
45
+ // The hook RUNNER is event-agnostic (exit 2 -> blockingError @259223339); all
46
+ // asymmetry lives in the ~29 call sites that read or ignore it.
47
+ claude: {
48
+ PreToolUse: "block", // @256826565 gan() -> behavior==="deny" short-circuits the permission pipeline; tool.call never reached (@256827907)
49
+ UserPromptSubmit: "block", // @262298150 blockingError -> {shouldQuery:false} early return; prompt never sent to the model
50
+ PermissionRequest: "block", // @255767400 runHooks reads permissionRequestResult -> buildDeny. NOTE exit 2 is IGNORED here (docs are wrong); we emit hookSpecificOutput.decision.behavior:"deny" which IS the honoured shape — do not "simplify" to exit 2
51
+ Stop: "block", // @254572503 blockingErrors -> stopHookActive + `continue` re-enters the loop. CAP: CLAUDE_CODE_STOP_HOOK_BLOCK_CAP (default 8); discarded on end-turn paths (@254511900)
52
+ SubagentStop: "block", // @259180818 VEe emits SubagentStop when agentId set -> same consumer @254572503. Same cap + same end-turn discard
53
+ PreCompact: "block", // MEe @259170280 blockedBy -> consumers @254317851 / @254328121 return early; compaction does not run
54
+ PostToolBatch: "block", // @254577316 -> yields hook_stopped_continuation and RETURNS from the query generator. Tools in the batch already ran
55
+ ConfigChange: "block", // slt @259171700 -> consumer @251465990 skips the reload. CAVEAT: source==="policy_settings" is force-unblocked (MDM immune)
56
+ UserPromptExpansion: "block", // producer @254678081, real consumer @254672418 `if("blocked" in C) return C.blocked`. Slash-command / MCP-prompt only
57
+ TaskCreated: "block", // TaskCreate.call @256302400: blockingError -> task deleted (yAo) + tool throws
58
+ TaskCompleted: "block", // @254518449 -> same honoured consumer @254572503. GATE: teammate mode only (oy() @249372028)
59
+ TeammateIdle: "block", // @254518714 -> same consumer @254572503. Same teammate-mode gate
60
+ Elicitation: "block", // oOt @259172011 -> $_o @253296308 -> MCP handler U_y @253294456 returns {action:"decline"} to the server
61
+ ElicitationResult: "block", // iOt @259172485 -> N_o @253296535 / Zbo @253568095 force {action:"decline"}. Vetoes the response, does NOT stop the agent
62
+ PostToolUse: "observe", // OVERTURNED from "block". We emit hookSpecificOutput.additionalContext (policy-evaluator.ts:520-534) -> man's additionalContexts branch @256141810, never blockingError; and the tool already ran either way
63
+ PostToolUseFailure: "observe", // OVERTURNED. han @256143680 yields only cancelled / blocking_error / additional_context; consumer @256840799 just appends messages after the failed tool result. No stop channel exists
64
+ SessionStart: "observe", // @254090768 blockingError -> Pur() = hook_non_blocking_error; session proceeds. Only additionalContext has effect
65
+ Setup: "observe", // @254092164 -> same Pur() downgrade
66
+ SubagentStart: "observe", // @254689000 -> same Pur() downgrade; the subagent launches anyway
67
+ SessionEnd: "observe", // TFt @259174994 reads only !succeeded -> stderr passthrough; `blocked` never read
68
+ StopFailure: "observe", // zpt @~259180670 discards the EM() return entirely. Docs concur: "output and exit code are ignored"
69
+ PermissionDenied: "observe", // @256829448 reads ONLY hookSpecificOutput.retry; also fires only for classifier==="auto-mode"
70
+ Notification: "observe", // h9 @259174090 — return value not assigned
71
+ InstructionsLoaded: "observe", // nPt @259173437 — return value not assigned
72
+ CwdChanged: "observe", // vtn @259173188 -> zop @259172968 reads only watchPaths/systemMessage
73
+ FileChanged: "observe", // Atn @259173298 -> same zop consumer
74
+ PostCompact: "observe", // xpt @259170833 builds a display message; `blocked` never read (contrast MEe/PreCompact directly above it)
75
+ WorktreeRemove: "observe", // Zor @259183361 only records whether some hook succeeded; a deny just logs
76
+ // WorktreeCreate DELIBERATELY OMITTED: ODt @259182432 is a path PROVIDER, not
77
+ // a gate — with no stdout it throws "hook succeeded but returned no worktree
78
+ // path", so an ALLOW aborts creation exactly like a deny. Degenerate, not a
79
+ // capability. Fix the bug (emit a path on allow) before classifying it.
80
+ },
81
+
82
+ // ── codex ─────────────────────────────────────────────────────────────────
83
+ // openai/codex @fe01054a. Every verdict funnels through codex-rs/core/src/hook_runtime.rs.
84
+ codex: {
85
+ PreToolUse: "block", // tools/registry.rs:528 PreToolUseHookResult::Blocked -> FunctionCallError::RespondToModel; handler never runs. CAVEAT hook_runtime.rs:202 an empty reason silently degrades to Continue
86
+ PermissionRequest: "block", // tools/approvals.rs:220 Deny -> ReviewDecision::denied. CAVEAT only consulted when the call reaches the approval path (orchestrator.rs:168 Skip bypasses it)
87
+ UserPromptSubmit: "block", // events/user_prompt_submit.rs:226 Some(2)+stderr -> should_stop; turn.rs:588 skips record_pending_input, turn.rs:237 returns. CAVEAT turn.rs:603 — a co-submitted accepted message lets the turn proceed
88
+ Stop: "block", // events/stop.rs:303 Some(2) -> should_block + continuation_prompt; turn.rs:473-489 records it and `continue`s. CAVEAT turn.rs:490 a block with no prompt is warned + ignored
89
+ SubagentStop: "block", // shared parse_completed (stop.rs:202) + same consumer turn.rs:473. CAVEAT hook_runtime.rs:345 — only ThreadSpawn subagents dispatch it; all other SubAgent sources never run the hook
90
+ PostToolUse: "observe", // codex CAN block here (registry.rs:678) but we emit hookSpecificOutput.additionalContext with no top-level `decision`, and output_parser.rs:215 needs decision==="block". Inert by our choice; tool already ran regardless
91
+ SessionStart: "observe", // events/session_start.rs:243-322 has NO Some(2) arm — our exit-2 deny is logged as HookRunStatus::Failed. CLI blocks only on exit 0 + {"continue":false} (session_start.rs:273 -> turn.rs:233)
92
+ SubagentStart: "observe", // permanently unblockable: session_start.rs:273 guard excludes it, output schema has no `decision`, vendor doc says continue:false does not stop it
93
+ PreCompact: "observe", // compact.rs:246-260 no Some(2) arm; latent block needs {"continue":false} (-> core/src/compact.rs:190 TurnAborted). Inert as we emit
94
+ PostCompact: "observe", // compact.rs:349-363 same structure; latent block needs {"continue":false} (-> compact.rs:216). Inert as we emit
95
+ },
96
+
97
+ // ── copilot ───────────────────────────────────────────────────────────────
98
+ // Closed source; vendor build ships readable JS. Offsets are BYTE offsets into
99
+ // the @github/copilot-linux-x64 app.js (c71/ = 1.0.71, copilot/ = 1.0.68).
100
+ // CRITICAL: exit 2 is NEVER a deny channel on copilot, for any event
101
+ // (runtime.node@60056587 "Hook command exited with code 2 (warning)").
102
+ copilot: {
103
+ PreToolUse: "block", // 1.0.68 app.js@2878277 permissionDecision==="deny" -> resultType:"denied"; 1.0.71 moved to Rust, runtime.node@60059067 "Denied by preToolUse hook". Corroborated LIVE on 1.0.71 (session events success:false code:denied) + daily integration-suite probe
104
+ Stop: "block", // 1.0.71 app.js@2855430 agentStop: decision==="block" && string reason -> enqueueUserMessage(reason). We emit exactly that shape
105
+ SubagentStop: "block", // 1.0.71 app.js@1074101 subagentStop: decision==="block" && reason -> `continue` re-runs the subagent turn. CAVEAT skipped entirely for isSidekick subagents
106
+ UserPromptSubmit: "block", // FIXED: we now emit {decision:"block",reason} at exit 0 (gate V$t @2547438, consumer @2823018). Was inert — we sent exit 2 + stderr, which copilot logs as a warning for EVERY event and never treats as a deny
107
+ PermissionRequest: "block", // FIXED: we now emit the FLAT {behavior,message} copilot parses (normalizer CMn @179042 -> mapper h4t @2686538). Was inert — the Codex-shaped nested hookSpecificOutput.decision normalized to {}
108
+ PostToolUse: "observe", // OVERTURNED. Gate vK @173853 needs top-level decision==="block"; we emit hookSpecificOutput.additionalContext. Post-hoc anyway (result-rewrite only). Vendor docs: "Can block? No"
109
+ SessionStart: "observe", // 1.0.71 app.js@2836633 reads ONLY additionalContext
110
+ SessionEnd: "observe", // 1.0.71 app.js@2627263 `.then(()=>{})` — return explicitly thrown away
111
+ PostToolUseFailure: "observe", // 1.0.68 app.js@2883424 and @3038111 destructure only additionalContext
112
+ PreCompact: "observe", // 1.0.71 app.js@2758186 and @2852753 — result unassigned at both call sites
113
+ Notification: "observe", // 1.0.71 app.js@2746820 reads only additionalContext
114
+ // ErrorOccurred is installed but has no canonical HookEventType, so it
115
+ // cannot be keyed here. It is observe: 1.0.71 app.js@2846297, result unassigned.
116
+ },
117
+
118
+ // ── cursor ────────────────────────────────────────────────────────────────
119
+ // Closed source; local install ships webpack-minified JS. Offsets are CHAR
120
+ // offsets into the shipped cursor-agent 2026.07.16-899851b bundle.
121
+ cursor: {
122
+ PreToolUse: "block", // 3143.index.js char 26967 permission==="deny" -> {type:"rejected"}; second MCP consumer char 42084
123
+ Stop: "block", // 1931.index.js char 881820 followup_message queued as the next user message. CAPS: loop_limit default 5 (index.js char 4123300); consumed ONLY on the status:"completed" path — user-abort (char 870298) and turn-error (char 883549) discard it
124
+ UserPromptSubmit: "block", // FIXED: we now emit {continue:false,user_message} (1931.index.js char 887883 — the only block key). Was inert: {permission:"deny"} validates as an unknown-key object and is dropped on this event
125
+ PostToolUse: "observe", // 3143.index.js char 26339 fireSuccessAsync reads only additional_context; validator accepts nothing else (index.js char 4126700)
126
+ SessionStart: "observe", // 1931.index.js char 731566 consumes only env + additional_context; `continue` never read even though Cursor's own q() builds it
127
+ SessionEnd: "observe", // 1931.index.js char 724540 — result assigned to nothing
128
+ // SubagentStop OMITTED: producer exists (index.js char 3769676 forwards
129
+ // followupMessage) and vendor docs describe loop-style follow-ups, but no
130
+ // code in the shipped install ever CONSTRUCTS a subagentStop request, so
131
+ // the consumer is outside the bundle. unknown.
132
+ },
133
+
134
+ // ── opencode ──────────────────────────────────────────────────────────────
135
+ // sst/opencode @7565e035 (v1.18.9); every finding re-checked at tag v1.14.33.
136
+ opencode: {
137
+ PreToolUse: "block", // session/tools.ts:106-110 trigger runs BEFORE item.execute (:111) inside run.promise; a throw is an Effect defect that kills the fiber. Upstream test test/tool/code-mode.test.ts:428 proves the tool body never executed
138
+ PostToolUse: "observe", // session/tools.ts:121-125 sits after item.execute; a throw replaces the output with a tool-error the model sees, but the write/command already happened
139
+ SessionStart: "observe", // plugin/index.ts:255 `void hook["event"]?.(…)` — fire-and-forget, return structurally unreadable
140
+ SessionEnd: "observe", // plugin/index.ts:255 — same void dispatch
141
+ UserPromptSubmit: "observe", // plugin/index.ts:255 same void dispatch; and message.updated is published from session.ts:631 AFTER the message is persisted. The real gate is chat.message, which we do not install (§4)
142
+ PermissionRequest: "observe", // ☠ DEAD HOOK: permission.ask is DECLARED (packages/plugin/src/index.ts:261) and documented upstream, but NEVER INVOKED — the only two dispatch sites are hook["event"] (:255) and Plugin.trigger (:288), and permission.ask is not among the 13 names passed to .trigger(). The policy does not even run. See §5.3 for the copy hazard
143
+ // Stop OMITTED: session.idle's plugin return is definitively discarded
144
+ // (plugin/index.ts:255), so we instead call client.session.prompt(...)
145
+ // out-of-band (integrations.ts:886-897). That SDK route does start a fresh
146
+ // turn, but the composite (alive at idle? one-shot `opencode run`?) is
147
+ // unverified end to end. unknown.
148
+ },
149
+
150
+ // ── pi ────────────────────────────────────────────────────────────────────
151
+ // @earendil-works/pi-coding-agent 0.80.10 installed (d.ts read directly);
152
+ // source cross-read at repo cced6a2 (0.82.1). Identical result shapes.
153
+ pi: {
154
+ PreToolUse: "block", // tool_call: ToolCallEventResult{block,reason} (types.d.ts:766) -> runner.emitToolCall returns on result.block -> agent-loop.ts:626 `if(beforeResult?.block) return createErrorToolResult(...)`; tool never executes. user_bash also maps here and is now FIXED too: the shim returns a full-replacement {result:BashResult} (types.d.ts:772) so the command is not run — {block,reason} had no matching field
155
+ UserPromptSubmit: "block", // FIXED: the shim now returns {action:"handled"} (InputEventResult, types.d.ts:629) so the prompt is never submitted. Was inert — {block,reason} matched no branch. CAVEAT: `handled` drops the prompt SILENTLY; Pi shows the user nothing, so the shim logs the reason to stderr
156
+ PostToolUse: "observe", // tool_result: ToolResultEventResult{content,details,isError,usage} (types.d.ts:778) — no block field; mutation only
157
+ SessionStart: "observe", // on(event:"session_start", ExtensionHandler<SessionStartEvent>) — NO Result type param (types.d.ts:842); runner.emit() only consumes returns for session_before_* (runner.ts:787-813)
158
+ SessionEnd: "observe", // session_shutdown: same — handler declared with no Result type (types.d.ts:848)
159
+ Stop: "observe", // agent_end: handler has NO Result type (types.d.ts:857) — the turn cannot be retried. Our shim stashes the reason and injects it into the NEXT turn via before_agent_start.systemPrompt (index.ts:458). Real, but it is a next-turn instruction, not a gate
160
+ },
161
+
162
+ // ── hermes ────────────────────────────────────────────────────────────────
163
+ // hermes-agent @5771a6e. agent/shell_hooks.py _parse_response is EVENT-GATED:
164
+ // it returns a verdict only for pre_tool_call and pre_verify.
165
+ hermes: {
166
+ PreToolUse: "block", // pre_tool_call — the only tool gate; the tool does not run
167
+ PostToolUse: "observe", // post_tool_call — not in _parse_response's gate
168
+ SubagentStop: "observe", // OVERTURNED from CLAUDE.md's "✅ block". Not in _parse_response's gate, AND the call site (tools/delegate_tool.py) discards the return. Customers with a SubagentStop deny had ZERO enforcement
169
+ SessionStart: "observe", // on_session_start — not gated in
170
+ SessionEnd: "observe", // on_session_end — call site discards the return
171
+ // Stop: NOT KEYED — `pre_verify` is a real turn-end gate upstream but we do
172
+ // not install it, so no canonical Stop event ever fires for hermes.
173
+ },
174
+
175
+ // ── openclaw ──────────────────────────────────────────────────────────────
176
+ // openclaw/openclaw @f8ed8ecf (v2026.7.2) + our own shipped shim
177
+ // openclaw-plugin/index.js, which returns undefined for 5 of the 8 hooks.
178
+ openclaw: {
179
+ PreToolUse: "block", // before_tool_call -> shim returns {block:true, blockReason}
180
+ UserPromptSubmit: "block", // before_agent_run -> shim returns {outcome:"block", reason}
181
+ Stop: "block", // before_agent_finalize -> shim returns {action:"revise", reason}; the turn re-runs
182
+ PostToolUse: "observe", // void upstream AND our shim returns undefined
183
+ SessionStart: "observe", // void upstream AND our shim returns undefined
184
+ SessionEnd: "observe", // void upstream AND our shim returns undefined
185
+ SubagentStop: "observe", // subagent_ended cannot veto upstream; our shim also returns undefined
186
+ PreCompact: "observe", // before_compaction — same
187
+ },
188
+
189
+ // ── factory (droid) ───────────────────────────────────────────────────────
190
+ // droid 0.175.1 (the shipped droid binary, ELF not stripped, embedded JS readable).
191
+ // Anchors below are unique grep-able strings in that bundle.
192
+ factory: {
193
+ PreToolUse: "block", // grep `throwing ToolExecutionControlError`: exit 2 -> throw $SH(stderr); also hookSpecificOutput.permissionDecision==="deny" -> throw $SH, continue===false -> throw LSH, exit 3 -> AgentAbortError. We emit exit 2 + stderr
194
+ UserPromptSubmit: "block", // executeUserPromptSubmitHooks: `if(B.exitCode===2||B.continue===!1){ … return {prompt,hookContext,blocked:!0} }` — the turn never starts
195
+ Stop: "block", // grep `[Agent] Stop hook blocking with decision: block`: {decision:"block",reason} sets the continue flag; grep `[Agent] Stop hook blocking with exit code 2` shows exit 2 is a FALLBACK too (CLAUDE.md says otherwise — harmless, we emit the JSON)
196
+ PreCompact: "block", // grep `[Compaction] PreCompact hook blocked compaction`: exit 2 or 3 -> returns the uncompacted conversation
197
+ PostToolUse: "observe", // exit 2 -> ADD_MESSAGE(role:"system", visibility:"llm_only") with the postToolUseFeedbackPrefix — feedback, not a block; tool already ran. (continue===false / exit 3 WOULD stop, but we emit neither)
198
+ SubagentStop: "observe", // `await MR("SubagentStop",{…})` — the awaited result is not assigned to anything. Hermes-class: our SubagentStop deny is discarded
199
+ SessionStart: "observe", // applySessionStartHookResults consumes only the env file + additionalContext; `if(L.exitCode!==0) TH("[Session] SessionStart hook failed")` just logs
200
+ SessionEnd: "observe", // executeSessionEndHooks: `for(let r of n) if(r.exitCode!==0) TH("[Session] SessionEnd hook failed")` — logs only
201
+ Notification: "observe", // `await MR("Notification",{…})` — return unassigned
202
+ },
203
+
204
+ // ── devin ─────────────────────────────────────────────────────────────────
205
+ // devin 3000.2.17 is a STRIPPED Rust ELF — no readable call sites. Tier is
206
+ // LIVE_PROBE: 11 recorded runs against the real binary with an isolated
207
+ // --config, each verified by side effect (marker file) AND by absence of the
208
+ // reason in ~/.local/share/devin/cli/{transcripts,sessions.db}.
209
+ devin: {
210
+ PreToolUse: "block", // probe L9: "Error: A tool was rejected by the user", RAN_MARKER absent — and it overrode --permission-mode dangerous
211
+ UserPromptSubmit: "block", // probe L10: "Prompt blocked: <reason>"; no PreToolUse/Stop hook fired; no model turn
212
+ Stop: "block", // probe L11 + transcript orchid-pillow.json /steps/8,/steps/10: the reason is injected as a USER message and the agent takes extra turns
213
+ PermissionRequest: "block", // probe L17: {"decision":"block"} -> marker absent, run aborted; L16: {"decision":"approve"} flipped a rejection into an execution, proving the channel is live. ⚠⚠ CONDITIONAL — see §5.1: never fires under --permission-mode dangerous (L8) and never for auto-approved read-only tools (L18, where a secret leaked)
214
+ PostToolUse: "observe", // probe L12: marker created, session finished normally, reason absent from transcript AND sessions.db
215
+ SessionStart: "observe", // probe L13: session ran normally, reason absent from transcript and DB
216
+ SessionEnd: "observe", // probe L14: session ended normally, reason absent
217
+ // Static corroboration (strings @82817071): "Ignoring pre_stop decision type:",
218
+ // "Ignoring permission decision type:", "Blocked by hook", "Denied by hook",
219
+ // "Prevented by hook" — agent-ext/src/hooks/event_handler.rs.
220
+ },
221
+
222
+ // ── antigravity (agy) ─────────────────────────────────────────────────────
223
+ // agy 1.1.8. Two independent kinds of evidence: live probes, plus the
224
+ // embedded FileDescriptorProto (binary @80315228, exa.hooks_pb) which is
225
+ // AUTHORITATIVE about which fields a response can even carry.
226
+ antigravity: {
227
+ PreToolUse: "block", // probe [1]: `touch PROBE_RAN` never ran; agent reported the tool "blocked with … PROBE: denied". Proto: PreToolHookResult{decision, reason, allow_tool, deny_reason}
228
+ Stop: "block", // probe [4]: {"decision":"continue",reason} re-entered the loop AND the agent obeyed the reason (STOP_GATE_PROOF created). Proto: StopHookResult{decision, reason}
229
+ PostToolUse: "observe", // probe [2]: agy logged `unknown field "decision"` (protojson unmarshal failure) 5x and continued. Proto: PostToolHookResult is an EMPTY MESSAGE — nothing is consumable
230
+ UserPromptSubmit: "observe", // PreInvocation. probe [3]: our {"decision":"deny"} caused `failed to call custom pre-invocation hook … unknown field "decision"`; 4 invocations ran unimpeded. Proto: PreInvocationHookResult carries ONLY inject_steps. (instruct works — probe [5] injectSteps honoured)
231
+ },
232
+
233
+ // ── goose ─────────────────────────────────────────────────────────────────
234
+ // block/goose, read AT TAG v1.43.0 (the installed version) as well as HEAD
235
+ // 0b234bdc. Two dispatchers: emit() is fire-and-forget (crates/goose/src/
236
+ // hooks/mod.rs:295 — the return only feeds a warn! log); emit_blocking()
237
+ // (:364) consumes exit 2 or {"decision":"block","reason"} via deny_reason(:429).
238
+ goose: {
239
+ PreToolUse: "block", // v1.43.0 agent.rs:1080 emit_blocking -> HookDecision::Deny -> returns Err(ErrorData "Tool call denied by policy hook `<plugin>`: …"); the tool is never dispatched
240
+ UserPromptSubmit: "observe", // v1.43.0 agent.rs:1609 and :1935 use emit() — verdict discarded
241
+ PostToolUse: "observe", // v1.43.0 agent.rs:~593 with_post_tool_hook uses emit() — verdict discarded, and it is post-hoc
242
+ SessionStart: "observe", // v1.43.0 agent.rs:1595 emit_hook -> emit() — verdict discarded
243
+ SessionEnd: "observe", // goose-cli/src/session/mod.rs:529 and :1294 emit_hook -> emit() — verdict discarded
244
+ // Goose HAS a blocking Stop event we do not install — see §4. CLAUDE.md and
245
+ // the policy-evaluator comment both claim "Goose has NO Stop event"; that is
246
+ // false at v1.43.0 (agent.rs:1956, :2840 emit_stop_hook_blocking).
247
+ },
248
+ };
249
+
250
+ /**
251
+ * Capability for a (cli, event) pair, or `undefined` when not verified.
252
+ *
253
+ * Callers MUST branch on `undefined` and render nothing. Defaulting it to
254
+ * "block" would assert protection nobody confirmed; defaulting to "observe"
255
+ * would tell a user a working policy is inert. Silence is the only honest
256
+ * answer for a pair we have not traced.
257
+ */
258
+ export function enforcementFor(
259
+ cli: IntegrationType | undefined,
260
+ event: HookEventType | string | undefined,
261
+ ): EnforcementCapability | undefined {
262
+ if (!cli || !event) return undefined;
263
+ const perCli = ENFORCEMENT_CAPABILITY[cli];
264
+ if (!perCli) return undefined;
265
+ return perCli[event as HookEventType];
266
+ }
@@ -29,7 +29,7 @@ import type { PolicyFunction, PolicyResult } from "./policy-types";
29
29
  import { readMergedHooksConfig } from "./hooks-config";
30
30
  import { registerBuiltinPolicies } from "./builtin-policies";
31
31
  import { evaluatePolicies } from "./policy-evaluator";
32
- import { clearPolicies, registerPolicy } from "./policy-registry";
32
+ import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
33
33
  import { loadAllCustomHooks } from "./custom-hooks-loader";
34
34
  import type { CustomHook } from "./policy-types";
35
35
  import { persistHookActivity } from "./hook-activity-store";
@@ -324,6 +324,16 @@ export async function handleHookEvent(
324
324
  process.stderr.write(result.stderr);
325
325
  }
326
326
 
327
+ // Which policies actually ran for this event, regardless of how they
328
+ // decided. `result.policyName` names only the decider — null on a plain
329
+ // allow — so without this a row cannot tell "your policy ran and allowed"
330
+ // from "no policy covers this event". The lookup is the same cached call
331
+ // the evaluator already made, so it costs nothing.
332
+ const matchedPolicies = getPoliciesForEvent(
333
+ canonicalEventType,
334
+ parsed.tool_name as string | undefined,
335
+ ).map((p) => p.name);
336
+
327
337
  // Persist activity to disk (visible in /policies activity tab)
328
338
  const activityEntry = {
329
339
  timestamp: Date.now(),
@@ -332,6 +342,7 @@ export async function handleHookEvent(
332
342
  toolName: (parsed.tool_name as string) ?? null,
333
343
  policyName: result.policyName,
334
344
  policyNames: result.policyNames,
345
+ matchedPolicies,
335
346
  decision: result.decision,
336
347
  reason: result.reason,
337
348
  durationMs,
@@ -47,6 +47,20 @@ export interface HookActivityEntry {
47
47
  toolName: string | null;
48
48
  policyName: string | null;
49
49
  policyNames?: string[];
50
+ /**
51
+ * Every policy that MATCHED this event and ran, whichever way it decided.
52
+ *
53
+ * `policyName` only ever names the policy that *decided* — on a plain
54
+ * `allow()` it is null, so a row could not distinguish "no policy applies
55
+ * here" from "your policy ran and allowed". That is the whole reason a user
56
+ * cannot tell whether a policy fired, and why an observation-only event looks
57
+ * identical to an uncovered one.
58
+ *
59
+ * Optional because rows written before this existed have no such field; every
60
+ * consumer must tolerate `undefined` rather than render an empty list as
61
+ * "nothing ran".
62
+ */
63
+ matchedPolicies?: string[];
50
64
  decision: "allow" | "deny" | "instruct";
51
65
  reason: string | null;
52
66
  durationMs: number;
@@ -4,7 +4,7 @@
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
5
5
  import { resolve, dirname } from "node:path";
6
6
  import { homedir } from "node:os";
7
- import type { HooksConfig } from "./policy-types";
7
+ import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
8
8
  import type { HookScope } from "./types";
9
9
  import { hookLogInfo, hookLogWarn } from "./hook-logger";
10
10
 
@@ -171,6 +171,64 @@ export function writeScopedHooksConfig(config: HooksConfig, scope: HookScope, cw
171
171
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
172
172
  }
173
173
 
174
+ /**
175
+ * Mirror the convention policies discovered at `scope` into that scope's
176
+ * config file, so `policies-config.json` shows what is installed rather than
177
+ * only what was explicitly enabled.
178
+ *
179
+ * Wholesale replace, not merge: the point is to reflect the directory as it is
180
+ * now, so a deleted file must vanish from the record. Writes only when the
181
+ * value actually changes, so repeated `failproofai policies` runs do not
182
+ * rewrite the file (and do not churn its mtime) for no reason.
183
+ *
184
+ * Callers must be one-shot commands. This is never safe to call from the hook
185
+ * path — see the `conventionPolicies` doc comment on `HooksConfig`.
186
+ *
187
+ * Returns true when the file was written.
188
+ */
189
+ export function syncConventionPolicies(
190
+ entries: ConventionPolicyRecord[],
191
+ scope: HookScope,
192
+ cwd?: string,
193
+ ): boolean {
194
+ const configPath = getConfigPathForScope(scope, cwd);
195
+
196
+ // Never bring a config file into existence just to record an empty list —
197
+ // that would litter every directory the CLI is run from.
198
+ if (!existsSync(configPath) && entries.length === 0) return false;
199
+
200
+ // NEVER write over a file we could not parse. `readScopedHooksConfig` fails
201
+ // soft, returning `{enabledPolicies: []}` on a syntax error — correct for the
202
+ // hook path, which must not die because a config is malformed, but fatal
203
+ // here: writing that default back silently DESTROYS the user's real settings.
204
+ // A hand-edited file with one stray comma would lose every enabled policy the
205
+ // moment someone ran `failproofai policies`, a read-only command. Bail and
206
+ // leave the file exactly as it is; the parse warning already surfaced.
207
+ if (existsSync(configPath)) {
208
+ try {
209
+ JSON.parse(readFileSync(configPath, "utf8"));
210
+ } catch {
211
+ hookLogWarn(
212
+ `not recording convention policies: ${configPath} is not valid JSON — ` +
213
+ `fix the syntax error and re-run. Nothing was written.`,
214
+ );
215
+ return false;
216
+ }
217
+ }
218
+
219
+ const config = readScopedHooksConfig(scope, cwd);
220
+ const sorted = [...entries].sort((a, b) => a.file.localeCompare(b.file));
221
+ const next: HooksConfig = { ...config };
222
+ if (sorted.length === 0) delete next.conventionPolicies;
223
+ else next.conventionPolicies = sorted;
224
+
225
+ if (JSON.stringify(next.conventionPolicies ?? null) === JSON.stringify(config.conventionPolicies ?? null)) {
226
+ return false;
227
+ }
228
+ writeScopedHooksConfig(next, scope, cwd);
229
+ return true;
230
+ }
231
+
174
232
  export interface ResolvedLlmConfig {
175
233
  baseUrl: string;
176
234
  apiKey: string;