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
@@ -26,10 +26,18 @@ import {
26
26
  OPENCODE_HOOK_SCOPES,
27
27
  PI_HOOK_EVENT_TYPES,
28
28
  PI_HOOK_SCOPES,
29
- GEMINI_HOOK_EVENT_TYPES,
30
- GEMINI_HOOK_SCOPES,
31
29
  HERMES_HOOK_EVENT_TYPES,
32
30
  HERMES_HOOK_SCOPES,
31
+ OPENCLAW_HOOK_EVENT_TYPES,
32
+ OPENCLAW_HOOK_SCOPES,
33
+ FACTORY_HOOK_EVENT_TYPES,
34
+ FACTORY_HOOK_SCOPES,
35
+ DEVIN_HOOK_EVENT_TYPES,
36
+ DEVIN_HOOK_SCOPES,
37
+ ANTIGRAVITY_HOOK_EVENT_TYPES,
38
+ ANTIGRAVITY_HOOK_SCOPES,
39
+ GOOSE_HOOK_EVENT_TYPES,
40
+ GOOSE_HOOK_SCOPES,
33
41
  FAILPROOFAI_HOOK_MARKER,
34
42
  INTEGRATION_TYPES,
35
43
  type IntegrationType,
@@ -310,8 +318,7 @@ export const codex: Integration = {
310
318
  type: "command",
311
319
  // Codex reads `timeout` in SECONDS (the field is literally `timeout`,
312
320
  // default 600 per https://developers.openai.com/codex/hooks) — same unit as
313
- // Claude/Cursor/Copilot, and unlike Gemini, whose `timeout` is in
314
- // milliseconds (default 60000). 60 = 60s, not 60000ms (~16.7h).
321
+ // Claude/Cursor/Copilot. 60 = 60s.
315
322
  command,
316
323
  timeout: 60,
317
324
  [FAILPROOFAI_HOOK_MARKER]: true,
@@ -1286,61 +1293,456 @@ function makePiProjectRelativeEntry(extPath: string): string {
1286
1293
  // works for the local user.
1287
1294
  return extResolved;
1288
1295
  }
1289
- // ── Gemini CLI integration ──────────────────────────────────────────────────
1290
- //
1291
- // Gemini's hook contract is the closest thing to a Claude Code clone we've
1292
- // shipped: same `{matcher, hooks: [{type, command, timeout}]}` settings shape,
1293
- // PascalCase event names, snake_case stdin payload field names (session_id,
1294
- // tool_name, tool_input, hook_event_name, cwd, transcript_path), subprocess
1295
- // execution model, and `$CLAUDE_PROJECT_DIR` env-var alias on top of its own
1296
- // `$GEMINI_PROJECT_DIR`. The integration is structurally identical to
1297
- // claudeCode below, with three deltas:
1298
- //
1299
- // • Settings paths: ~/.gemini/settings.json (user) / <cwd>/.gemini/settings.json (project).
1300
- // System scope (/etc/gemini-cli/settings.json) is documented but not exposed.
1301
- //
1302
- // • Matcher field: each Gemini matcher entry carries an explicit `matcher`
1303
- // regex (e.g. `"write_file|replace"`). We default to `"*"` so policies fire
1304
- // on every tool call, mirroring the failproofai default of "every event,
1305
- // every tool". Users can hand-edit settings.json to scope tighter; we
1306
- // preserve their `matcher` field across re-installs by NOT replacing
1307
- // entries that aren't failproofai-marked.
1308
- //
1309
- // • Tool name canonicalization happens in handler.ts (snake_case →
1310
- // PascalCase via GEMINI_TOOL_MAP) so policies match unchanged; not the
1311
- // install layer's concern.
1296
+ // ── Hermes (hermes-agent) live hooks (Pillar 1) ────────────────────────────
1312
1297
  //
1313
- // Detected via the `gemini` binary on PATH.
1298
+ // External-command CLI like codex/cursor, but its config is YAML
1299
+ // (`~/.hermes/config.yaml`) under a `hooks:` map, so the I/O layer uses the yaml
1300
+ // Document API (comment-preserving). Flat per-event arrays like cursor:
1301
+ // `hooks: { pre_tool_call: [ { command, timeout, <marker> } ], … }`. Hermes
1302
+ // reads a `{"decision":"block",…}` JSON response on stdout (see
1303
+ // policy-evaluator.ts); exit codes are ignored. User-scope only.
1304
+
1305
+ /** One hook entry as stored under a `hooks:` event key in config.yaml. */
1306
+ interface HermesHookEntry {
1307
+ command: string;
1308
+ timeout?: number;
1309
+ [key: string]: unknown;
1310
+ }
1311
+
1312
+ export const hermes: Integration = {
1313
+ id: "hermes",
1314
+ displayName: "Hermes",
1315
+ scopes: HERMES_HOOK_SCOPES,
1316
+ eventTypes: HERMES_HOOK_EVENT_TYPES,
1317
+
1318
+ // Hermes config is USER-scope only (`~/.hermes/config.yaml`); there is no
1319
+ // project/local file, so scope/cwd are irrelevant here.
1320
+ getSettingsPath() {
1321
+ return resolve(homedir(), ".hermes", "config.yaml");
1322
+ },
1323
+
1324
+ readSettings(settingsPath) {
1325
+ // Return the yaml Document (cast) so writeHookEntries/writeSettings can
1326
+ // round-trip it while preserving the user's other keys + comments.
1327
+ return readYamlDoc(settingsPath) as unknown as Record<string, unknown>;
1328
+ },
1329
+
1330
+ writeSettings(settingsPath, settings) {
1331
+ writeYamlDoc(settingsPath, settings as unknown as Document);
1332
+ },
1333
+
1334
+ buildHookEntry(binaryPath, eventType, scope) {
1335
+ // No matcher → fires for ALL tools / all platforms (slack/telegram/cli/cron)
1336
+ // and internal subagents. `timeout` is in seconds; Hermes runs the command
1337
+ // via shlex.split (shell=false).
1338
+ const command =
1339
+ scope === "project"
1340
+ ? `npx -y failproofai --hook ${eventType} --cli hermes`
1341
+ : `"${binaryPath}" --hook ${eventType} --cli hermes`;
1342
+ return {
1343
+ command,
1344
+ timeout: 30,
1345
+ [FAILPROOFAI_HOOK_MARKER]: true,
1346
+ };
1347
+ },
1348
+
1349
+ isFailproofaiHook: isMarkedHook,
1350
+
1351
+ writeHookEntries(settings, binaryPath, scope) {
1352
+ const doc = settings as unknown as Document;
1353
+ // Read the current hooks map as plain JS, then re-set ONLY the `hooks` key —
1354
+ // preserving comments on every other part of config.yaml.
1355
+ const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1356
+ const hooks: Record<string, HermesHookEntry[]> =
1357
+ js.hooks && typeof js.hooks === "object" ? js.hooks : {};
1358
+
1359
+ for (const eventType of HERMES_HOOK_EVENT_TYPES) {
1360
+ const entry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as HermesHookEntry;
1361
+ const arr = Array.isArray(hooks[eventType]) ? hooks[eventType] : [];
1362
+ const idx = arr.findIndex((h) => isMarkedHook(h));
1363
+ if (idx >= 0) arr[idx] = entry;
1364
+ else arr.push(entry);
1365
+ hooks[eventType] = arr;
1366
+ }
1367
+ doc.set("hooks", hooks);
1368
+ // The headless gateway has no TTY to answer Hermes's first-use hook-consent
1369
+ // prompt, so auto-accept declared hooks. Tradeoff: also auto-accepts any
1370
+ // other hook the operator adds; a targeted `shell-hooks-allowlist.json`
1371
+ // pre-seed is a future refinement.
1372
+ doc.set("hooks_auto_accept", true);
1373
+ },
1374
+
1375
+ removeHooksFromFile(settingsPath) {
1376
+ if (!existsSync(settingsPath)) return 0;
1377
+ const doc = readYamlDoc(settingsPath);
1378
+ const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1379
+ const hooks = js.hooks;
1380
+
1381
+ let removed = 0;
1382
+ if (hooks && typeof hooks === "object") {
1383
+ for (const eventType of Object.keys(hooks)) {
1384
+ const entries = hooks[eventType];
1385
+ if (!Array.isArray(entries)) continue;
1386
+ const before = entries.length;
1387
+ const filtered = entries.filter((h) => !isMarkedHook(h));
1388
+ removed += before - filtered.length;
1389
+ if (filtered.length === 0) delete hooks[eventType];
1390
+ else hooks[eventType] = filtered;
1391
+ }
1392
+ if (removed > 0) {
1393
+ if (Object.keys(hooks).length === 0) doc.delete("hooks");
1394
+ else doc.set("hooks", hooks);
1395
+ }
1396
+ }
1397
+
1398
+ // Always drop our headless-consent flag on uninstall — even if the hooks were
1399
+ // already removed manually — so it can't silently auto-accept future operator
1400
+ // hooks. `doc.delete` returns true iff the key was present.
1401
+ const droppedAutoAccept = doc.delete("hooks_auto_accept");
1402
+ if (removed > 0 || droppedAutoAccept) writeYamlDoc(settingsPath, doc);
1403
+ return removed;
1404
+ },
1405
+
1406
+ hooksInstalledInSettings(scope, cwd) {
1407
+ const settingsPath = this.getSettingsPath(scope, cwd);
1408
+ if (!existsSync(settingsPath)) return false;
1409
+ try {
1410
+ const doc = readYamlDoc(settingsPath);
1411
+ const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1412
+ const hooks = js.hooks;
1413
+ if (!hooks || typeof hooks !== "object") return false;
1414
+ for (const entries of Object.values(hooks)) {
1415
+ if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true;
1416
+ }
1417
+ } catch {
1418
+ // Corrupt config — treat as not installed.
1419
+ }
1420
+ return false;
1421
+ },
1422
+
1423
+ detectInstalled() {
1424
+ return binaryExists("hermes");
1425
+ },
1426
+ };
1427
+
1428
+ // ── OpenClaw integration ────────────────────────────────────────────────────
1314
1429
  //
1315
- // Ref: https://geminicli.com/docs/hooks/
1430
+ // OpenClaw is a self-hosted assistant gateway. Enforcement is via its in-process
1431
+ // PLUGIN hooks (file-based "internal hooks" are observation-only), so failproofai
1432
+ // ships a static plugin package (`openclaw-plugin/`, like Pi's pi-extension) that
1433
+ // async-spawns the binary and maps verdicts back. Install registers the plugin in
1434
+ // `~/.openclaw/openclaw.json` (JSON):
1435
+ // • plugins.load.paths[] → the shipped openclaw-plugin dir (absolute path)
1436
+ // • plugins.entries.failproofai = { enabled: true, hooks: { allowConversationAccess: true } }
1437
+ // (allowConversationAccess is required for the raw-conversation hooks
1438
+ // before_agent_run / before_agent_finalize; verified live v2026.7.1).
1439
+ // USER scope only — OpenClaw has no project config (workspace plugins are
1440
+ // disabled by default). We NEVER delete the shipped plugin dir on uninstall
1441
+ // (unlike OpenCode's generated shim) — uninstall only edits openclaw.json.
1442
+ // Detected via the `openclaw` binary on PATH.
1443
+
1444
+ const OPENCLAW_PLUGIN_ID = "failproofai";
1445
+
1446
+ interface OpenClawPluginsSection {
1447
+ load?: { paths?: string[]; [k: string]: unknown };
1448
+ entries?: Record<string, Record<string, unknown>>;
1449
+ allow?: string[];
1450
+ deny?: string[];
1451
+ [k: string]: unknown;
1452
+ }
1453
+ interface OpenClawSettingsFile {
1454
+ plugins?: OpenClawPluginsSection;
1455
+ [k: string]: unknown;
1456
+ }
1316
1457
 
1317
- interface GeminiHookMatcher {
1318
- matcher?: string;
1319
- hooks?: Array<ClaudeHookEntry | Record<string, unknown>>;
1458
+ /** Absolute path to the failproofai-shipped OpenClaw plugin package. */
1459
+ function getOpenClawPluginPath(): string {
1460
+ const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
1461
+ if (fromEnv) return resolve(fromEnv, "openclaw-plugin");
1462
+ return resolve(fileURLToPath(import.meta.url), "..", "..", "..", "openclaw-plugin");
1320
1463
  }
1321
1464
 
1322
- interface GeminiSettingsFile {
1323
- hooks?: Record<string, GeminiHookMatcher[]>;
1324
- [key: string]: unknown;
1465
+ /** True iff a plugins.load.paths entry was written by failproofai. */
1466
+ function isFailproofaiOpenClawPath(p: unknown): boolean {
1467
+ if (typeof p !== "string") return false;
1468
+ return /(?:^|\/)openclaw-plugin\/?$/.test(p) || (p.includes("openclaw-plugin") && p.includes("failproofai"));
1325
1469
  }
1326
1470
 
1327
- export const gemini: Integration = {
1328
- id: "gemini",
1329
- displayName: "Gemini CLI",
1330
- scopes: GEMINI_HOOK_SCOPES,
1331
- eventTypes: GEMINI_HOOK_EVENT_TYPES,
1471
+ export const openclaw: Integration = {
1472
+ id: "openclaw",
1473
+ displayName: "OpenClaw",
1474
+ scopes: OPENCLAW_HOOK_SCOPES,
1475
+ eventTypes: OPENCLAW_HOOK_EVENT_TYPES,
1476
+
1477
+ // USER scope only (~/.openclaw/openclaw.json); OpenClaw has no project config.
1478
+ getSettingsPath() {
1479
+ return resolve(homedir(), ".openclaw", "openclaw.json");
1480
+ },
1481
+
1482
+ readSettings(settingsPath) {
1483
+ return readJsonFile(settingsPath);
1484
+ },
1485
+
1486
+ writeSettings(settingsPath, settings) {
1487
+ writeJsonFile(settingsPath, settings);
1488
+ },
1489
+
1490
+ buildHookEntry() {
1491
+ // OpenClaw registers plugins at the package level — one registration covers
1492
+ // all hooks (the plugin's index.js wires every api.on(...) handler). This
1493
+ // sentinel satisfies the interface; writeHookEntries does the real work.
1494
+ return {
1495
+ [FAILPROOFAI_HOOK_MARKER]: true,
1496
+ _openclawPluginPath: getOpenClawPluginPath(),
1497
+ };
1498
+ },
1499
+
1500
+ isFailproofaiHook(hook) {
1501
+ if (typeof hook === "string") return isFailproofaiOpenClawPath(hook);
1502
+ if (!hook || typeof hook !== "object") return false;
1503
+ const h = hook as Record<string, unknown>;
1504
+ if (h[FAILPROOFAI_HOOK_MARKER] === true) return true;
1505
+ if (typeof h.source === "string") return isFailproofaiOpenClawPath(h.source);
1506
+ return false;
1507
+ },
1508
+
1509
+ writeHookEntries(settings) {
1510
+ const s = settings as OpenClawSettingsFile;
1511
+ const plugins = (s.plugins ??= {});
1512
+ const load = (plugins.load ??= {});
1513
+ if (!Array.isArray(load.paths)) load.paths = [];
1514
+ const dir = getOpenClawPluginPath();
1515
+ // Idempotent: replace any existing failproofai path, otherwise append.
1516
+ const idx = load.paths.findIndex((p) => isFailproofaiOpenClawPath(p));
1517
+ if (idx >= 0) load.paths[idx] = dir;
1518
+ else load.paths.push(dir);
1519
+
1520
+ // Enable the plugin + grant conversation access (needed for before_agent_run
1521
+ // / before_agent_finalize). Only set our own keys — operator config on this
1522
+ // entry (e.g. hooks.timeouts.<hookName>) and other entries are preserved.
1523
+ const entries = (plugins.entries ??= {});
1524
+ const entry = (entries[OPENCLAW_PLUGIN_ID] ??= {});
1525
+ entry.enabled = true;
1526
+ const hooks = ((entry.hooks as Record<string, unknown>) ??= {});
1527
+ hooks.allowConversationAccess = true;
1528
+ },
1529
+
1530
+ removeHooksFromFile(settingsPath) {
1531
+ if (!existsSync(settingsPath)) return 0;
1532
+ const settings = this.readSettings(settingsPath) as OpenClawSettingsFile;
1533
+ const plugins = settings.plugins;
1534
+ if (!plugins) return 0;
1535
+ let removed = 0;
1536
+
1537
+ if (Array.isArray(plugins.load?.paths)) {
1538
+ const before = plugins.load.paths.length;
1539
+ plugins.load.paths = plugins.load.paths.filter((p) => !isFailproofaiOpenClawPath(p));
1540
+ removed += before - plugins.load.paths.length;
1541
+ if (plugins.load.paths.length === 0) delete plugins.load.paths;
1542
+ if (plugins.load && Object.keys(plugins.load).length === 0) delete plugins.load;
1543
+ }
1544
+ if (plugins.entries && OPENCLAW_PLUGIN_ID in plugins.entries) {
1545
+ delete plugins.entries[OPENCLAW_PLUGIN_ID];
1546
+ removed += 1;
1547
+ if (Object.keys(plugins.entries).length === 0) delete plugins.entries;
1548
+ }
1549
+ // Prune an empty plugins object so uninstall leaves openclaw.json clean.
1550
+ if (Object.keys(plugins).length === 0) delete settings.plugins;
1551
+
1552
+ this.writeSettings(settingsPath, settings as Record<string, unknown>);
1553
+ return removed;
1554
+ },
1555
+
1556
+ hooksInstalledInSettings(scope, cwd) {
1557
+ const settingsPath = this.getSettingsPath(scope, cwd);
1558
+ if (!existsSync(settingsPath)) return false;
1559
+ try {
1560
+ const settings = this.readSettings(settingsPath) as OpenClawSettingsFile;
1561
+ const paths = settings.plugins?.load?.paths;
1562
+ const pathPresent = Array.isArray(paths) && paths.some((p) => isFailproofaiOpenClawPath(p));
1563
+ const entryEnabled = settings.plugins?.entries?.[OPENCLAW_PLUGIN_ID]?.enabled === true;
1564
+ return pathPresent && entryEnabled;
1565
+ } catch {
1566
+ return false;
1567
+ }
1568
+ },
1569
+
1570
+ detectInstalled() {
1571
+ return binaryExists("openclaw");
1572
+ },
1573
+ };
1574
+
1575
+ // ── Factory Droid (droid) integration ───────────────────────────────────────
1576
+ //
1577
+ // Factory's droid CLI uses a Claude-compatible external-command hook system,
1578
+ // but its hooks.json puts event names at the TOP LEVEL — there is NO `"hooks"`
1579
+ // wrapper (the file IS the events object). Verified live against droid
1580
+ // v0.171.0: a `{"hooks":{…}}` wrapper is rejected with `WARN Ignoring unknown
1581
+ // hook event keys keys:["hooks"]`. Tool events (PreToolUse / PostToolUse) carry
1582
+ // `"matcher": "*"`; non-tool events omit the matcher. Deny is exit-2 + stderr
1583
+ // (see policy-evaluator.ts). Event names are already PascalCase, so no event
1584
+ // canonicalization is needed. Settings paths: ~/.factory/hooks.json (user) and
1585
+ // <cwd>/.factory/hooks.json (project); no "local" scope.
1586
+
1587
+ interface FactoryHookMatcher {
1588
+ matcher?: string;
1589
+ hooks: Array<ClaudeHookEntry | Record<string, unknown>>;
1590
+ }
1591
+
1592
+ /** The Factory settings file IS the top-level events map (no wrapper key). */
1593
+ type FactorySettingsFile = Record<string, FactoryHookMatcher[] | unknown>;
1594
+
1595
+ export const factory: Integration = {
1596
+ id: "factory",
1597
+ displayName: "Factory Droid",
1598
+ scopes: FACTORY_HOOK_SCOPES,
1599
+ eventTypes: FACTORY_HOOK_EVENT_TYPES,
1600
+
1601
+ getSettingsPath(scope, cwd) {
1602
+ const base = cwd ? resolve(cwd) : process.cwd();
1603
+ switch (scope) {
1604
+ case "user":
1605
+ return resolve(homedir(), ".factory", "hooks.json");
1606
+ case "project":
1607
+ return resolve(base, ".factory", "hooks.json");
1608
+ case "local":
1609
+ // Factory has no "local" scope; the CLI rejects --cli factory --scope
1610
+ // local before reaching here, but fall back to project so callers don't
1611
+ // crash.
1612
+ return resolve(base, ".factory", "hooks.json");
1613
+ }
1614
+ },
1615
+
1616
+ readSettings(settingsPath) {
1617
+ return readJsonFile(settingsPath);
1618
+ },
1619
+
1620
+ writeSettings(settingsPath, settings) {
1621
+ writeJsonFile(settingsPath, settings);
1622
+ },
1623
+
1624
+ buildHookEntry(binaryPath, eventType, scope) {
1625
+ const command =
1626
+ scope === "project"
1627
+ ? `npx -y failproofai --hook ${eventType} --cli factory`
1628
+ : `"${binaryPath}" --hook ${eventType} --cli factory`;
1629
+ return {
1630
+ type: "command",
1631
+ command,
1632
+ // droid reads `timeout` in SECONDS (verified against droid v0.171.0). 30s.
1633
+ timeout: 30,
1634
+ [FAILPROOFAI_HOOK_MARKER]: true,
1635
+ };
1636
+ },
1637
+
1638
+ isFailproofaiHook: isMarkedHook,
1639
+
1640
+ writeHookEntries(settings, binaryPath, scope) {
1641
+ const s = settings as Record<string, FactoryHookMatcher[]>;
1642
+
1643
+ for (const eventType of FACTORY_HOOK_EVENT_TYPES) {
1644
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
1645
+ if (!Array.isArray(s[eventType])) s[eventType] = [];
1646
+ const matchers: FactoryHookMatcher[] = s[eventType];
1647
+
1648
+ let found = false;
1649
+ for (const matcher of matchers) {
1650
+ if (!matcher.hooks) continue;
1651
+ const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
1652
+ if (idx >= 0) {
1653
+ matcher.hooks[idx] = hookEntry;
1654
+ found = true;
1655
+ break;
1656
+ }
1657
+ }
1658
+ if (!found) {
1659
+ // Tool events match all tools via `matcher: "*"`; non-tool events carry
1660
+ // no matcher (verified live against droid v0.171.0).
1661
+ const isToolEvent = eventType === "PreToolUse" || eventType === "PostToolUse";
1662
+ matchers.push(isToolEvent ? { matcher: "*", hooks: [hookEntry] } : { hooks: [hookEntry] });
1663
+ }
1664
+ }
1665
+ },
1666
+
1667
+ removeHooksFromFile(settingsPath) {
1668
+ const settings = this.readSettings(settingsPath) as FactorySettingsFile;
1669
+
1670
+ let removed = 0;
1671
+ for (const eventType of Object.keys(settings)) {
1672
+ const matchers = settings[eventType];
1673
+ if (!Array.isArray(matchers)) continue;
1674
+ for (let i = matchers.length - 1; i >= 0; i--) {
1675
+ const matcher = matchers[i] as FactoryHookMatcher;
1676
+ if (!matcher || !matcher.hooks) continue;
1677
+ const before = matcher.hooks.length;
1678
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record<string, unknown>));
1679
+ removed += before - matcher.hooks.length;
1680
+ if (matcher.hooks.length === 0) matchers.splice(i, 1);
1681
+ }
1682
+ if (matchers.length === 0) delete settings[eventType];
1683
+ }
1684
+
1685
+ this.writeSettings(settingsPath, settings as Record<string, unknown>);
1686
+ return removed;
1687
+ },
1688
+
1689
+ hooksInstalledInSettings(scope, cwd) {
1690
+ const settingsPath = this.getSettingsPath(scope, cwd);
1691
+ if (!existsSync(settingsPath)) return false;
1692
+ try {
1693
+ const settings = this.readSettings(settingsPath) as FactorySettingsFile;
1694
+ for (const matchers of Object.values(settings)) {
1695
+ if (!Array.isArray(matchers)) continue;
1696
+ for (const matcher of matchers as FactoryHookMatcher[]) {
1697
+ if (!matcher || !matcher.hooks) continue;
1698
+ if (matcher.hooks.some((h) => isMarkedHook(h as Record<string, unknown>))) return true;
1699
+ }
1700
+ }
1701
+ } catch {
1702
+ // Corrupt settings — treat as not installed
1703
+ }
1704
+ return false;
1705
+ },
1706
+
1707
+ detectInstalled() {
1708
+ return binaryExists("droid");
1709
+ },
1710
+ };
1711
+
1712
+ // ── Devin CLI (devin) integration ────────────────────────────────────────────
1713
+ //
1714
+ // Devin's CLI (Cognition) is a **pure Claude-clone** external-command hook
1715
+ // system, verified live against devin v3000.1.27. It uses the standard Claude
1716
+ // `"hooks"`-wrapper schema, so this Integration mirrors `claudeCode` verbatim —
1717
+ // only the settings paths and the `--cli devin` command flag differ. The
1718
+ // config file also carries the operator's own keys (`org_id`, `theme_mode`,
1719
+ // …), so readSettings/writeSettings use the merge-preserving JSON helpers.
1720
+ //
1721
+ // Settings paths (verified against devin v3000.1.27):
1722
+ // user → ~/.config/devin/config.json (the `"hooks"` key)
1723
+ // project → <cwd>/.devin/config.json (the `"hooks"` key)
1724
+ // No "local" scope. Event names are already PascalCase (no event map / no
1725
+ // handler branch); the stdin payload is Claude snake_case (no normalization).
1726
+ // Deny/instruct semantics live in policy-evaluator.ts's `cli === "devin"`
1727
+ // branch (`{decision:"block", reason}` on stdout at exit 0).
1728
+
1729
+ export const devin: Integration = {
1730
+ id: "devin",
1731
+ displayName: "Devin CLI",
1732
+ scopes: DEVIN_HOOK_SCOPES,
1733
+ eventTypes: DEVIN_HOOK_EVENT_TYPES,
1332
1734
 
1333
1735
  getSettingsPath(scope, cwd) {
1334
1736
  const base = cwd ? resolve(cwd) : process.cwd();
1335
1737
  switch (scope) {
1336
1738
  case "user":
1337
- return resolve(homedir(), ".gemini", "settings.json");
1739
+ return resolve(homedir(), ".config", "devin", "config.json");
1338
1740
  case "project":
1339
- return resolve(base, ".gemini", "settings.json");
1741
+ return resolve(base, ".devin", "config.json");
1340
1742
  case "local":
1341
- // Gemini has no "local" scope; CLI rejects --cli gemini --scope local
1743
+ // Devin has no "local" scope; the CLI rejects --cli devin --scope local
1342
1744
  // before reaching here, but fall back to project so callers don't crash.
1343
- return resolve(base, ".gemini", "settings.json");
1745
+ return resolve(base, ".devin", "config.json");
1344
1746
  }
1345
1747
  },
1346
1748
 
@@ -1355,12 +1757,13 @@ export const gemini: Integration = {
1355
1757
  buildHookEntry(binaryPath, eventType, scope) {
1356
1758
  const command =
1357
1759
  scope === "project"
1358
- ? `npx -y failproofai --hook ${eventType} --cli gemini`
1359
- : `"${binaryPath}" --hook ${eventType} --cli gemini`;
1760
+ ? `npx -y failproofai --hook ${eventType} --cli devin`
1761
+ : `"${binaryPath}" --hook ${eventType} --cli devin`;
1360
1762
  return {
1361
1763
  type: "command",
1362
1764
  command,
1363
- timeout: 60_000,
1765
+ // Devin reads `timeout` in SECONDS like Claude. 60 = 60s.
1766
+ timeout: 60,
1364
1767
  [FAILPROOFAI_HOOK_MARKER]: true,
1365
1768
  };
1366
1769
  },
@@ -1368,19 +1771,14 @@ export const gemini: Integration = {
1368
1771
  isFailproofaiHook: isMarkedHook,
1369
1772
 
1370
1773
  writeHookEntries(settings, binaryPath, scope) {
1371
- const s = settings as GeminiSettingsFile;
1774
+ const s = settings as ClaudeSettings;
1372
1775
  if (!s.hooks) s.hooks = {};
1373
1776
 
1374
- for (const eventType of GEMINI_HOOK_EVENT_TYPES) {
1777
+ for (const eventType of DEVIN_HOOK_EVENT_TYPES) {
1375
1778
  const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
1376
1779
  if (!s.hooks[eventType]) s.hooks[eventType] = [];
1377
- const matchers: GeminiHookMatcher[] = s.hooks[eventType];
1780
+ const matchers: ClaudeHookMatcher[] = s.hooks[eventType];
1378
1781
 
1379
- // Idempotent: replace an existing failproofai-marked entry inside our
1380
- // own matcher; otherwise append a new `{matcher: "*", hooks: [...]}`.
1381
- // Hand-written matchers (with their own `matcher` regex) are never
1382
- // touched — we identify our matcher by checking whether ANY of its
1383
- // inner hooks are failproofai-marked.
1384
1782
  let found = false;
1385
1783
  for (const matcher of matchers) {
1386
1784
  if (!matcher.hooks) continue;
@@ -1391,12 +1789,12 @@ export const gemini: Integration = {
1391
1789
  break;
1392
1790
  }
1393
1791
  }
1394
- if (!found) matchers.push({ matcher: "*", hooks: [hookEntry] });
1792
+ if (!found) matchers.push({ hooks: [hookEntry] });
1395
1793
  }
1396
1794
  },
1397
1795
 
1398
1796
  removeHooksFromFile(settingsPath) {
1399
- const settings = this.readSettings(settingsPath) as GeminiSettingsFile;
1797
+ const settings = this.readSettings(settingsPath) as ClaudeSettings;
1400
1798
  if (!settings.hooks) return 0;
1401
1799
 
1402
1800
  let removed = 0;
@@ -1423,7 +1821,7 @@ export const gemini: Integration = {
1423
1821
  const settingsPath = this.getSettingsPath(scope, cwd);
1424
1822
  if (!existsSync(settingsPath)) return false;
1425
1823
  try {
1426
- const settings = this.readSettings(settingsPath) as GeminiSettingsFile;
1824
+ const settings = this.readSettings(settingsPath) as ClaudeSettings;
1427
1825
  if (!settings.hooks) return false;
1428
1826
  for (const matchers of Object.values(settings.hooks)) {
1429
1827
  if (!Array.isArray(matchers)) continue;
@@ -1439,58 +1837,77 @@ export const gemini: Integration = {
1439
1837
  },
1440
1838
 
1441
1839
  detectInstalled() {
1442
- return binaryExists("gemini");
1840
+ return binaryExists("devin");
1443
1841
  },
1444
1842
  };
1445
1843
 
1446
- // ── Hermes (hermes-agent) live hooks (Pillar 1) ────────────────────────────
1844
+ // ── Antigravity CLI (antigravity) integration ────────────────────────────────
1447
1845
  //
1448
- // External-command CLI like codex/cursor, but its config is YAML
1449
- // (`~/.hermes/config.yaml`) under a `hooks:` map, so the I/O layer uses the yaml
1450
- // Document API (comment-preserving). Flat per-event arrays like cursor:
1451
- // `hooks: { pre_tool_call: [ { command, timeout, <marker> } ], … }`. Hermes
1452
- // reads a `{"decision":"block",…}` JSON response on stdout (see
1453
- // policy-evaluator.ts); exit codes are ignored. User-scope only.
1454
-
1455
- /** One hook entry as stored under a `hooks:` event key in config.yaml. */
1456
- interface HermesHookEntry {
1457
- command: string;
1458
- timeout?: number;
1459
- [key: string]: unknown;
1846
+ // Antigravity's `agy` CLI uses a NAMED-hook schema: the hooks.json top-level key
1847
+ // is a hook *name* ("failproofai"), whose value is an event→handlers map. Tool
1848
+ // events (PreToolUse / PostToolUse) wrap handlers in `{matcher, hooks:[…]}`;
1849
+ // non-tool events (PreInvocation / Stop) are FLAT arrays of handler objects.
1850
+ // Verified live against agy v1.1.2. Settings paths:
1851
+ // user → ~/.gemini/config/hooks.json
1852
+ // project → <cwd>/.agents/hooks.json
1853
+ // No "local" scope. Deny/instruct semantics live in policy-evaluator.ts's
1854
+ // `cli === "antigravity"` branch (Antigravity's OWN `{decision:"deny"}` /
1855
+ // `{decision:"continue"}` / `{injectSteps}` shapes — NOT Claude's).
1856
+
1857
+ /** The Antigravity settings file is `{ "<hookName>": { <event>: … } }`. Our
1858
+ * named hook is "failproofai". */
1859
+ const ANTIGRAVITY_HOOK_NAME = "failproofai";
1860
+
1861
+ interface AntigravityToolMatcher {
1862
+ matcher?: string;
1863
+ hooks: Array<ClaudeHookEntry | Record<string, unknown>>;
1460
1864
  }
1865
+ /** An Antigravity named-hook body: tool events → `{matcher, hooks}[]`; flat
1866
+ * events (PreInvocation / Stop) → `handler[]`. */
1867
+ type AntigravityNamedHook = Record<
1868
+ string,
1869
+ AntigravityToolMatcher[] | Array<ClaudeHookEntry | Record<string, unknown>>
1870
+ >;
1461
1871
 
1462
- export const hermes: Integration = {
1463
- id: "hermes",
1464
- displayName: "Hermes",
1465
- scopes: HERMES_HOOK_SCOPES,
1466
- eventTypes: HERMES_HOOK_EVENT_TYPES,
1872
+ const ANTIGRAVITY_TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse"]);
1467
1873
 
1468
- // Hermes config is USER-scope only (`~/.hermes/config.yaml`); there is no
1469
- // project/local file, so scope/cwd are irrelevant here.
1470
- getSettingsPath() {
1471
- return resolve(homedir(), ".hermes", "config.yaml");
1874
+ export const antigravity: Integration = {
1875
+ id: "antigravity",
1876
+ displayName: "Antigravity CLI",
1877
+ scopes: ANTIGRAVITY_HOOK_SCOPES,
1878
+ eventTypes: ANTIGRAVITY_HOOK_EVENT_TYPES,
1879
+
1880
+ getSettingsPath(scope, cwd) {
1881
+ const base = cwd ? resolve(cwd) : process.cwd();
1882
+ switch (scope) {
1883
+ case "user":
1884
+ return resolve(homedir(), ".gemini", "config", "hooks.json");
1885
+ case "project":
1886
+ return resolve(base, ".agents", "hooks.json");
1887
+ case "local":
1888
+ // Antigravity has no "local" scope; the CLI rejects it before reaching
1889
+ // here, but fall back to project so callers don't crash.
1890
+ return resolve(base, ".agents", "hooks.json");
1891
+ }
1472
1892
  },
1473
1893
 
1474
1894
  readSettings(settingsPath) {
1475
- // Return the yaml Document (cast) so writeHookEntries/writeSettings can
1476
- // round-trip it while preserving the user's other keys + comments.
1477
- return readYamlDoc(settingsPath) as unknown as Record<string, unknown>;
1895
+ return readJsonFile(settingsPath);
1478
1896
  },
1479
1897
 
1480
1898
  writeSettings(settingsPath, settings) {
1481
- writeYamlDoc(settingsPath, settings as unknown as Document);
1899
+ writeJsonFile(settingsPath, settings);
1482
1900
  },
1483
1901
 
1484
1902
  buildHookEntry(binaryPath, eventType, scope) {
1485
- // No matcher → fires for ALL tools / all platforms (slack/telegram/cli/cron)
1486
- // and internal subagents. `timeout` is in seconds; Hermes runs the command
1487
- // via shlex.split (shell=false).
1488
1903
  const command =
1489
1904
  scope === "project"
1490
- ? `npx -y failproofai --hook ${eventType} --cli hermes`
1491
- : `"${binaryPath}" --hook ${eventType} --cli hermes`;
1905
+ ? `npx -y failproofai --hook ${eventType} --cli antigravity`
1906
+ : `"${binaryPath}" --hook ${eventType} --cli antigravity`;
1492
1907
  return {
1908
+ type: "command",
1493
1909
  command,
1910
+ // Antigravity reads `timeout` in SECONDS (verified agy v1.1.2). 30s.
1494
1911
  timeout: 30,
1495
1912
  [FAILPROOFAI_HOOK_MARKER]: true,
1496
1913
  };
@@ -1499,57 +1916,235 @@ export const hermes: Integration = {
1499
1916
  isFailproofaiHook: isMarkedHook,
1500
1917
 
1501
1918
  writeHookEntries(settings, binaryPath, scope) {
1502
- const doc = settings as unknown as Document;
1503
- // Read the current hooks map as plain JS, then re-set ONLY the `hooks` key —
1504
- // preserving comments on every other part of config.yaml.
1505
- const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1506
- const hooks: Record<string, HermesHookEntry[]> =
1507
- js.hooks && typeof js.hooks === "object" ? js.hooks : {};
1919
+ const s = settings as Record<string, unknown>;
1920
+ if (!s[ANTIGRAVITY_HOOK_NAME] || typeof s[ANTIGRAVITY_HOOK_NAME] !== "object") {
1921
+ s[ANTIGRAVITY_HOOK_NAME] = {};
1922
+ }
1923
+ const named = s[ANTIGRAVITY_HOOK_NAME] as AntigravityNamedHook;
1508
1924
 
1509
- for (const eventType of HERMES_HOOK_EVENT_TYPES) {
1510
- const entry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as HermesHookEntry;
1511
- const arr = Array.isArray(hooks[eventType]) ? hooks[eventType] : [];
1512
- const idx = arr.findIndex((h) => isMarkedHook(h));
1513
- if (idx >= 0) arr[idx] = entry;
1514
- else arr.push(entry);
1515
- hooks[eventType] = arr;
1925
+ for (const eventType of ANTIGRAVITY_HOOK_EVENT_TYPES) {
1926
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
1927
+ const isToolEvent = ANTIGRAVITY_TOOL_EVENTS.has(eventType);
1928
+
1929
+ if (isToolEvent) {
1930
+ if (!Array.isArray(named[eventType])) named[eventType] = [] as AntigravityToolMatcher[];
1931
+ const matchers = named[eventType] as AntigravityToolMatcher[];
1932
+ let found = false;
1933
+ for (const matcher of matchers) {
1934
+ if (!matcher.hooks) continue;
1935
+ const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
1936
+ if (idx >= 0) {
1937
+ matcher.hooks[idx] = hookEntry;
1938
+ found = true;
1939
+ break;
1940
+ }
1941
+ }
1942
+ if (!found) matchers.push({ matcher: "*", hooks: [hookEntry] });
1943
+ } else {
1944
+ // Flat array of handler objects (PreInvocation / Stop).
1945
+ if (!Array.isArray(named[eventType])) named[eventType] = [] as Array<ClaudeHookEntry | Record<string, unknown>>;
1946
+ const handlers = named[eventType] as Array<ClaudeHookEntry | Record<string, unknown>>;
1947
+ const idx = handlers.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
1948
+ if (idx >= 0) handlers[idx] = hookEntry;
1949
+ else handlers.push(hookEntry);
1950
+ }
1516
1951
  }
1517
- doc.set("hooks", hooks);
1518
- // The headless gateway has no TTY to answer Hermes's first-use hook-consent
1519
- // prompt, so auto-accept declared hooks. Tradeoff: also auto-accepts any
1520
- // other hook the operator adds; a targeted `shell-hooks-allowlist.json`
1521
- // pre-seed is a future refinement.
1522
- doc.set("hooks_auto_accept", true);
1523
1952
  },
1524
1953
 
1525
1954
  removeHooksFromFile(settingsPath) {
1526
- if (!existsSync(settingsPath)) return 0;
1527
- const doc = readYamlDoc(settingsPath);
1528
- const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1529
- const hooks = js.hooks;
1955
+ const settings = this.readSettings(settingsPath) as Record<string, unknown>;
1956
+ const named = settings[ANTIGRAVITY_HOOK_NAME];
1957
+ if (!named || typeof named !== "object") {
1958
+ this.writeSettings(settingsPath, settings);
1959
+ return 0;
1960
+ }
1961
+ const namedHook = named as AntigravityNamedHook;
1530
1962
 
1531
1963
  let removed = 0;
1532
- if (hooks && typeof hooks === "object") {
1533
- for (const eventType of Object.keys(hooks)) {
1534
- const entries = hooks[eventType];
1535
- if (!Array.isArray(entries)) continue;
1536
- const before = entries.length;
1537
- const filtered = entries.filter((h) => !isMarkedHook(h));
1964
+ for (const eventType of Object.keys(namedHook)) {
1965
+ const value = namedHook[eventType];
1966
+ if (!Array.isArray(value)) continue;
1967
+ if (ANTIGRAVITY_TOOL_EVENTS.has(eventType)) {
1968
+ const matchers = value as AntigravityToolMatcher[];
1969
+ for (let i = matchers.length - 1; i >= 0; i--) {
1970
+ const matcher = matchers[i];
1971
+ if (!matcher || !matcher.hooks) continue;
1972
+ const before = matcher.hooks.length;
1973
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record<string, unknown>));
1974
+ removed += before - matcher.hooks.length;
1975
+ if (matcher.hooks.length === 0) matchers.splice(i, 1);
1976
+ }
1977
+ if (matchers.length === 0) delete namedHook[eventType];
1978
+ } else {
1979
+ const handlers = value as Array<Record<string, unknown>>;
1980
+ const before = handlers.length;
1981
+ const filtered = handlers.filter((h) => !isMarkedHook(h));
1538
1982
  removed += before - filtered.length;
1539
- if (filtered.length === 0) delete hooks[eventType];
1540
- else hooks[eventType] = filtered;
1983
+ if (filtered.length === 0) delete namedHook[eventType];
1984
+ else namedHook[eventType] = filtered as Array<ClaudeHookEntry | Record<string, unknown>>;
1541
1985
  }
1542
- if (removed > 0) {
1543
- if (Object.keys(hooks).length === 0) doc.delete("hooks");
1544
- else doc.set("hooks", hooks);
1986
+ }
1987
+ // Drop the named hook entirely if it has no remaining events.
1988
+ if (Object.keys(namedHook).length === 0) delete settings[ANTIGRAVITY_HOOK_NAME];
1989
+
1990
+ this.writeSettings(settingsPath, settings);
1991
+ return removed;
1992
+ },
1993
+
1994
+ hooksInstalledInSettings(scope, cwd) {
1995
+ const settingsPath = this.getSettingsPath(scope, cwd);
1996
+ if (!existsSync(settingsPath)) return false;
1997
+ try {
1998
+ const settings = this.readSettings(settingsPath) as Record<string, unknown>;
1999
+ const named = settings[ANTIGRAVITY_HOOK_NAME];
2000
+ if (!named || typeof named !== "object") return false;
2001
+ for (const value of Object.values(named as AntigravityNamedHook)) {
2002
+ if (!Array.isArray(value)) continue;
2003
+ for (const item of value) {
2004
+ if (isMarkedHook(item as Record<string, unknown>)) return true;
2005
+ const matcher = item as AntigravityToolMatcher;
2006
+ if (matcher && Array.isArray(matcher.hooks) && matcher.hooks.some((h) => isMarkedHook(h as Record<string, unknown>))) {
2007
+ return true;
2008
+ }
2009
+ }
1545
2010
  }
2011
+ } catch {
2012
+ // Corrupt settings — treat as not installed
1546
2013
  }
2014
+ return false;
2015
+ },
1547
2016
 
1548
- // Always drop our headless-consent flag on uninstall — even if the hooks were
1549
- // already removed manually — so it can't silently auto-accept future operator
1550
- // hooks. `doc.delete` returns true iff the key was present.
1551
- const droppedAutoAccept = doc.delete("hooks_auto_accept");
1552
- if (removed > 0 || droppedAutoAccept) writeYamlDoc(settingsPath, doc);
2017
+ detectInstalled() {
2018
+ return binaryExists("agy");
2019
+ },
2020
+ };
2021
+
2022
+ // ── Goose (codename goose, Block) integration ────────────────────────────────
2023
+ //
2024
+ // Goose's "hooks" system follows the cross-agent Open Plugins spec: a plugin
2025
+ // directory whose hooks/hooks.json wires shell commands into agent events.
2026
+ // failproofai owns the entire `failproofai` plugin dir and writes an Open
2027
+ // Plugins hooks.json — `{"hooks": {<Event>: [{"hooks": [{type, command}]}]}}`.
2028
+ // Two differences from Factory (verified live against goose v1.43.0):
2029
+ // 1. the schema DOES carry the top-level "hooks" wrapper (like Claude), and
2030
+ // 2. the matcher is OMITTED on every event — a bare "*" is an invalid regex
2031
+ // that matches NOTHING (omitted = match all tools).
2032
+ // Entries are the clean `{type, command}` shape with NO marker field — Goose
2033
+ // parses this file, so we identify our hooks by the `--cli goose` command
2034
+ // substring instead of injecting `__failproofai_hook__`. Goose auto-discovers
2035
+ // the dir at startup (no config edit) and self-registers it into
2036
+ // ~/.config/goose/config.yaml; uninstall clears our entries (the emptied
2037
+ // hooks.json and the auto-added config `plugins:` entry are left for Goose to
2038
+ // reconcile on next start — it logs the empty plugin and continues, harmless).
2039
+ // Deny is PreToolUse-only JSON (see policy-evaluator.ts). Settings paths:
2040
+ // ~/.agents/plugins/failproofai/hooks/hooks.json (user) and
2041
+ // <cwd>/.agents/plugins/failproofai/hooks/hooks.json (project); no "local" scope.
2042
+
2043
+ interface GooseHookMatcher {
2044
+ matcher?: string;
2045
+ hooks: Array<Record<string, unknown>>;
2046
+ }
2047
+ /** The Open Plugins hooks file: { "hooks": { <Event>: GooseHookMatcher[] } }. */
2048
+ interface GooseHooksFile {
2049
+ hooks?: Record<string, GooseHookMatcher[]>;
2050
+ [key: string]: unknown;
2051
+ }
2052
+
2053
+ /** failproofai owns the `failproofai` Goose plugin dir; identify our hook
2054
+ * entries by the `--cli goose` command substring (the file is Goose-parsed, so
2055
+ * entries stay the clean {type, command} shape with no marker field). */
2056
+ function isGooseFailproofaiHook(hook: unknown): boolean {
2057
+ if (!hook || typeof hook !== "object") return false;
2058
+ const cmd = (hook as { command?: unknown }).command;
2059
+ return typeof cmd === "string" && cmd.includes("failproofai") && cmd.includes("--cli goose");
2060
+ }
2061
+
2062
+ export const goose: Integration = {
2063
+ id: "goose",
2064
+ displayName: "Goose",
2065
+ scopes: GOOSE_HOOK_SCOPES,
2066
+ eventTypes: GOOSE_HOOK_EVENT_TYPES,
2067
+
2068
+ getSettingsPath(scope, cwd) {
2069
+ const base = cwd ? resolve(cwd) : process.cwd();
2070
+ switch (scope) {
2071
+ case "user":
2072
+ return resolve(homedir(), ".agents", "plugins", "failproofai", "hooks", "hooks.json");
2073
+ case "project":
2074
+ return resolve(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json");
2075
+ case "local":
2076
+ // Goose has no "local" scope; the CLI rejects --scope local before
2077
+ // reaching here, but fall back to project so callers don't crash.
2078
+ return resolve(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json");
2079
+ }
2080
+ },
2081
+
2082
+ readSettings(settingsPath) {
2083
+ return readJsonFile(settingsPath);
2084
+ },
2085
+
2086
+ writeSettings(settingsPath, settings) {
2087
+ writeJsonFile(settingsPath, settings);
2088
+ },
2089
+
2090
+ buildHookEntry(binaryPath, eventType, scope) {
2091
+ const command =
2092
+ scope === "project"
2093
+ ? `npx -y failproofai --hook ${eventType} --cli goose`
2094
+ : `"${binaryPath}" --hook ${eventType} --cli goose`;
2095
+ // Open Plugins command entry: { type, command } only (Goose applies its own
2096
+ // timeout; no marker field — see isGooseFailproofaiHook).
2097
+ return { type: "command", command };
2098
+ },
2099
+
2100
+ isFailproofaiHook: isGooseFailproofaiHook,
2101
+
2102
+ writeHookEntries(settings, binaryPath, scope) {
2103
+ const s = settings as GooseHooksFile;
2104
+ if (!s.hooks) s.hooks = {};
2105
+
2106
+ for (const eventType of GOOSE_HOOK_EVENT_TYPES) {
2107
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
2108
+ if (!Array.isArray(s.hooks[eventType])) s.hooks[eventType] = [];
2109
+ const matchers: GooseHookMatcher[] = s.hooks[eventType];
2110
+
2111
+ let found = false;
2112
+ for (const matcher of matchers) {
2113
+ if (!matcher.hooks) continue;
2114
+ const idx = matcher.hooks.findIndex((h) => isGooseFailproofaiHook(h));
2115
+ if (idx >= 0) {
2116
+ matcher.hooks[idx] = hookEntry;
2117
+ found = true;
2118
+ break;
2119
+ }
2120
+ }
2121
+ // matcher OMITTED on every event (a bare "*" matches nothing; omitted =
2122
+ // match all tools — verified live against goose v1.43.0).
2123
+ if (!found) matchers.push({ hooks: [hookEntry] });
2124
+ }
2125
+ },
2126
+
2127
+ removeHooksFromFile(settingsPath) {
2128
+ const settings = this.readSettings(settingsPath) as GooseHooksFile;
2129
+ if (!settings.hooks) return 0;
2130
+
2131
+ let removed = 0;
2132
+ for (const eventType of Object.keys(settings.hooks)) {
2133
+ const matchers = settings.hooks[eventType];
2134
+ if (!Array.isArray(matchers)) continue;
2135
+ for (let i = matchers.length - 1; i >= 0; i--) {
2136
+ const matcher = matchers[i];
2137
+ if (!matcher || !matcher.hooks) continue;
2138
+ const before = matcher.hooks.length;
2139
+ matcher.hooks = matcher.hooks.filter((h) => !isGooseFailproofaiHook(h));
2140
+ removed += before - matcher.hooks.length;
2141
+ if (matcher.hooks.length === 0) matchers.splice(i, 1);
2142
+ }
2143
+ if (matchers.length === 0) delete settings.hooks[eventType];
2144
+ }
2145
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
2146
+
2147
+ this.writeSettings(settingsPath, settings as Record<string, unknown>);
1553
2148
  return removed;
1554
2149
  },
1555
2150
 
@@ -1557,21 +2152,23 @@ export const hermes: Integration = {
1557
2152
  const settingsPath = this.getSettingsPath(scope, cwd);
1558
2153
  if (!existsSync(settingsPath)) return false;
1559
2154
  try {
1560
- const doc = readYamlDoc(settingsPath);
1561
- const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1562
- const hooks = js.hooks;
1563
- if (!hooks || typeof hooks !== "object") return false;
1564
- for (const entries of Object.values(hooks)) {
1565
- if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true;
2155
+ const settings = this.readSettings(settingsPath) as GooseHooksFile;
2156
+ if (!settings.hooks) return false;
2157
+ for (const matchers of Object.values(settings.hooks)) {
2158
+ if (!Array.isArray(matchers)) continue;
2159
+ for (const matcher of matchers) {
2160
+ if (!matcher || !matcher.hooks) continue;
2161
+ if (matcher.hooks.some((h) => isGooseFailproofaiHook(h))) return true;
2162
+ }
1566
2163
  }
1567
2164
  } catch {
1568
- // Corrupt config — treat as not installed.
2165
+ // Corrupt settings — treat as not installed
1569
2166
  }
1570
2167
  return false;
1571
2168
  },
1572
2169
 
1573
2170
  detectInstalled() {
1574
- return binaryExists("hermes");
2171
+ return binaryExists("goose");
1575
2172
  },
1576
2173
  };
1577
2174
 
@@ -1589,8 +2186,12 @@ const INTEGRATIONS: Partial<Record<IntegrationType, Integration>> = {
1589
2186
  cursor,
1590
2187
  opencode,
1591
2188
  pi,
1592
- gemini,
1593
2189
  hermes,
2190
+ openclaw,
2191
+ factory,
2192
+ devin,
2193
+ antigravity,
2194
+ goose,
1594
2195
  };
1595
2196
 
1596
2197
  export function getIntegration(id: IntegrationType): Integration {