failproofai 0.0.14 → 0.0.15-beta.1

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 (270) 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 +20 -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]__02r5bgf._.js +4 -0
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +4 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lpvwqm._.js +21 -0
  111. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ynf7tx._.js → [root-of-the-server]__0qucxyj._.js} +3 -3
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +4 -0
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1s2b6h0._.js → [root-of-the-server]__0x3anhr._.js} +2 -2
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0y4wf1u._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +10 -10
  117. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00cz9as._.js → [root-of-the-server]__1apb7k_._.js} +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +4 -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]__1zqz4v8._.js +4 -0
  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_components_project-list_tsx_1ke6anr._.js +3 -0
  129. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  131. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0gkzjaa.js → node_modules_html-to-image_es_index_1wnt2ya.js} +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  136. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  137. package/.next/standalone/.next/server/pages/404.html +1 -1
  138. package/.next/standalone/.next/server/pages/500.html +1 -1
  139. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.json +22 -10
  141. package/.next/standalone/.next/static/chunks/{29o4j32z643_a.js → 05h7pau8d3rpj.js} +1 -1
  142. package/.next/standalone/.next/static/chunks/0oos4ntyvkmjl.js +1 -0
  143. package/.next/standalone/.next/static/chunks/0tuwf_7uhfn2y.js +6 -0
  144. package/.next/standalone/.next/static/chunks/1hdhteui932bq.js +1 -0
  145. package/.next/standalone/.next/static/chunks/1l6q2-8m0gk19.js +1 -0
  146. package/.next/standalone/.next/static/chunks/2geqsiz0n8e3w.js +1 -0
  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/{1ua5armwfph8o.js → 31fmsp1soxi07.js} +1 -1
  150. package/.next/standalone/.next/static/chunks/{3s8aaig91937v.js → 37-so-zy208k7.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/3a_o5v_sl858j.js +1 -0
  152. package/.next/standalone/.next/static/chunks/{02jcotghcxrbu.js → 3kmwfieo15mr2.js} +1 -1
  153. package/.next/standalone/.next/static/chunks/{2uh_4k4m296z5.js → 3p66lo2y-ezfr.js} +1 -1
  154. package/.next/standalone/.next/static/chunks/{06z_ex1ts3n4s.js → 412ajx6jkb5_i.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{turbopack-10hj8t5pf33tu.js → turbopack-1lsv92x7klwrn.js} +1 -1
  156. package/.next/standalone/app/actions/get-hooks-config.ts +125 -8
  157. package/.next/standalone/app/actions/update-hooks-config.ts +38 -1
  158. package/.next/standalone/app/components/project-list.tsx +229 -42
  159. package/.next/standalone/app/policies/hooks-client.tsx +149 -27
  160. package/.next/standalone/app/project/[name]/page.tsx +12 -2
  161. package/.next/standalone/integration-suite/README.md +43 -1
  162. package/.next/standalone/integration-suite/ci-entrypoint.sh +21 -6
  163. package/.next/standalone/integration-suite/install-clis.sh +58 -13
  164. package/.next/standalone/integration-suite/probe-cli.sh +51 -11
  165. package/.next/standalone/integration-suite/report.js +52 -6
  166. package/.next/standalone/integration-suite/run.sh +55 -3
  167. package/.next/standalone/lib/hermes-profiles.ts +156 -0
  168. package/.next/standalone/lib/hermes-projects.ts +91 -25
  169. package/.next/standalone/lib/hermes-sessions.ts +65 -10
  170. package/.next/standalone/lib/openclaw-projects.ts +111 -18
  171. package/.next/standalone/lib/openclaw-sessions.ts +20 -0
  172. package/.next/standalone/lib/paths.ts +36 -0
  173. package/.next/standalone/lib/project-tree.ts +193 -0
  174. package/.next/standalone/lib/projects.ts +22 -0
  175. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  176. package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +3 -114
  177. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  178. package/.next/standalone/node_modules/next/dist/cli/next-test.js +2 -1
  179. package/.next/standalone/node_modules/next/dist/compiled/http-proxy/index.js +5 -5
  180. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +11 -10
  181. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -10
  182. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
  183. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +9 -9
  184. package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +1 -1
  185. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  186. package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
  187. package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +1 -1
  188. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
  189. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
  190. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +97 -56
  191. package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +129 -34
  192. package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +52 -4
  193. package/.next/standalone/node_modules/next/dist/server/config-schema.js +1 -1
  194. package/.next/standalone/node_modules/next/dist/server/config-shared.js +1 -2
  195. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  196. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  197. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  198. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +8 -27
  199. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  200. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +90 -53
  201. package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +15 -2
  202. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +4 -0
  203. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  204. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +1 -1
  205. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  206. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +2 -1
  207. package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +12 -0
  208. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  209. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  210. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  211. package/.next/standalone/node_modules/next/package.json +10 -10
  212. package/.next/standalone/package.json +7 -6
  213. package/.next/standalone/pi-extension/index.ts +38 -5
  214. package/.next/standalone/server.js +1 -1
  215. package/bin/failproofai.mjs +14 -13
  216. package/dist/cli.mjs +789 -399
  217. package/lib/hermes-profiles.ts +156 -0
  218. package/lib/hermes-projects.ts +91 -25
  219. package/lib/hermes-sessions.ts +65 -10
  220. package/lib/openclaw-projects.ts +111 -18
  221. package/lib/openclaw-sessions.ts +20 -0
  222. package/lib/paths.ts +36 -0
  223. package/lib/project-tree.ts +193 -0
  224. package/lib/projects.ts +22 -0
  225. package/package.json +7 -6
  226. package/pi-extension/index.ts +38 -5
  227. package/scripts/launch.ts +7 -0
  228. package/src/audit/cli-adapters/hermes.ts +6 -3
  229. package/src/audit/cli-adapters/openclaw.ts +17 -13
  230. package/src/hooks/configure-wizard.ts +24 -2
  231. package/src/hooks/custom-hooks-loader.ts +90 -13
  232. package/src/hooks/enforcement-capability.ts +266 -0
  233. package/src/hooks/handler.ts +20 -3
  234. package/src/hooks/hook-activity-store.ts +14 -0
  235. package/src/hooks/hooks-config.ts +90 -6
  236. package/src/hooks/integrations.ts +123 -19
  237. package/src/hooks/manager.ts +206 -75
  238. package/src/hooks/policy-evaluator.ts +59 -7
  239. package/src/hooks/policy-types.ts +31 -0
  240. package/src/hooks/types.ts +50 -2
  241. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +0 -3
  243. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +0 -4
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +0 -4
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +0 -4
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +0 -4
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zf8xlq._.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/[root-of-the-server]__1tkjqka._.js +0 -4
  257. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +0 -3
  260. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +0 -3
  261. package/.next/standalone/.next/static/chunks/0o0ut0ugd0_-s.js +0 -1
  262. package/.next/standalone/.next/static/chunks/0xl97x-874k-u.js +0 -1
  263. package/.next/standalone/.next/static/chunks/19d7hqx8udopz.js +0 -1
  264. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +0 -2
  265. package/.next/standalone/.next/static/chunks/370sj2lgkw5ey.js +0 -1
  266. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +0 -1
  267. package/.next/standalone/.next/static/chunks/42oeva1-w0z2d.js +0 -6
  268. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → nhn6Tt0--xZKdgVGIqR6E}/_buildManifest.js +0 -0
  269. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → nhn6Tt0--xZKdgVGIqR6E}/_clientMiddlewareManifest.js +0 -0
  270. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → nhn6Tt0--xZKdgVGIqR6E}/_ssgManifest.js +0 -0
@@ -14,15 +14,16 @@ import {
14
14
  type HookScope,
15
15
  type IntegrationType,
16
16
  } from "./types";
17
- import { claudeCode, getIntegration } from "./integrations";
17
+ import { claudeCode, getIntegration, settingsPathsFor } from "./integrations";
18
18
  import { promptPolicySelection } from "./install-prompt";
19
- import { readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig } from "./hooks-config";
20
- import type { HooksConfig } from "./policy-types";
19
+ import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
20
+ import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
21
21
  import { BUILTIN_POLICIES } from "./builtin-policies";
22
22
  import { loadCustomHooks, discoverPolicyFiles } from "./custom-hooks-loader";
23
23
  import { trackHookEvent } from "./hook-telemetry";
24
24
  import { getInstanceId, hashToId } from "../../lib/telemetry-id";
25
25
  import { CliError } from "../cli-error";
26
+ import { hookLogWarn } from "./hook-logger";
26
27
 
27
28
  const VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
28
29
 
@@ -109,7 +110,7 @@ export async function installHooks(
109
110
  cwd?: string,
110
111
  includeBeta = false,
111
112
  source?: string,
112
- customPoliciesPath?: string,
113
+ customPoliciesPath?: string | string[],
113
114
  removeCustomHooks = false,
114
115
  cli?: IntegrationType[],
115
116
  options: InstallHooksOptions = {},
@@ -140,7 +141,7 @@ async function installHooksImpl(
140
141
  cwd?: string,
141
142
  includeBeta = false,
142
143
  source?: string,
143
- customPoliciesPath?: string,
144
+ customPoliciesPath?: string | string[],
144
145
  removeCustomHooks = false,
145
146
  cli?: IntegrationType[],
146
147
  replace = false,
@@ -211,62 +212,104 @@ async function installHooksImpl(
211
212
  selectedPolicies = await promptPolicySelection(preSelected, { includeBeta });
212
213
  }
213
214
 
214
- // Preserve existing config fields (policyParams, customPoliciesPath, llm) when updating
215
+ // Preserve existing config fields when updating. New writes use the plural
216
+ // form, while reads continue to accept the legacy singular field.
215
217
  const configToWrite = { ...previousConfig, enabledPolicies: selectedPolicies };
216
218
  if (removeCustomHooks) {
217
219
  delete configToWrite.customPoliciesPath;
218
- } else if (customPoliciesPath) {
219
- configToWrite.customPoliciesPath = resolve(customPoliciesPath);
220
- // Validate the file before committing it to config
221
- let validatedHooks: Awaited<ReturnType<typeof loadCustomHooks>> = [];
222
- try {
223
- validatedHooks = await loadCustomHooks(configToWrite.customPoliciesPath, { strict: true });
224
- } catch (err) {
225
- const msg = err instanceof Error ? err.message : String(err);
226
- try {
227
- await trackHookEvent(getInstanceId(), "custom_policy_validation_failed", {
228
- scope,
229
- error_type: /not found/i.test(msg) ? "file_not_found" : "load_error",
230
- });
231
- } catch {}
232
- console.error(`Error: ${msg}`);
233
- process.exit(1);
234
- }
235
- if (validatedHooks.length === 0) {
220
+ delete configToWrite.customPoliciesPaths;
221
+ } else if (
222
+ customPoliciesPath &&
223
+ (typeof customPoliciesPath === "string" || customPoliciesPath.length > 0)
224
+ ) {
225
+ const incoming = (typeof customPoliciesPath === "string" ? [customPoliciesPath] : customPoliciesPath)
226
+ .map((path) => resolve(path));
227
+
228
+ // Additive by default, mirroring `enabledPolicies` directly above: a second
229
+ // `--custom` ADDS to what is configured rather than silently discarding it.
230
+ // Replacing was the surprising half of the old single-path field — running
231
+ // `-c a` then `-c b` left only `b`, with nothing printed to say `a` had
232
+ // stopped applying. `replace` (passed by the configure wizard) still makes
233
+ // the given set authoritative, exactly as it does for enabled policies.
234
+ //
235
+ // Carried-over paths are filtered by existence first: a file deleted after
236
+ // it was configured must not make every future install fail, which is what
237
+ // the strict validation below would do.
238
+ const carried = (
239
+ previousConfig.customPoliciesPaths ??
240
+ (previousConfig.customPoliciesPath ? [previousConfig.customPoliciesPath] : [])
241
+ )
242
+ .map((path) => resolve(path))
243
+ .filter((path) => {
244
+ if (existsSync(path)) return true;
245
+ console.log(`Dropping custom policies path (file no longer exists): ${path}`);
246
+ return false;
247
+ });
248
+
249
+ configToWrite.customPoliciesPaths = replace
250
+ ? [...new Set(incoming)]
251
+ : [...new Set([...carried, ...incoming])];
252
+ delete configToWrite.customPoliciesPath;
253
+
254
+ // Validate only what this invocation added. Carried-over paths were
255
+ // validated when they were added, and re-validating them here would let one
256
+ // stale file block an unrelated install.
257
+ for (const path of incoming) {
258
+ let validatedHooks: Awaited<ReturnType<typeof loadCustomHooks>> = [];
236
259
  try {
237
- await trackHookEvent(getInstanceId(), "custom_policy_validation_failed", {
238
- scope,
239
- error_type: "no_hooks_registered",
240
- });
241
- } catch {}
242
- console.error(
243
- `Error: no hooks registered in ${customPoliciesPath}. ` +
244
- `Make sure your file calls customPolicies.add(...) at least once.`,
260
+ validatedHooks = await loadCustomHooks(path, { strict: true });
261
+ } catch (err) {
262
+ const msg = err instanceof Error ? err.message : String(err);
263
+ try {
264
+ await trackHookEvent(getInstanceId(), "custom_policy_validation_failed", {
265
+ scope,
266
+ error_type: /not found/i.test(msg) ? "file_not_found" : "load_error",
267
+ });
268
+ } catch {}
269
+ console.error(`Error: ${msg}`);
270
+ process.exit(1);
271
+ }
272
+ if (validatedHooks.length === 0) {
273
+ try {
274
+ await trackHookEvent(getInstanceId(), "custom_policy_validation_failed", {
275
+ scope,
276
+ error_type: "no_hooks_registered",
277
+ });
278
+ } catch {}
279
+ console.error(
280
+ `Error: no hooks registered in ${path}. ` +
281
+ `Make sure your file calls customPolicies.add(...) at least once.`,
282
+ );
283
+ process.exit(1);
284
+ }
285
+ console.log(
286
+ `\nValidated ${validatedHooks.length} custom hook(s) from ${path}: ${validatedHooks.map((h) => h.name).join(", ")}`,
245
287
  );
246
- process.exit(1);
247
288
  }
248
- console.log(
249
- `\nValidated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`,
250
- );
251
289
  }
252
290
  writeScopedHooksConfig(configToWrite, scope, cwd);
253
291
  console.log(`\nEnabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}\n`);
254
292
  if (removeCustomHooks) {
255
293
  console.log("Custom hooks path cleared.");
256
- } else if (configToWrite.customPoliciesPath) {
257
- console.log(`Custom hooks path: ${configToWrite.customPoliciesPath}`);
294
+ } else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {
295
+ const paths = configToWrite.customPoliciesPaths ?? [configToWrite.customPoliciesPath!];
296
+ console.log(`Custom hooks paths: ${paths.join(", ")}`);
258
297
  }
259
298
 
260
299
  // Write hooks for each selected CLI
261
300
  const writtenSettingsPaths: { cli: IntegrationType; path: string }[] = [];
262
301
  for (const cliId of selectedClis) {
263
302
  const integration = getIntegration(cliId);
264
- const settingsPath = integration.getSettingsPath(scope, cwd);
303
+ // Usually one path; Hermes returns one per profile (each is a separate home
304
+ // dir with its own config.yaml, and a missed one runs unhooked in silence).
305
+ const settingsPaths = settingsPathsFor(integration, scope, cwd);
265
306
  try {
266
- const settings = integration.readSettings(settingsPath);
267
- integration.writeHookEntries(settings, binaryPath, scope);
268
- integration.writeSettings(settingsPath, settings);
269
- writtenSettingsPaths.push({ cli: cliId, path: settingsPath });
307
+ for (const settingsPath of settingsPaths) {
308
+ const settings = integration.readSettings(settingsPath);
309
+ integration.writeHookEntries(settings, binaryPath, scope);
310
+ integration.writeSettings(settingsPath, settings);
311
+ writtenSettingsPaths.push({ cli: cliId, path: settingsPath });
312
+ }
270
313
  } catch (err) {
271
314
  const errorType = err instanceof Error && /EACCES|EPERM/.test(err.message)
272
315
  ? "permission_denied"
@@ -303,7 +346,7 @@ async function installHooksImpl(
303
346
  arch: arch(),
304
347
  os_release: release(),
305
348
  hostname_hash: hashToId(hostname()),
306
- has_custom_hooks_path: !!(configToWrite.customPoliciesPath),
349
+ has_custom_hooks_path: !!(configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath),
307
350
  has_policy_params: !!(configToWrite.policyParams && Object.keys(configToWrite.policyParams).length > 0),
308
351
  param_policy_names: configToWrite.policyParams ? Object.keys(configToWrite.policyParams) : [],
309
352
  command_format: scope === "project" ? "npx" : "absolute",
@@ -382,6 +425,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
382
425
  if (opts?.removeCustomHooks) {
383
426
  const config = readScopedHooksConfig(configScope, cwd);
384
427
  delete config.customPoliciesPath;
428
+ delete config.customPoliciesPaths;
385
429
  writeScopedHooksConfig(config, configScope, cwd);
386
430
  console.log("Custom hooks path cleared.");
387
431
  }
@@ -452,9 +496,11 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
452
496
  : [];
453
497
 
454
498
  for (const s of scopesToRemove) {
455
- const settingsPath = integration.getSettingsPath(s, cwd);
499
+ // Usually one path; Hermes returns one per profile.
500
+ const settingsPaths = settingsPathsFor(integration, s, cwd);
501
+ const existing = settingsPaths.filter((p) => existsSync(p));
456
502
 
457
- if (!existsSync(settingsPath)) {
503
+ if (existing.length === 0) {
458
504
  if (scope !== "all" && selectedClis.length === 1) {
459
505
  console.log("No settings file found. Nothing to remove.");
460
506
  nothingToReport = true;
@@ -462,18 +508,22 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
462
508
  continue;
463
509
  }
464
510
 
465
- const removed = integration.removeHooksFromFile(settingsPath);
466
- if (removed === 0 && scope !== "all" && selectedClis.length === 1) {
511
+ let removedHere = 0;
512
+ for (const settingsPath of existing) {
513
+ const removed = integration.removeHooksFromFile(settingsPath);
514
+ removedHere += removed;
515
+ if (removed > 0 && scope !== "all") {
516
+ console.log(`Removed ${removed} failproofai hook(s) from ${integration.displayName} settings.`);
517
+ console.log(`Settings: ${settingsPath}`);
518
+ }
519
+ }
520
+
521
+ if (removedHere === 0 && scope !== "all" && selectedClis.length === 1) {
467
522
  console.log("No hooks found in settings. Nothing to remove.");
468
523
  nothingToReport = true;
469
524
  continue;
470
525
  }
471
- totalRemoved += removed;
472
-
473
- if (scope !== "all") {
474
- console.log(`Removed ${removed} failproofai hook(s) from ${integration.displayName} settings.`);
475
- console.log(`Settings: ${settingsPath}`);
476
- }
526
+ totalRemoved += removedHere;
477
527
  }
478
528
  }
479
529
 
@@ -484,7 +534,9 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
484
534
  for (const cliId of selectedClis) {
485
535
  const integration = getIntegration(cliId);
486
536
  for (const s of integration.scopes) {
487
- console.log(` ${integration.displayName} / ${s}: ${integration.getSettingsPath(s, cwd)}`);
537
+ for (const p of settingsPathsFor(integration, s, cwd)) {
538
+ console.log(` ${integration.displayName} / ${s}: ${p}`);
539
+ }
488
540
  }
489
541
  }
490
542
  }
@@ -513,14 +565,14 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
513
565
  // Clear config across all three scopes
514
566
  for (const s of HOOK_SCOPES) {
515
567
  const existing = readScopedHooksConfig(s, cwd);
516
- if (existing.enabledPolicies.length > 0 || existing.customPoliciesPath || existing.policyParams) {
517
- const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
568
+ if (existing.enabledPolicies.length > 0 || existing.customPoliciesPaths?.length || existing.customPoliciesPath || existing.policyParams) {
569
+ const { customPoliciesPath: _drop, customPoliciesPaths: _dropMany, policyParams: _dropParams, ...rest } = existing;
518
570
  writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, s, cwd);
519
571
  }
520
572
  }
521
573
  } else if (!HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
522
574
  const existing = readScopedHooksConfig(configScope, cwd);
523
- const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
575
+ const { customPoliciesPath: _drop, customPoliciesPaths: _dropMany, policyParams: _dropParams, ...rest } = existing;
524
576
  writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
525
577
  }
526
578
  }
@@ -540,6 +592,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
540
592
  export async function listHooks(cwd?: string): Promise<void> {
541
593
  const config = readMergedHooksConfig(cwd);
542
594
  const enabledSet = new Set(config.enabledPolicies);
595
+ const disabledCustomSet = new Set(config.disabledCustomPolicies ?? []);
543
596
 
544
597
  // Determine which scopes have hooks installed (deduplicate when paths overlap, e.g. cwd === home)
545
598
  const uniqueScopes = deduplicateScopes(HOOK_SCOPES, cwd);
@@ -676,19 +729,29 @@ export async function listHooks(cwd?: string): Promise<void> {
676
729
  }
677
730
  }
678
731
 
679
- // Custom Policies section
680
- if (config.customPoliciesPath) {
681
- console.log(`\n \u2500\u2500 Custom Policies (${config.customPoliciesPath}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
682
- if (!existsSync(config.customPoliciesPath)) {
683
- console.log(` \x1B[31m\u2717 File not found: ${config.customPoliciesPath}\x1B[0m`);
684
- } else {
685
- const hooks = await loadCustomHooks(config.customPoliciesPath);
732
+ // Explicit Custom Policies section
733
+ const explicitPaths = configuredCustomPolicyPaths(config);
734
+ if (explicitPaths.length > 0) {
735
+ console.log(`\n \u2500\u2500 Custom Policies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
736
+ for (const path of explicitPaths) {
737
+ // Enforcement resolves configured paths from the project config root.
738
+ // Use the same canonical path here so the ID checked by the CLI exactly
739
+ // matches the ID written by the dashboard.
740
+ const absPath = resolve(findProjectConfigDir(cwd ?? process.cwd()), path);
741
+ console.log(` ${absPath}`);
742
+ if (!existsSync(absPath)) {
743
+ console.log(` \x1B[31m\u2717 File not found: ${absPath}\x1B[0m`);
744
+ continue;
745
+ }
746
+ const hooks = await loadCustomHooks(absPath);
686
747
  if (hooks.length === 0) {
687
748
  console.log(` \x1B[31m\u2717 ERR failed to load (check ~/.failproofai/logs/hooks.log)\x1B[0m`);
688
749
  } else {
689
750
  const descColWidth = nameColWidth;
690
751
  for (const hook of hooks) {
691
- console.log(` \x1B[32m\u2713\x1B[0m ${hook.name.padEnd(descColWidth)}${hook.description ?? ""}`);
752
+ const disabled = disabledCustomSet.has(`custom:${absPath}:${hook.name}`);
753
+ const status = disabled ? "\x1B[2m OFF\x1B[0m" : "\x1B[32m\u2713 ON\x1B[0m";
754
+ console.log(` ${status} ${hook.name.padEnd(descColWidth)}${hook.description ?? ""}`);
692
755
  }
693
756
  }
694
757
  }
@@ -696,33 +759,101 @@ export async function listHooks(cwd?: string): Promise<void> {
696
759
  }
697
760
 
698
761
  // Convention Policies section (.failproofai/policies/*policies.{js,mjs,ts})
699
- const base = cwd ? resolve(cwd) : process.cwd();
762
+ // Walk up to the project root like enforcement does
763
+ // (custom-hooks-loader -> findProjectConfigDir); resolving at the exact cwd
764
+ // meant running this from a subdirectory listed no project policies while the
765
+ // hook path was loading them.
766
+ const base = findProjectConfigDir(cwd ?? process.cwd());
767
+ const projectDir = resolve(base, ".failproofai", "policies");
768
+ const userDir = resolve(homedir(), ".failproofai", "policies");
769
+ const sameDir = userDir === projectDir;
700
770
  const conventionDirs: { label: string; dir: string }[] = [
701
- { label: "Project", dir: resolve(base, ".failproofai", "policies") },
702
- { label: "User", dir: resolve(homedir(), ".failproofai", "policies") },
771
+ { label: sameDir ? "Project + User" : "Project", dir: projectDir },
772
+ // Running from $HOME makes both paths identical. Listing the directory
773
+ // twice printed every file a second time as "failed to load" — the file was
774
+ // already imported by the first pass, so the module cache short-circuits
775
+ // `customPolicies.add` and `loadCustomHooks` legitimately returns 0 hooks.
776
+ // Nothing was wrong with the policy; the second listing was.
777
+ ...(sameDir ? [] : [{ label: "User", dir: userDir }]),
703
778
  ];
704
779
 
780
+ // Record of what was found, mirrored into policies-config.json below so the
781
+ // config shows installed convention policies and not only enabled builtins.
782
+ const discovered: Record<"project" | "user", ConventionPolicyRecord[]> = {
783
+ project: [],
784
+ user: [],
785
+ };
786
+
705
787
  for (const { label, dir } of conventionDirs) {
706
788
  const files = discoverPolicyFiles(dir);
707
789
  if (files.length === 0) continue;
708
790
 
791
+ // A shared project/user directory is listed once but belongs to both, so
792
+ // its record is written to both scopes' config files.
793
+ const targets: ("project" | "user")[] =
794
+ dir === projectDir && dir === userDir ? ["project", "user"] : dir === projectDir ? ["project"] : ["user"];
795
+ // When both directories are the same, runtime discovery loads the file as
796
+ // project convention policy and skips the duplicate user pass.
797
+ const policyScope: "project" | "user" = dir === projectDir ? "project" : "user";
798
+ const record = (file: string, hooks: string[]) => {
799
+ for (const t of targets) discovered[t].push({ file, hooks });
800
+ };
801
+
709
802
  console.log(`\n \u2500\u2500 Convention Policies \u2014 ${label} (${dir}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
803
+ // `nameColWidth` is sized to the longest BUILTIN name, but a convention
804
+ // filename can be longer \u2014 `padEnd` is then a no-op and the hook count runs
805
+ // straight into the filename with no space
806
+ // (`enforce-bengaluru-event-links-policies.mjs1 hook(s)`). Widen to fit the
807
+ // files actually being printed, keeping a two-space gutter.
808
+ const colWidth = Math.max(nameColWidth, ...files.map((f) => basename(f).length + 2));
710
809
  for (const file of files) {
711
810
  try {
712
811
  const hooks = await loadCustomHooks(file);
812
+ const filename = basename(file);
813
+ record(filename, hooks.map((h) => h.name));
713
814
  if (hooks.length === 0) {
714
- const filename = basename(file);
715
- console.log(` \x1B[31m\u2717\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
815
+ console.log(` \x1B[31m\u2717\x1B[0m ${filename.padEnd(colWidth)}\x1B[31mfailed to load\x1B[0m`);
716
816
  } else {
717
- const filename = basename(file);
718
- const hookSummary = hooks.map((h) => h.name).join(", ");
719
- console.log(` \x1B[32m\u2713\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
817
+ const hookStates = hooks.map((hook) => ({
818
+ hook,
819
+ disabled: disabledCustomSet.has(`convention:${policyScope}:${filename}:${hook.name}`),
820
+ }));
821
+ const disabledCount = hookStates.filter((entry) => entry.disabled).length;
822
+ const status = disabledCount === 0
823
+ ? "\x1B[32m\u2713 ON\x1B[0m"
824
+ : disabledCount === hooks.length
825
+ ? "\x1B[2m OFF\x1B[0m"
826
+ : "\x1B[33m\u25D0 MIXED\x1B[0m";
827
+ const hookSummary = hookStates
828
+ .map(({ hook, disabled }) => `${hook.name}${disabled ? " (OFF)" : ""}`)
829
+ .join(", ");
830
+ console.log(` ${status} ${filename.padEnd(colWidth)}${hooks.length} hook(s): ${hookSummary}`);
720
831
  }
721
832
  } catch {
722
833
  const filename = basename(file);
723
- console.log(` \x1B[31m\u2717\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
834
+ record(filename, []);
835
+ console.log(` \x1B[31m\u2717\x1B[0m ${filename.padEnd(colWidth)}\x1B[31merror\x1B[0m`);
724
836
  }
725
837
  }
726
838
  console.log();
727
839
  }
840
+
841
+ // Mirror what was just listed into the USER config. Safe here because
842
+ // `failproofai policies` is a one-shot command — never do this on the hook
843
+ // path (see the HooksConfig.conventionPolicies doc comment).
844
+ //
845
+ // User scope only, deliberately. A project's `.failproofai/policies-config.json`
846
+ // is routinely committed (this repo tracks its own), so writing the record
847
+ // there would make a plain `failproofai policies` dirty the working tree and
848
+ // put a spurious diff in front of every contributor — a read command must not
849
+ // do that. `discovered.project` is still collected so the listing above can
850
+ // report it; it simply is not persisted.
851
+ try {
852
+ syncConventionPolicies(discovered.user, "user");
853
+ } catch (err) {
854
+ // Listing must never fail because the mirror could not be written.
855
+ hookLogWarn(
856
+ `could not record convention policies in config: ${err instanceof Error ? err.message : String(err)}`,
857
+ );
858
+ }
728
859
  }
@@ -179,6 +179,21 @@ export async function evaluatePolicies(
179
179
  decision: "deny",
180
180
  };
181
181
  }
182
+ // `beforeSubmitPrompt` does NOT read `permission`. Its only block key
183
+ // is `continue: false` (+ an optional `user_message` shown to the
184
+ // user); an object carrying unknown keys validates and is dropped, so
185
+ // the flat deny below was inert and every prompt went through. Verified
186
+ // against cursor-agent 2026.07.16-899851b, 1931.index.js char 887883.
187
+ if (eventType === "UserPromptSubmit") {
188
+ return {
189
+ exitCode: 0,
190
+ stdout: JSON.stringify({ continue: false, user_message: blockedMessage }),
191
+ stderr: "",
192
+ policyName: policy.name,
193
+ reason,
194
+ decision: "deny",
195
+ };
196
+ }
182
197
  const response = {
183
198
  permission: "deny",
184
199
  user_message: blockedMessage,
@@ -229,13 +244,15 @@ export async function evaluatePolicies(
229
244
  }
230
245
 
231
246
  // Hermes: the block contract is `{"decision":"block","reason"}` on stdout;
232
- // Hermes IGNORES exit codes, so the JSON is the only channel. This one
233
- // return covers every Hermes event we install (PreToolUse / PostToolUse /
234
- // SubagentStop) — there is no Stop/turn-end event to special-case (Hermes
235
- // has no turn boundary; the 5 require-*-before-stop builtins never fire for
236
- // it — see CLAUDE.md / the audit plan). A block on PreToolUse stops the
237
- // tool before it runs, regardless of the originating platform
238
- // (slack/telegram/cli/cron) or subagent.
247
+ // Hermes IGNORES exit codes, so the JSON is the only channel. A block on
248
+ // PreToolUse stops the tool before it runs, regardless of the originating
249
+ // platform (slack/telegram/cli/cron) or subagent.
250
+ //
251
+ // Only `pre_tool_call` and `pre_verify` are gated in upstream's
252
+ // `_parse_response` (agent/shell_hooks.py:567-621); the shape is still
253
+ // emitted for the other installed events, where it is read by nothing.
254
+ // We do not install `pre_verify` (see the note in types.ts), so Hermes
255
+ // has no canonical Stop event and no Stop branch is needed here.
239
256
  if (session?.cli === "hermes") {
240
257
  return {
241
258
  exitCode: 0,
@@ -422,6 +439,41 @@ export async function evaluatePolicies(
422
439
  };
423
440
  }
424
441
 
442
+ // Copilot reads two shapes we were not sending, so a deny on either of
443
+ // these events was emitted, logged, counted as enforcement — and ignored.
444
+ // Both verified against the shipped @github/copilot-linux-x64 bundle.
445
+ // Note exit 2 is NEVER a deny channel on copilot for any event; it is
446
+ // logged as `Hook command exited with code 2 (warning)`.
447
+ if (session?.cli === "copilot") {
448
+ // `userPromptSubmit` gates the turn, but only on {decision:"block"} at
449
+ // exit 0 (consumer app.js@2823018). We were emitting exit 2 + stderr,
450
+ // which copilot warns about and then submits the prompt anyway.
451
+ if (eventType === "UserPromptSubmit") {
452
+ return {
453
+ exitCode: 0,
454
+ stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
455
+ stderr: "",
456
+ policyName: policy.name,
457
+ reason,
458
+ decision: "deny",
459
+ };
460
+ }
461
+ // Copilot consumes a FLAT {behavior, message} here (normalizer
462
+ // CMn@179042 -> mapper h4t@2686538). The Codex-shaped nested
463
+ // hookSpecificOutput.decision below normalizes to `{}` on copilot, so
464
+ // the permission prompt proceeded as if no policy existed.
465
+ if (eventType === "PermissionRequest") {
466
+ return {
467
+ exitCode: 0,
468
+ stdout: JSON.stringify({ behavior: "deny", message: blockedMessage }),
469
+ stderr: "",
470
+ policyName: policy.name,
471
+ reason,
472
+ decision: "deny",
473
+ };
474
+ }
475
+ }
476
+
425
477
  if (eventType === "PermissionRequest") {
426
478
  // Codex-only: hookSpecificOutput.decision.behavior = "allow" | "deny"
427
479
  // (per https://developers.openai.com/codex/hooks#permissionrequest).
@@ -84,7 +84,12 @@ export interface HooksConfig {
84
84
  enabledPolicies: string[];
85
85
  llm?: LlmConfig;
86
86
  policyParams?: Record<string, Record<string, unknown>>;
87
+ /** Explicit custom policy files, loaded in array order. */
88
+ customPoliciesPaths?: string[];
89
+ /** @deprecated Use customPoliciesPaths. Kept for existing config files. */
87
90
  customPoliciesPath?: string;
91
+ /** Source-qualified custom policy IDs that should not be registered. */
92
+ disabledCustomPolicies?: string[];
88
93
  /**
89
94
  * Turn off convention-discovered custom policies (`.failproofai/policies/`)
90
95
  * without deleting or renaming the files. Absent means enabled — the default
@@ -93,4 +98,30 @@ export interface HooksConfig {
93
98
  * Only ever written explicitly as `false`.
94
99
  */
95
100
  customPoliciesEnabled?: boolean;
101
+ /**
102
+ * Record of the convention policies (`.failproofai/policies/*policies.mjs`)
103
+ * discovered at this scope the last time a failproofai command ran.
104
+ *
105
+ * DESCRIPTIVE, NEVER AUTHORITATIVE. Enforcement always discovers from the
106
+ * filesystem (`loadAllCustomHooks`) and never reads this key — a policy file
107
+ * dropped in enforces on the very next tool call whether or not this list has
108
+ * caught up. Making the loader trust it would mean a freshly-copied policy
109
+ * silently doing nothing until some command refreshed the config, which is
110
+ * the exact silent-non-enforcement failure this project exists to remove.
111
+ *
112
+ * Refreshed wholesale (not merged) by `failproofai policies` and the setup
113
+ * wizard, so a deleted file disappears from the record on the next run. It is
114
+ * deliberately NOT written from the hook path: that runs on every tool call,
115
+ * and a read-modify-write from concurrent short-lived hook processes with no
116
+ * locking would corrupt the file that governs enforcement.
117
+ */
118
+ conventionPolicies?: ConventionPolicyRecord[];
119
+ }
120
+
121
+ /** One convention policy file and the hooks it registered, as last observed. */
122
+ export interface ConventionPolicyRecord {
123
+ /** Basename, e.g. `hermes-guardrails.policies.mjs`. */
124
+ file: string;
125
+ /** Hook names the file registered, or `[]` if it failed to load. */
126
+ hooks: string[];
96
127
  }
@@ -105,8 +105,34 @@ export const HERMES_TOOL_INPUT_MAP: Record<string, Record<string, string>> = {
105
105
  // (`~/.hermes/config.yaml`; Hermes has no project scope). `pre_tool_call` is the
106
106
  // core deny point — it fires for tool calls from every source
107
107
  // (slack/telegram/cli/cron) and internal subagents, so a single install
108
- // intercepts all platforms. Hermes has NO turn-end `Stop` event, so the
109
- // `require-*-before-stop` builtins never fire for it (see the audit plan).
108
+ // intercepts all platforms.
109
+ //
110
+ // `pre_verify` IS a turn-end gate — the earlier claim here that Hermes has none
111
+ // was wrong. We deliberately do NOT install it (product decision, 2026-07-29);
112
+ // this note records what it would buy so the choice can be revisited without
113
+ // re-deriving it.
114
+ //
115
+ // Upstream fires it once per turn when the agent has edited code and is about to
116
+ // finish (`agent/conversation_loop.py:6754`), and its parser accepts our Claude
117
+ // Stop shape verbatim — `{decision:"block", reason}` means "block the stop",
118
+ // i.e. keep going (`agent/shell_hooks.py:606-615`). The reason is injected as a
119
+ // synthetic user message and the loop re-enters
120
+ // (`conversation_loop.py:6774-6800`). Installing it is one entry in
121
+ // config.yaml — it is a shell hook like the other five, no Python involved.
122
+ //
123
+ // It would make the 5 `require-*-before-stop` builtins fire on Hermes, subject
124
+ // to three upstream conditions:
125
+ // 1. It fires ONLY on turns that landed a file mutation, and that means
126
+ // exactly `write_file` or `patch` (`agent/tool_result_classification.py:9`)
127
+ // — a turn that did its work through `terminal` (sed -i, rm, >) does not
128
+ // qualify, so a chat-only gateway may never see it.
129
+ // 2. Capped at 3 nudges per turn (`DEFAULT_MAX_VERIFY_NUDGES`,
130
+ // `agent/verify_hooks.py:21`), operator-overridable; resets each turn.
131
+ // 3. It landed upstream ~2026-06-30. Older Hermes fails the key against
132
+ // VALID_HOOKS and warn-and-skips it SILENTLY (`agent/shell_hooks.py:325`).
133
+ //
134
+ // Until it is installed, `HERMES_EVENT_MAP` emits no `Stop` and those 5
135
+ // builtins remain inapplicable on Hermes.
110
136
  export const HERMES_HOOK_SCOPES = ["user"] as const;
111
137
  export type HermesHookScope = (typeof HERMES_HOOK_SCOPES)[number];
112
138
 
@@ -1002,8 +1028,30 @@ export const HOOK_EVENT_TYPES = [
1002
1028
  "Setup",
1003
1029
  ] as const;
1004
1030
 
1031
+
1032
+
1005
1033
  export type HookEventType = (typeof HOOK_EVENT_TYPES)[number];
1006
1034
 
1035
+ /**
1036
+ * Events failproofai actually INSTALLS a hook for on Claude Code.
1037
+ *
1038
+ * Everything in `HOOK_EVENT_TYPES` except `WorktreeCreate`, which is not a
1039
+ * permission gate at all: Claude uses it as a worktree-PATH PROVIDER, taking
1040
+ * the stdout of the first hook that succeeds as the directory to create and
1041
+ * failing with "WorktreeCreate hook failed" when no hook supplies one. Our
1042
+ * allow path writes nothing to stdout — correctly, by the contract every other
1043
+ * event uses — so merely being registered there broke `claude --worktree` and
1044
+ * `/worktree` for every user, whatever any policy decided.
1045
+ *
1046
+ * It stays in `HOOK_EVENT_TYPES` because that list is the canonical set a
1047
+ * policy may subscribe to; this one governs what we write into settings.json.
1048
+ * No builtin matches it (all 39 match only PreToolUse / PostToolUse /
1049
+ * PermissionRequest / Stop), so nothing is lost by not registering.
1050
+ */
1051
+ export const CLAUDE_INSTALL_EVENT_TYPES = HOOK_EVENT_TYPES.filter(
1052
+ (e) => e !== "WorktreeCreate",
1053
+ ) as readonly HookEventType[];
1054
+
1007
1055
  export const FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__" as const;
1008
1056
 
1009
1057
  export interface ClaudeHookEntry {