failproofai 0.0.13 → 0.0.14-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 (310) hide show
  1. package/.next/standalone/.codex/hooks.json +6 -6
  2. package/.next/standalone/.cursor/hooks.json +6 -6
  3. package/.next/standalone/.factory/hooks.json +112 -0
  4. package/.next/standalone/.next/BUILD_ID +1 -1
  5. package/.next/standalone/.next/build-manifest.json +5 -5
  6. package/.next/standalone/.next/prerender-manifest.json +3 -3
  7. package/.next/standalone/.next/required-server-files.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  9. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  16. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  26. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  27. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  30. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  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 +3 -3
  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.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
  40. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  42. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/index.html +1 -1
  46. package/.next/standalone/.next/server/app/index.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  48. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  49. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  50. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  51. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  52. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  57. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  58. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  59. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
  64. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -3
  70. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  73. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  74. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  75. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  76. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jllheg._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/{_1z062h5._.js → _1vslkoe._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +24 -12
  105. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__108slqn._.js → [root-of-the-server]__0brjczq._.js} +2 -2
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +3 -0
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
  122. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07iom4f._.js → [root-of-the-server]__0wp9w6i._.js} +2 -2
  123. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
  124. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +12 -9
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +4 -0
  126. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
  129. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +3 -0
  131. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/{_0ul5rf_._.js → _1_ozgif._.js} +2 -2
  133. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  138. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/ssr/lib_antigravity-projects_ts_0_6zf11._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/lib_devin-projects_ts_0a64qdy._.js +3 -0
  143. package/.next/standalone/.next/server/chunks/ssr/lib_factory-projects_ts_1uyaahq._.js +3 -0
  144. package/.next/standalone/.next/server/chunks/ssr/lib_goose-projects_ts_0zkscri._.js +3 -0
  145. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +3 -0
  146. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_13p7zob.js → node_modules_html-to-image_es_index_12rjfea.js} +1 -1
  147. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  150. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  151. package/.next/standalone/.next/server/pages/404.html +1 -1
  152. package/.next/standalone/.next/server/pages/500.html +1 -1
  153. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  154. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  155. package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{30ysv-_8jj052.js → 0d5lypu1zs21r.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/0emg30_6svycu.js +1 -0
  158. package/.next/standalone/.next/static/chunks/0o9wpnze1ey8y.js +6 -0
  159. package/.next/standalone/.next/static/chunks/{05apzuand0c22.js → 1359o5785rmve.js} +2 -2
  160. package/.next/standalone/.next/static/chunks/20tuctw38jbgw.js +1 -0
  161. package/.next/standalone/.next/static/chunks/{0vc0eak148vo6.js → 2ldhnaweu9ew3.js} +1 -1
  162. package/.next/standalone/.next/static/chunks/{23c7v2svx90dp.js → 2vav_x0_336ds.js} +1 -1
  163. package/.next/standalone/.next/static/chunks/3-ogr79xqbw0h.js +1 -0
  164. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
  165. package/.next/standalone/.next/static/chunks/3cg7x0joi2b98.js +1 -0
  166. package/.next/standalone/.next/static/chunks/{36wdtkgdf0zrd.js → 3k87p-tijud-4.js} +1 -1
  167. package/.next/standalone/.next/static/chunks/{2cejn75gug3j8.js → 3kmaqcp7rldad.js} +1 -1
  168. package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
  169. package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
  170. package/.next/standalone/app/api/audit/run/route.ts +2 -0
  171. package/.next/standalone/app/audit/_components/empty-state.tsx +1 -1
  172. package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
  173. package/.next/standalone/app/globals.css +25 -1
  174. package/.next/standalone/app/policies/hooks-client.tsx +2 -4
  175. package/.next/standalone/app/project/[name]/page.tsx +30 -10
  176. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +71 -21
  177. package/.next/standalone/assets/logos/antigravity.svg +1 -0
  178. package/.next/standalone/assets/logos/devin.svg +1 -0
  179. package/.next/standalone/assets/logos/factory-dark.png +0 -0
  180. package/.next/standalone/assets/logos/factory-light.png +0 -0
  181. package/.next/standalone/assets/logos/goose-dark.svg +1 -0
  182. package/.next/standalone/assets/logos/goose-light.svg +1 -0
  183. package/.next/standalone/assets/logos/openclaw.svg +22 -0
  184. package/.next/standalone/ci/cli-integration/Dockerfile +47 -0
  185. package/.next/standalone/ci/cli-integration/README.md +69 -0
  186. package/.next/standalone/ci/cli-integration/canary-policies.mjs +45 -0
  187. package/.next/standalone/ci/cli-integration/capture-tokens.sh +48 -0
  188. package/.next/standalone/ci/cli-integration/inject-tokens.sh +44 -0
  189. package/.next/standalone/ci/cli-integration/install-clis.sh +91 -0
  190. package/.next/standalone/ci/cli-integration/probe-cli.sh +208 -0
  191. package/.next/standalone/ci/cli-integration/report.js +74 -0
  192. package/.next/standalone/ci/cli-integration/run.sh +107 -0
  193. package/.next/standalone/components/navbar.tsx +5 -11
  194. package/.next/standalone/components/reach-developers.tsx +2 -2
  195. package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
  196. package/.next/standalone/lib/antigravity-projects.ts +224 -0
  197. package/.next/standalone/lib/antigravity-sessions.ts +283 -0
  198. package/.next/standalone/lib/cli-registry.ts +26 -6
  199. package/.next/standalone/lib/devin-projects.ts +141 -0
  200. package/.next/standalone/lib/devin-sessions.ts +343 -0
  201. package/.next/standalone/lib/download-session.ts +56 -5
  202. package/.next/standalone/lib/error-origin.ts +67 -0
  203. package/.next/standalone/lib/factory-projects.ts +109 -0
  204. package/.next/standalone/lib/factory-sessions.ts +312 -0
  205. package/.next/standalone/lib/goose-projects.ts +148 -0
  206. package/.next/standalone/lib/goose-sessions.ts +296 -0
  207. package/.next/standalone/lib/install-check.ts +231 -0
  208. package/.next/standalone/lib/openclaw-projects.ts +182 -0
  209. package/.next/standalone/lib/openclaw-sessions.ts +284 -0
  210. package/.next/standalone/lib/projects.ts +33 -9
  211. package/.next/standalone/lib/telemetry-sanitize.ts +43 -0
  212. package/.next/standalone/openclaw-plugin/index.js +223 -0
  213. package/.next/standalone/openclaw-plugin/openclaw.plugin.json +6 -0
  214. package/.next/standalone/openclaw-plugin/package.json +12 -0
  215. package/.next/standalone/package.json +2 -3
  216. package/.next/standalone/server.js +1 -1
  217. package/README.md +94 -62
  218. package/bin/failproofai.mjs +98 -30
  219. package/dist/cli.mjs +12499 -9243
  220. package/lib/antigravity-projects.ts +224 -0
  221. package/lib/antigravity-sessions.ts +283 -0
  222. package/lib/cli-registry.ts +26 -6
  223. package/lib/devin-projects.ts +141 -0
  224. package/lib/devin-sessions.ts +343 -0
  225. package/lib/download-session.ts +56 -5
  226. package/lib/error-origin.ts +67 -0
  227. package/lib/factory-projects.ts +109 -0
  228. package/lib/factory-sessions.ts +312 -0
  229. package/lib/goose-projects.ts +148 -0
  230. package/lib/goose-sessions.ts +296 -0
  231. package/lib/install-check.ts +231 -0
  232. package/lib/openclaw-projects.ts +182 -0
  233. package/lib/openclaw-sessions.ts +284 -0
  234. package/lib/projects.ts +33 -9
  235. package/lib/telemetry-sanitize.ts +43 -0
  236. package/openclaw-plugin/index.js +223 -0
  237. package/openclaw-plugin/openclaw.plugin.json +6 -0
  238. package/openclaw-plugin/package.json +12 -0
  239. package/package.json +2 -3
  240. package/scripts/dev-hook.mjs +292 -0
  241. package/scripts/install-diagnosis.mjs +5 -8
  242. package/scripts/install-telemetry.mjs +12 -4
  243. package/scripts/launch.ts +4 -8
  244. package/scripts/sync-agent-cli-harnesses-prompt.md +7 -10
  245. package/scripts/translate-docs/cli.ts +120 -53
  246. package/scripts/translate-docs/config.ts +57 -13
  247. package/scripts/translate-docs/mdx-translator.ts +177 -28
  248. package/scripts/translate-docs/mintlify-nav.ts +91 -10
  249. package/scripts/translate-docs/readme-translator.ts +13 -5
  250. package/scripts/translate-docs/translator.ts +38 -2
  251. package/scripts/translate-docs/types.ts +3 -0
  252. package/scripts/validate-mdx.ts +12 -4
  253. package/src/audit/cli-adapters/antigravity.ts +87 -0
  254. package/src/audit/cli-adapters/devin.ts +53 -0
  255. package/src/audit/cli-adapters/factory.ts +75 -0
  256. package/src/audit/cli-adapters/goose.ts +53 -0
  257. package/src/audit/cli-adapters/index.ts +30 -6
  258. package/src/audit/cli-adapters/openclaw.ts +56 -0
  259. package/src/audit/cli.ts +83 -8
  260. package/src/audit/report.ts +1 -1
  261. package/src/audit/types.ts +1 -1
  262. package/src/hooks/builtin-policies.ts +5 -5
  263. package/src/hooks/configure-wizard.ts +356 -0
  264. package/src/hooks/handler.ts +283 -218
  265. package/src/hooks/hook-telemetry.ts +43 -3
  266. package/src/hooks/install-prompt.ts +92 -173
  267. package/src/hooks/integrations.ts +739 -138
  268. package/src/hooks/manager.ts +46 -2
  269. package/src/hooks/policy-evaluator.ts +301 -121
  270. package/src/hooks/policy-presets.ts +71 -0
  271. package/src/hooks/resolve-cwd.ts +1 -1
  272. package/src/hooks/resolve-permission-mode.ts +1 -5
  273. package/src/hooks/resolve-transcript-path.ts +25 -7
  274. package/src/hooks/tool-name-canonicalize.ts +36 -3
  275. package/src/hooks/tui.ts +578 -0
  276. package/src/hooks/types.ts +447 -109
  277. package/.next/standalone/.gemini/settings.json +0 -147
  278. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cag8qd._.js +0 -3
  279. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +0 -3
  280. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +0 -3
  281. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +0 -3
  282. package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +0 -3
  283. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +0 -3
  284. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hgv_75._.js +0 -3
  285. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +0 -3
  286. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +0 -3
  287. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +0 -3
  288. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +0 -4
  289. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +0 -3
  290. package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +0 -3
  291. package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_1c7bgx-._.js +0 -3
  292. package/.next/standalone/.next/static/chunks/0fa7j157kg9_d.js +0 -1
  293. package/.next/standalone/.next/static/chunks/0nrah-qv6smeg.js +0 -6
  294. package/.next/standalone/.next/static/chunks/0wtagd8a5gwq-.js +0 -1
  295. package/.next/standalone/.next/static/chunks/1c8i78p7t1i-5.js +0 -1
  296. package/.next/standalone/.next/static/chunks/25qr0e6ez0ei0.js +0 -1
  297. package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +0 -2
  298. package/.next/standalone/assets/logos/gemini-dark.svg +0 -13
  299. package/.next/standalone/assets/logos/gemini-light.svg +0 -13
  300. package/.next/standalone/lib/gemini-projects.ts +0 -243
  301. package/.next/standalone/lib/gemini-sessions.ts +0 -365
  302. package/lib/gemini-projects.ts +0 -243
  303. package/lib/gemini-sessions.ts +0 -365
  304. package/scripts/postinstall.mjs +0 -243
  305. package/scripts/preuninstall.mjs +0 -131
  306. package/src/audit/cli-adapters/gemini.ts +0 -51
  307. package/src/hooks/first-run-nudge.ts +0 -146
  308. /package/.next/standalone/.next/static/{kmzdjTLVs6LqsfyOaUf69 → LkN-JjmhKGmL-WklH7WVi}/_buildManifest.js +0 -0
  309. /package/.next/standalone/.next/static/{kmzdjTLVs6LqsfyOaUf69 → LkN-JjmhKGmL-WklH7WVi}/_clientMiddlewareManifest.js +0 -0
  310. /package/.next/standalone/.next/static/{kmzdjTLVs6LqsfyOaUf69 → LkN-JjmhKGmL-WklH7WVi}/_ssgManifest.js +0 -0
@@ -12,15 +12,17 @@ import type {
12
12
  CodexHookEventType,
13
13
  CursorHookEventType,
14
14
  PiHookEventType,
15
- GeminiHookEventType,
16
15
  HermesHookEventType,
16
+ OpenClawHookEventType,
17
+ AntigravityHookEventType,
17
18
  } from "./types";
18
19
  import {
19
20
  CODEX_EVENT_MAP,
20
21
  CURSOR_EVENT_MAP,
21
22
  PI_EVENT_MAP,
22
- GEMINI_EVENT_MAP,
23
23
  HERMES_EVENT_MAP,
24
+ OPENCLAW_EVENT_MAP,
25
+ ANTIGRAVITY_EVENT_MAP,
24
26
  } from "./types";
25
27
  import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize";
26
28
  import type { PolicyFunction, PolicyResult } from "./policy-types";
@@ -31,7 +33,7 @@ import { clearPolicies, registerPolicy } from "./policy-registry";
31
33
  import { loadAllCustomHooks } from "./custom-hooks-loader";
32
34
  import type { CustomHook } from "./policy-types";
33
35
  import { persistHookActivity } from "./hook-activity-store";
34
- import { trackHookEvent } from "./hook-telemetry";
36
+ import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
35
37
  import { resolveCwd } from "./resolve-cwd";
36
38
  import { resolvePermissionMode } from "./resolve-permission-mode";
37
39
  import { resolveTranscriptPath } from "./resolve-transcript-path";
@@ -45,9 +47,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
45
47
  * `session_start`); Claude Code sends PascalCase. Copilot CLI is installed
46
48
  * in "VS Code compatible" PascalCase mode (see integrations.ts), so its event
47
49
  * NAMES arrive PascalCase already (note: tool names are a separate matter and
48
- * are canonicalized in canonicalizeToolName below). Gemini also sends
49
- * PascalCase event names but with different spellings (`BeforeTool`,
50
- * `BeforeAgent`, `AfterAgent`); we map via GEMINI_EVENT_MAP. The internal
50
+ * are canonicalized in canonicalizeToolName below). The internal
51
51
  * registry, builtin policies, and policy.match.events all key on PascalCase.
52
52
  */
53
53
  function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType {
@@ -63,16 +63,25 @@ function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType
63
63
  const mapped = PI_EVENT_MAP[raw as PiHookEventType];
64
64
  if (mapped) return mapped;
65
65
  }
66
- if (cli === "gemini") {
67
- const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType];
68
- if (mapped) return mapped;
69
- }
70
66
  if (cli === "hermes") {
71
67
  // Hermes sends snake_case event names (pre_tool_call, on_session_start, …);
72
68
  // map to PascalCase. Has no turn-end Stop event, so no Stop mapping exists.
73
69
  const mapped = HERMES_EVENT_MAP[raw as HermesHookEventType];
74
70
  if (mapped) return mapped;
75
71
  }
72
+ if (cli === "openclaw") {
73
+ // The openclaw-plugin shim forwards raw snake_case plugin-hook names
74
+ // (before_tool_call, before_agent_finalize, …); map to PascalCase.
75
+ // before_agent_finalize is the real turn-end gate → Stop.
76
+ const mapped = OPENCLAW_EVENT_MAP[raw as OpenClawHookEventType];
77
+ if (mapped) return mapped;
78
+ }
79
+ if (cli === "antigravity") {
80
+ // Antigravity's --hook args are PreToolUse|PostToolUse|PreInvocation|Stop.
81
+ // PreInvocation (before-model) → UserPromptSubmit. Verified agy v1.1.2.
82
+ const mapped = ANTIGRAVITY_EVENT_MAP[raw as AntigravityHookEventType];
83
+ if (mapped) return mapped;
84
+ }
76
85
  // claude / copilot / unknown — already PascalCase, pass through.
77
86
  // HOOK_EVENT_TYPES type-checks downstream.
78
87
  return raw as HookEventType;
@@ -83,239 +92,295 @@ export async function handleHookEvent(
83
92
  cli: IntegrationType = "claude",
84
93
  ): Promise<number> {
85
94
  const startTime = performance.now();
86
-
87
- // Read stdin payload (Claude passes JSON)
88
- const MAX_STDIN_BYTES = 1_048_576; // 1 MB
89
- let payload = "";
90
- let stdinOversized = false;
91
95
  try {
92
- payload = await new Promise<string>((resolve, reject) => {
93
- const chunks: string[] = [];
94
- let totalBytes = 0;
95
- process.stdin.setEncoding("utf8");
96
- process.stdin.on("data", (chunk: string) => {
97
- totalBytes += Buffer.byteLength(chunk);
98
- if (totalBytes > MAX_STDIN_BYTES) {
99
- hookLogWarn(`stdin payload exceeds 1 MB for ${eventType}, discarding`);
100
- stdinOversized = true;
101
- process.stdin.destroy();
102
- resolve("");
103
- return;
104
- }
105
- chunks.push(chunk);
106
- });
107
- process.stdin.on("end", () => resolve(chunks.join("")));
108
- process.stdin.on("error", reject);
109
- // If stdin is already closed or not piped, resolve immediately
110
- if (process.stdin.readableEnded) resolve("");
111
- });
112
- } catch (err) {
113
- hookLogWarn(`stdin read failed for ${eventType}`);
114
- void trackHookEvent(getInstanceId(), "hook_stdin_error", {
115
- event_type: eventType,
116
- cli,
117
- error_type: err instanceof Error ? err.name : "unknown",
118
- });
119
- }
120
- if (stdinOversized) {
121
- void trackHookEvent(getInstanceId(), "hook_stdin_error", {
122
- event_type: eventType,
123
- cli,
124
- error_type: "oversized",
125
- });
126
- }
127
96
 
128
- let parsed: Record<string, unknown> = {};
129
- if (payload) {
97
+ // Read stdin payload (Claude passes JSON)
98
+ const MAX_STDIN_BYTES = 1_048_576; // 1 MB
99
+ let payload = "";
100
+ let stdinOversized = false;
130
101
  try {
131
- parsed = JSON.parse(payload) as Record<string, unknown>;
132
- } catch {
133
- hookLogWarn(`payload parse failed for ${eventType} (${payload.length} bytes)`);
134
- void trackHookEvent(getInstanceId(), "hook_payload_parse_error", {
102
+ payload = await new Promise<string>((resolve, reject) => {
103
+ const chunks: string[] = [];
104
+ let totalBytes = 0;
105
+ process.stdin.setEncoding("utf8");
106
+ process.stdin.on("data", (chunk: string) => {
107
+ totalBytes += Buffer.byteLength(chunk);
108
+ if (totalBytes > MAX_STDIN_BYTES) {
109
+ hookLogWarn(`stdin payload exceeds 1 MB for ${eventType}, discarding`);
110
+ stdinOversized = true;
111
+ process.stdin.destroy();
112
+ resolve("");
113
+ return;
114
+ }
115
+ chunks.push(chunk);
116
+ });
117
+ process.stdin.on("end", () => resolve(chunks.join("")));
118
+ process.stdin.on("error", reject);
119
+ // If stdin is already closed or not piped, resolve immediately
120
+ if (process.stdin.readableEnded) resolve("");
121
+ });
122
+ } catch (err) {
123
+ hookLogWarn(`stdin read failed for ${eventType}`);
124
+ void trackHookEvent(getInstanceId(), "hook_stdin_error", {
135
125
  event_type: eventType,
136
126
  cli,
137
- payload_size: payload.length,
127
+ error_type: err instanceof Error ? err.name : "unknown",
128
+ });
129
+ }
130
+ if (stdinOversized) {
131
+ void trackHookEvent(getInstanceId(), "hook_stdin_error", {
132
+ event_type: eventType,
133
+ cli,
134
+ error_type: "oversized",
138
135
  });
139
136
  }
140
- }
141
-
142
- // Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
143
- const canonicalEventType = canonicalizeEventType(eventType, cli);
144
-
145
- // Canonicalize tool name in place so both the policy-registry tool-name
146
- // filter and policy bodies (`ctx.toolName === "Bash"`) see the canonical
147
- // form. Mutating `parsed.tool_name` keeps the activity store + telemetry
148
- // tagging consistent (they read from `parsed.tool_name`).
149
- const rawToolName = parsed.tool_name as string | undefined;
150
- const canonicalToolName = canonicalizeToolName(rawToolName, cli);
151
- if (canonicalToolName !== rawToolName) {
152
- parsed.tool_name = canonicalToolName;
153
- }
154
-
155
- // Canonicalize tool-input keys for OpenCode + Pi (no-op for other CLIs).
156
- // Defense-in-depth against stale shims that still pass camelCase /
157
- // Pi-shape keys to the binary. The per-CLI shim ALSO canonicalizes; both
158
- // passes are idempotent because the camelCase keys won't match a
159
- // snake_case input.
160
- const rawInput = parsed.tool_input;
161
- const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
162
- if (canonicalInput !== rawInput) {
163
- parsed.tool_input = canonicalInput;
164
- }
165
-
166
- // Extract session metadata from payload
167
- const sessionId = parsed.session_id as string | undefined;
168
- const session: SessionMetadata = {
169
- sessionId,
170
- transcriptPath: resolveTranscriptPath(cli, parsed, sessionId),
171
- cwd: resolveCwd(cli, parsed),
172
- permissionMode: resolvePermissionMode(cli, parsed, sessionId),
173
- hookEventName: parsed.hook_event_name as string | undefined,
174
- // Preserve the raw CLI-side event name (eventType arg) before
175
- // canonicalization. Response shapes that round-trip the agent-emitted
176
- // event name (e.g. Gemini's `hookSpecificOutput.hookEventName`) prefer
177
- // this over the canonicalized form when stdin omits hook_event_name.
178
- rawHookEventName: eventType,
179
- cli,
180
- };
181
-
182
- // Load enabled policies (merge across project/local/global scopes)
183
- const config = readMergedHooksConfig(session.cwd);
184
- clearPolicies();
185
- registerBuiltinPolicies(config.enabledPolicies);
186
-
187
- // Load and register custom hooks (layer 2, after builtins)
188
- const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
189
- const customHooksList = loadResult.hooks;
190
- const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
191
137
 
192
- for (const hook of customHooksList) {
193
- const hookName = hook.name;
194
- const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
195
- const isConvention = !!conventionScope;
196
- const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
197
- const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
138
+ let parsed: Record<string, unknown> = {};
139
+ if (payload) {
198
140
  try {
199
- const result = await Promise.race([
200
- hook.fn(ctx),
201
- new Promise<PolicyResult>((_, reject) =>
202
- setTimeout(() => reject(new Error("timeout")), 10_000),
203
- ),
204
- ]);
205
- return result;
206
- } catch (err) {
207
- const msg = err instanceof Error ? err.message : String(err);
208
- const isTimeout = msg === "timeout";
209
- hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
210
- void trackHookEvent(getInstanceId(), "custom_hook_error", {
211
- hook_name: hookName,
212
- error_type: isTimeout ? "timeout" : "exception",
141
+ parsed = JSON.parse(payload) as Record<string, unknown>;
142
+ } catch {
143
+ hookLogWarn(`payload parse failed for ${eventType} (${payload.length} bytes)`);
144
+ void trackHookEvent(getInstanceId(), "hook_payload_parse_error", {
213
145
  event_type: eventType,
214
146
  cli,
215
- is_convention_policy: isConvention,
216
- convention_scope: conventionScope ?? null,
147
+ payload_size: payload.length,
217
148
  });
218
- return { decision: "allow" };
219
149
  }
220
- };
221
- registerPolicy(
222
- `${prefix}/${hookName}`,
223
- hook.description ?? "",
224
- fn,
225
- hook.match ?? {},
226
- -1, // Custom hooks run after builtins (priority 0)
227
- );
228
- }
150
+ }
229
151
 
230
- // Fire telemetry once per invocation for custom hook loads
231
- if (customHooksList.length > 0) {
232
- void trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
233
- cli,
234
- custom_hooks_count: customHooksList.length,
235
- custom_hook_names: customHooksList.map((h) => h.name),
236
- event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))],
237
- });
238
- }
152
+ // Antigravity (agy) pipes a camelCase protojson payload; normalize the fields
153
+ // the handler downstream reads to canonical snake_case BEFORE any
154
+ // canonicalization runs. `toolCall.{name,args}` → `tool_name`/`tool_input`,
155
+ // `conversationId` → `session_id`, `workspacePaths[0]` → `cwd`,
156
+ // `transcriptPath` → `transcript_path`. Verified against agy v1.1.2.
157
+ if (cli === "antigravity") {
158
+ const tc = parsed.toolCall as { name?: string; args?: unknown } | undefined;
159
+ if (tc && typeof tc === "object") {
160
+ if (tc.name !== undefined) parsed.tool_name = tc.name;
161
+ if (tc.args !== undefined) parsed.tool_input = tc.args;
162
+ }
163
+ if (typeof parsed.conversationId === "string") parsed.session_id = parsed.conversationId;
164
+ if (Array.isArray(parsed.workspacePaths) && typeof parsed.workspacePaths[0] === "string") {
165
+ parsed.cwd = parsed.workspacePaths[0];
166
+ }
167
+ if (typeof parsed.transcriptPath === "string") parsed.transcript_path = parsed.transcriptPath;
168
+ }
169
+
170
+ // Copilot's snake_case events (PreToolUse/PostToolUse/Stop/…) are already
171
+ // Claude-shaped, but `permissionRequest` alone pipes a camelCase payload
172
+ // (`hookName`, `sessionId`, `toolName` in lowercase, `toolInput`) — verified
173
+ // live against Copilot CLI 1.0.71. Normalize the fields the handler reads so
174
+ // PermissionRequest-matched policies (e.g. block-sudo's Codex-escalation
175
+ // guard) fire instead of seeing a null tool name.
176
+ if (cli === "copilot") {
177
+ if (typeof parsed.toolName === "string" && parsed.tool_name === undefined) {
178
+ parsed.tool_name = parsed.toolName;
179
+ }
180
+ if (parsed.toolInput !== undefined && parsed.tool_input === undefined) {
181
+ parsed.tool_input = parsed.toolInput;
182
+ }
183
+ if (typeof parsed.sessionId === "string" && parsed.session_id === undefined) {
184
+ parsed.session_id = parsed.sessionId;
185
+ }
186
+ }
187
+
188
+ // Goose pipes `event` / `working_dir` instead of Claude's `hook_event_name` /
189
+ // `cwd` (its `tool_name` / `tool_input` are already canonical field names).
190
+ // Normalize both so resolveCwd keeps its cwd (block-read-outside-cwd) and the
191
+ // round-tripped event name is available. The --hook arg is already PascalCase,
192
+ // so canonicalizeEventType needs no goose branch. Verified goose v1.43.0.
193
+ if (cli === "goose") {
194
+ if (typeof parsed.working_dir === "string") parsed.cwd = parsed.working_dir;
195
+ if (typeof parsed.event === "string" && parsed.hook_event_name === undefined) {
196
+ parsed.hook_event_name = parsed.event;
197
+ }
198
+ }
199
+
200
+ // Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
201
+ const canonicalEventType = canonicalizeEventType(eventType, cli);
202
+
203
+ // Canonicalize tool name in place so both the policy-registry tool-name
204
+ // filter and policy bodies (`ctx.toolName === "Bash"`) see the canonical
205
+ // form. Mutating `parsed.tool_name` keeps the activity store + telemetry
206
+ // tagging consistent (they read from `parsed.tool_name`).
207
+ const rawToolName = parsed.tool_name as string | undefined;
208
+ const canonicalToolName = canonicalizeToolName(rawToolName, cli);
209
+ if (canonicalToolName !== rawToolName) {
210
+ parsed.tool_name = canonicalToolName;
211
+ }
212
+
213
+ // Canonicalize tool-input keys for OpenCode + Pi (no-op for other CLIs).
214
+ // Defense-in-depth against stale shims that still pass camelCase /
215
+ // Pi-shape keys to the binary. The per-CLI shim ALSO canonicalizes; both
216
+ // passes are idempotent because the camelCase keys won't match a
217
+ // snake_case input.
218
+ const rawInput = parsed.tool_input;
219
+ const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
220
+ if (canonicalInput !== rawInput) {
221
+ parsed.tool_input = canonicalInput;
222
+ }
239
223
 
240
- // Fire telemetry for convention-based policy discovery
241
- if (loadResult.conventionSources.length > 0) {
242
- void trackHookEvent(getInstanceId(), "convention_policies_loaded", {
243
- event_type: canonicalEventType,
224
+ // Extract session metadata from payload
225
+ const sessionId = parsed.session_id as string | undefined;
226
+ const session: SessionMetadata = {
227
+ sessionId,
228
+ transcriptPath: resolveTranscriptPath(cli, parsed, sessionId),
229
+ cwd: resolveCwd(cli, parsed),
230
+ permissionMode: resolvePermissionMode(cli, parsed, sessionId),
231
+ hookEventName: parsed.hook_event_name as string | undefined,
232
+ // Preserve the raw CLI-side event name (eventType arg) before
233
+ // canonicalization. Response shapes that round-trip the agent-emitted
234
+ // event name prefer this over the canonicalized form when stdin omits
235
+ // hook_event_name.
236
+ rawHookEventName: eventType,
244
237
  cli,
245
- project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
246
- user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
247
- convention_hook_count: conventionHookNames.size,
248
- convention_hook_names: [...conventionHookNames],
249
- });
250
- }
238
+ };
251
239
 
252
- hookLogInfo(`event=${canonicalEventType} cli=${cli} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
240
+ // Load enabled policies (merge across project/local/global scopes)
241
+ const config = readMergedHooksConfig(session.cwd);
242
+ clearPolicies();
243
+ registerBuiltinPolicies(config.enabledPolicies);
253
244
 
254
- // Evaluate policies (use canonical PascalCase event type)
255
- const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
256
- const durationMs = Math.round(performance.now() - startTime);
257
- hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
245
+ // Load and register custom hooks (layer 2, after builtins)
246
+ const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
247
+ const customHooksList = loadResult.hooks;
248
+ const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
258
249
 
259
- if (result.stdout) {
260
- process.stdout.write(result.stdout);
261
- }
262
- if (result.stderr) {
263
- process.stderr.write(result.stderr);
264
- }
250
+ for (const hook of customHooksList) {
251
+ const hookName = hook.name;
252
+ const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
253
+ const isConvention = !!conventionScope;
254
+ const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
255
+ const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
256
+ try {
257
+ const result = await Promise.race([
258
+ hook.fn(ctx),
259
+ new Promise<PolicyResult>((_, reject) =>
260
+ setTimeout(() => reject(new Error("timeout")), 10_000),
261
+ ),
262
+ ]);
263
+ return result;
264
+ } catch (err) {
265
+ const msg = err instanceof Error ? err.message : String(err);
266
+ const isTimeout = msg === "timeout";
267
+ hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
268
+ void trackHookEvent(getInstanceId(), "custom_hook_error", {
269
+ hook_name: hookName,
270
+ error_type: isTimeout ? "timeout" : "exception",
271
+ event_type: eventType,
272
+ cli,
273
+ is_convention_policy: isConvention,
274
+ convention_scope: conventionScope ?? null,
275
+ });
276
+ return { decision: "allow" };
277
+ }
278
+ };
279
+ registerPolicy(
280
+ `${prefix}/${hookName}`,
281
+ hook.description ?? "",
282
+ fn,
283
+ hook.match ?? {},
284
+ -1, // Custom hooks run after builtins (priority 0)
285
+ );
286
+ }
265
287
 
266
- // Persist activity to disk (visible in /policies activity tab)
267
- const activityEntry = {
268
- timestamp: Date.now(),
269
- eventType: canonicalEventType,
270
- integration: cli,
271
- toolName: (parsed.tool_name as string) ?? null,
272
- policyName: result.policyName,
273
- policyNames: result.policyNames,
274
- decision: result.decision,
275
- reason: result.reason,
276
- durationMs,
277
- sessionId: session.sessionId,
278
- transcriptPath: session.transcriptPath,
279
- cwd: session.cwd,
280
- permissionMode: session.permissionMode,
281
- hookEventName: session.hookEventName,
282
- };
283
- try {
284
- persistHookActivity(activityEntry);
285
- } catch {
286
- hookLogWarn("activity persistence failed");
287
- }
288
+ // Fire telemetry once per invocation for custom hook loads
289
+ if (customHooksList.length > 0) {
290
+ void trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
291
+ cli,
292
+ custom_hooks_count: customHooksList.length,
293
+ custom_hook_names: customHooksList.map((h) => h.name),
294
+ event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))],
295
+ });
296
+ }
288
297
 
289
- // Fire PostHog telemetry for decisions that affect Claude's behavior
290
- if (result.decision === "deny" || result.decision === "instruct") {
291
- try {
292
- const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
293
- const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
294
- const conventionScope = isConventionPolicy
295
- ? result.policyName!.match(/^\.failproofai-(project|user)\//)?.[1] ?? null
296
- : null;
297
- const hasCustomParams =
298
- !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
299
- const paramKeysOverridden = hasCustomParams
300
- ? Object.keys(config.policyParams![result.policyName!])
301
- : [];
302
- const distinctId = getInstanceId();
303
- await trackHookEvent(distinctId, "hook_policy_triggered", {
298
+ // Fire telemetry for convention-based policy discovery
299
+ if (loadResult.conventionSources.length > 0) {
300
+ void trackHookEvent(getInstanceId(), "convention_policies_loaded", {
304
301
  event_type: canonicalEventType,
305
302
  cli,
306
- tool_name: (parsed.tool_name as string) ?? null,
307
- policy_name: result.policyName,
308
- decision: result.decision,
309
- is_custom_hook: isCustomHook,
310
- is_convention_policy: isConventionPolicy,
311
- convention_scope: conventionScope,
312
- has_custom_params: hasCustomParams,
313
- param_keys_overridden: paramKeysOverridden,
303
+ project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
304
+ user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
305
+ convention_hook_count: conventionHookNames.size,
306
+ convention_hook_names: [...conventionHookNames],
314
307
  });
308
+ }
309
+
310
+ hookLogInfo(`event=${canonicalEventType} cli=${cli} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
311
+
312
+ // Evaluate policies (use canonical PascalCase event type)
313
+ const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
314
+ const durationMs = Math.round(performance.now() - startTime);
315
+ hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
316
+
317
+ if (result.stdout) {
318
+ process.stdout.write(result.stdout);
319
+ }
320
+ if (result.stderr) {
321
+ process.stderr.write(result.stderr);
322
+ }
323
+
324
+ // Persist activity to disk (visible in /policies activity tab)
325
+ const activityEntry = {
326
+ timestamp: Date.now(),
327
+ eventType: canonicalEventType,
328
+ integration: cli,
329
+ toolName: (parsed.tool_name as string) ?? null,
330
+ policyName: result.policyName,
331
+ policyNames: result.policyNames,
332
+ decision: result.decision,
333
+ reason: result.reason,
334
+ durationMs,
335
+ sessionId: session.sessionId,
336
+ transcriptPath: session.transcriptPath,
337
+ cwd: session.cwd,
338
+ permissionMode: session.permissionMode,
339
+ hookEventName: session.hookEventName,
340
+ };
341
+ try {
342
+ persistHookActivity(activityEntry);
315
343
  } catch {
316
- // Telemetry is best-effort — never block the hook
344
+ hookLogWarn("activity persistence failed");
317
345
  }
318
- }
319
346
 
320
- return result.exitCode;
347
+ // Fire PostHog telemetry for decisions that affect Claude's behavior
348
+ if (result.decision === "deny" || result.decision === "instruct") {
349
+ try {
350
+ const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
351
+ const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
352
+ const conventionScope = isConventionPolicy
353
+ ? result.policyName!.match(/^\.failproofai-(project|user)\//)?.[1] ?? null
354
+ : null;
355
+ const hasCustomParams =
356
+ !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
357
+ const paramKeysOverridden = hasCustomParams
358
+ ? Object.keys(config.policyParams![result.policyName!])
359
+ : [];
360
+ const distinctId = getInstanceId();
361
+ await trackHookEvent(distinctId, "hook_policy_triggered", {
362
+ event_type: canonicalEventType,
363
+ cli,
364
+ tool_name: (parsed.tool_name as string) ?? null,
365
+ policy_name: result.policyName,
366
+ decision: result.decision,
367
+ is_custom_hook: isCustomHook,
368
+ is_convention_policy: isConventionPolicy,
369
+ convention_scope: conventionScope,
370
+ has_custom_params: hasCustomParams,
371
+ param_keys_overridden: paramKeysOverridden,
372
+ });
373
+ } catch {
374
+ // Telemetry is best-effort — never block the hook
375
+ }
376
+ }
377
+ return result.exitCode;
378
+ } finally {
379
+ // Await any un-awaited (`void trackHookEvent(...)`) events fired during
380
+ // this invocation. bin/failproofai.mjs calls process.exit() the moment we
381
+ // return OR throw, which would otherwise drop in-flight POSTs — notably on
382
+ // the allow path (no trailing awaited event) and on any early throw
383
+ // (custom-hook load / policy eval) before the happy path is reached.
384
+ await flushHookTelemetry();
385
+ }
321
386
  }
@@ -11,13 +11,23 @@ import { POSTHOG_API_KEY, POSTHOG_PRODUCT } from "../posthog-key";
11
11
  const API_KEY = POSTHOG_API_KEY;
12
12
  const CAPTURE_URL = "https://us.i.posthog.com/capture/";
13
13
 
14
- export async function trackHookEvent(
14
+ /**
15
+ * In-flight telemetry POSTs. The hook binary is short-lived — `bin/failproofai.mjs`
16
+ * calls `process.exit()` the moment `handleHookEvent` returns, which kills any
17
+ * fetch that a caller fired with `void trackHookEvent(...)` (i.e. did not await).
18
+ * We track every send here so `flushHookTelemetry()` can await them all at the
19
+ * process-exit boundary, making delivery reliable regardless of whether the
20
+ * individual call site awaited. Without this, un-awaited events (custom_hooks_loaded,
21
+ * convention_policies_loaded, the *_error events) are dropped on the common
22
+ * allow path, since no trailing await holds the event loop open.
23
+ */
24
+ const pending = new Set<Promise<void>>();
25
+
26
+ async function sendEvent(
15
27
  distinctId: string,
16
28
  event: string,
17
29
  properties?: Record<string, unknown>,
18
30
  ): Promise<void> {
19
- if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1") return;
20
-
21
31
  const body = JSON.stringify({
22
32
  api_key: process.env.FAILPROOFAI_POSTHOG_KEY ?? API_KEY,
23
33
  event,
@@ -41,3 +51,33 @@ export async function trackHookEvent(
41
51
  // Telemetry is best-effort — never fail the hook
42
52
  }
43
53
  }
54
+
55
+ export async function trackHookEvent(
56
+ distinctId: string,
57
+ event: string,
58
+ properties?: Record<string, unknown>,
59
+ ): Promise<void> {
60
+ if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1") return;
61
+
62
+ const p = sendEvent(distinctId, event, properties);
63
+ pending.add(p);
64
+ // Deregister on settle so the set doesn't grow unbounded in long-lived
65
+ // callers (tests, the dashboard server). `sendEvent` never rejects, but
66
+ // `.finally` is defensive.
67
+ void p.finally(() => pending.delete(p));
68
+ await p;
69
+ }
70
+
71
+ /**
72
+ * Await every in-flight telemetry POST. Call this immediately before
73
+ * `process.exit()` on any short-lived path so `void trackHookEvent(...)` events
74
+ * are delivered instead of being cut off by the exit. No-op (resolves instantly)
75
+ * when nothing is pending. Never throws — `sendEvent` swallows its own errors.
76
+ */
77
+ export async function flushHookTelemetry(): Promise<void> {
78
+ // Loop in case a settling promise's `.finally` races a newly-added send;
79
+ // in practice one pass suffices because no send spawns another.
80
+ while (pending.size > 0) {
81
+ await Promise.allSettled([...pending]);
82
+ }
83
+ }