failproofai 0.0.12 → 0.0.13-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 (203) hide show
  1. package/.next/standalone/.cursor/hooks.json +6 -6
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  25. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  28. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -2
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +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 +2 -2
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/logout/route.js +2 -2
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
  40. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
  42. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  47. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/index.html +1 -1
  50. package/.next/standalone/.next/server/app/index.rsc +15 -15
  51. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  52. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  53. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  54. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  55. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  56. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/page.js +1 -1
  58. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  61. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  62. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
  66. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  70. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -2
  71. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  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 +2 -2
  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_0xu5z._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hv08mx._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0obaznk._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xvsnvt._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hkdkmz._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1i9rknc._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0cycwg6._.js → [root-of-the-server]__1tvn4wf._.js} +2 -2
  92. package/.next/standalone/.next/server/chunks/_1z062h5._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
  94. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +4 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ntd0fz._.js → [root-of-the-server]__099afy6._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +4 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +4 -0
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +4 -0
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ehe24g._.js → [root-of-the-server]__0ynf7tx._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +245 -0
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vh88nr._.js → [root-of-the-server]__1qlh4jx._.js} +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +4 -0
  111. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_0ul5rf_._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0bppeyf.js → node_modules_html-to-image_es_index_0pv57nx.js} +1 -1
  121. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  122. package/.next/standalone/.next/server/pages/404.html +1 -1
  123. package/.next/standalone/.next/server/pages/500.html +1 -1
  124. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  125. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  126. package/.next/standalone/.next/static/chunks/{3jk5y-qgnnti_.js → 0b6rdtcge_3ec.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/{09l7dfsd4xapi.js → 0pj_6lbbjd000.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{0ys-mwok1j9kp.js → 0xnrgonfdeivm.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/{00_ypuitb29l0.js → 105w_oznq-van.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{3pb3m-89k7ts2.js → 13tbm6i1swiru.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{36yb_gsfgfga4.js → 1fhuj4fjovzp5.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/209ful1jrphvy.js +1 -0
  133. package/.next/standalone/.next/static/chunks/{0qw65r3wfs089.js → 23odnajn2ltwu.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +2 -0
  135. package/.next/standalone/.next/static/chunks/36r2vgi6854za.js +6 -0
  136. package/.next/standalone/.next/static/chunks/3b77kulbdbrqw.js +1 -0
  137. package/.next/standalone/.next/static/chunks/3rjg-1s5ouu5r.js +1 -0
  138. package/.next/standalone/app/components/sessions-list.tsx +19 -0
  139. package/.next/standalone/app/project/[name]/page.tsx +9 -4
  140. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +26 -5
  141. package/.next/standalone/assets/logos/hermes-dark.svg +1 -0
  142. package/.next/standalone/assets/logos/hermes-light.svg +1 -0
  143. package/.next/standalone/docker-hook-sync/Dockerfile +75 -0
  144. package/.next/standalone/docker-hook-sync/entrypoint.sh +109 -0
  145. package/.next/standalone/lib/cli-registry.ts +6 -1
  146. package/.next/standalone/lib/download-session.ts +16 -0
  147. package/.next/standalone/lib/hermes-projects.ts +164 -0
  148. package/.next/standalone/lib/hermes-sessions.ts +273 -0
  149. package/.next/standalone/lib/projects.ts +14 -3
  150. package/.next/standalone/lib/sqlite-reader.ts +102 -0
  151. package/.next/standalone/package.json +5 -3
  152. package/.next/standalone/server.js +1 -1
  153. package/README.md +10 -1
  154. package/bin/failproofai.mjs +17 -16
  155. package/dist/cli.mjs +7779 -229
  156. package/lib/cli-registry.ts +6 -1
  157. package/lib/download-session.ts +16 -0
  158. package/lib/hermes-projects.ts +164 -0
  159. package/lib/hermes-sessions.ts +273 -0
  160. package/lib/projects.ts +14 -3
  161. package/lib/sql-js-asm.d.ts +26 -0
  162. package/lib/sqlite-reader.ts +102 -0
  163. package/package.json +5 -3
  164. package/scripts/sync-agent-cli-harnesses-prompt.md +222 -0
  165. package/src/audit/cli-adapters/hermes.ts +58 -0
  166. package/src/audit/cli-adapters/index.ts +6 -0
  167. package/src/hooks/handler.ts +8 -0
  168. package/src/hooks/install-prompt.ts +7 -3
  169. package/src/hooks/integrations.ts +191 -10
  170. package/src/hooks/policy-evaluator.ts +42 -0
  171. package/src/hooks/resolve-transcript-path.ts +4 -0
  172. package/src/hooks/tool-name-canonicalize.ts +6 -0
  173. package/src/hooks/types.ts +107 -1
  174. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dwpg-h._.js +0 -3
  175. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lnenda._.js +0 -3
  176. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13i_sva._.js +0 -3
  177. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +0 -3
  178. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m2_4t0._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +0 -3
  183. package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +0 -3
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +0 -4
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e4-6d8._.js +0 -4
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f62vu9._.js +0 -3
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0k65l27._.js +0 -3
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +0 -4
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +0 -4
  191. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e-x7j4._.js +0 -3
  192. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +0 -221
  193. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +0 -4
  194. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1uvfwgr._.js +0 -4
  195. package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +0 -1
  196. package/.next/standalone/.next/static/chunks/1o0xa47736gi9.css +0 -2
  197. package/.next/standalone/.next/static/chunks/2ey-m4-p9z7e0.js +0 -6
  198. package/.next/standalone/.next/static/chunks/2wbuxnsvux4di.js +0 -1
  199. package/.next/standalone/.next/static/chunks/3-veva26100-4.js +0 -1
  200. package/scripts/sync-hook-events-prompt.md +0 -179
  201. /package/.next/standalone/.next/static/{PdZTBvCBXzDRLe-duL9yC → 31ZoDDYKHEIN_TrgKv85t}/_buildManifest.js +0 -0
  202. /package/.next/standalone/.next/static/{PdZTBvCBXzDRLe-duL9yC → 31ZoDDYKHEIN_TrgKv85t}/_clientMiddlewareManifest.js +0 -0
  203. /package/.next/standalone/.next/static/{PdZTBvCBXzDRLe-duL9yC → 31ZoDDYKHEIN_TrgKv85t}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.12",
3
+ "version": "0.0.13-beta.1",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -22,7 +22,7 @@
22
22
  "scripts": {
23
23
  "predev": "bun run build:cli && bun link",
24
24
  "dev": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/dev.ts --port 8020",
25
- "build:cli": "bun build --target=node --format=esm --outfile=dist/cli.mjs bin/failproofai.mjs --external posthog-node && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/cli.mjs','utf8');fs.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node').replace('// @bun\\n',''))\"",
25
+ "build:cli": "bun build --target=node --format=esm --outfile=dist/cli.mjs bin/failproofai.mjs --external posthog-node --external sql.js && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/cli.mjs','utf8');fs.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node').replace('// @bun\\n',''))\"",
26
26
  "build": "bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts && bun run build:cli && bun --bun next build && node -e \"const {cpSync}=require('fs');cpSync('.next/static','.next/standalone/.next/static',{recursive:true});\" && node scripts/prune-standalone.mjs",
27
27
  "prestart": "bun run build:cli && bun link",
28
28
  "start": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/start.ts",
@@ -99,7 +99,9 @@
99
99
  "dependencies": {
100
100
  "html-to-image": "^1.11.13",
101
101
  "html2canvas": "^1.4.1",
102
- "posthog-node": "^5.37.1"
102
+ "posthog-node": "^5.37.1",
103
+ "sql.js": "^1.14.1",
104
+ "yaml": "^2.9.0"
103
105
  },
104
106
  "overrides": {
105
107
  "postcss": "8.5.14",
@@ -0,0 +1,222 @@
1
+ ultracode
2
+
3
+ You are an automated **ultracode** (multi-agent) run inside a fresh clone of the
4
+ failproofai repository. Your job is to keep failproofai in sync with the upstream
5
+ documentation for **every** agent CLI we integrate with, then open exactly one
6
+ pull request with the fix — or, if a sync PR is already open, comment on it instead.
7
+
8
+ ## Run contract (read first)
9
+
10
+ - You are running **headless and autonomously**. No human is watching; nobody can
11
+ answer a question mid-run. For reversible actions that follow from this task,
12
+ proceed without asking. Do **not** end your turn with a plan or a question —
13
+ finish the work.
14
+ - The entrypoint has **already cut a fresh branch** named `auto/sync-cli-harnesses-<UTC>`.
15
+ Do all your work here; do **not** switch branches or work on `main`.
16
+ - failproofai's own Stop hooks are active in this checkout. If you make commits, you
17
+ will not be allowed to stop until you have **committed**, **pushed**, and **opened a
18
+ PR** for them. If you make **no** commits (nothing to sync, or you only commented on
19
+ an already-open sync PR), a clean working tree stops fine — so on those paths, do
20
+ **not** edit any files.
21
+ - Use ultracode / multi-agent orchestration: in Phase 1, fan out **one subagent per
22
+ CLI** so the seven harnesses are checked in parallel.
23
+
24
+ failproofai integrates with **seven** agent CLIs. Each has its own hook surface,
25
+ tracked in `src/hooks/types.ts` and written to disk by `src/hooks/integrations.ts`:
26
+
27
+ | CLI | Event array (`src/hooks/types.ts`) | Event map | Settings file written (`src/hooks/integrations.ts`) |
28
+ |----------------|------------------------------------|-----------|------------------------------------------------------|
29
+ | Claude Code | `HOOK_EVENT_TYPES` (canonical) | — (canonical) | `.claude/settings.json` |
30
+ | OpenAI Codex | `CODEX_HOOK_EVENT_TYPES` | `CODEX_EVENT_MAP` | `.codex/hooks.json` |
31
+ | GitHub Copilot | `COPILOT_HOOK_EVENT_TYPES` | — (already Pascal) | `.github/hooks/failproofai.json` |
32
+ | Cursor Agent | `CURSOR_HOOK_EVENT_TYPES` | `CURSOR_EVENT_MAP` | `.cursor/hooks.json` |
33
+ | OpenCode | `OPENCODE_HOOK_EVENT_TYPES` | `OPENCODE_EVENT_MAP` | `.opencode/opencode.json` + `.opencode/plugins/failproofai.mjs` |
34
+ | Pi | `PI_HOOK_EVENT_TYPES` | `PI_EVENT_MAP` | `.pi/settings.json` |
35
+ | Gemini CLI | `GEMINI_HOOK_EVENT_TYPES` | `GEMINI_EVENT_MAP` | `.gemini/settings.json` |
36
+
37
+ ## What "drift" means — three scopes
38
+
39
+ For each verified CLI, compare the upstream docs to this repo across three scopes:
40
+
41
+ 1. **Scope 1 — hook event names.** Events documented upstream but missing from (or
42
+ removed from) the CLI's `*HOOK_EVENT_TYPES` array in `src/hooks/types.ts`.
43
+ 2. **Scope 2 — tool / payload schema.** Changes to the CLI's tool names or payload
44
+ field shapes, tracked in the `*_TOOL_MAP` / `*_TOOL_INPUT_MAP` tables in
45
+ `src/hooks/types.ts` and in the per-CLI response-shape branches of
46
+ `src/hooks/policy-evaluator.ts`.
47
+ 3. **Scope 3 — settings-file shape.** Changes to the structure of the config file
48
+ failproofai writes for the CLI: the hard-coded literals in
49
+ `src/hooks/integrations.ts` (matcher-wrapper vs flat-array vs packages-array vs
50
+ plugin registration; field names like `command` vs `bash`/`powershell`;
51
+ `timeout` vs `timeoutSec` and its **unit** seconds-vs-ms; presence of `version`
52
+ / `$schema` / `matcher`; the settings-file path) **and** the committed dogfood
53
+ fixture on disk. The canonical worked example is git history / PR #482, which
54
+ dropped an invalid top-level `version` field and fixed a `60000`ms→`60`s timeout
55
+ unit in `.codex/hooks.json` by editing the writer, the fixture, and the tests
56
+ together.
57
+
58
+ ## Reading rules
59
+
60
+ - This run relaxed two policies (`require-ci-green-before-stop`,
61
+ `block-read-outside-cwd`) so you can read and edit the settings fixtures. Even so,
62
+ when you only need to **inspect** a committed fixture, prefer
63
+ `git show HEAD:<path>` (e.g. `git show HEAD:.codex/hooks.json`) — it is never
64
+ blocked and is the reliable way to read `.claude/settings.json`, `.codex/hooks.json`,
65
+ `.cursor/hooks.json`, `.github/hooks/failproofai.json`, `.gemini/settings.json`,
66
+ `.pi/settings.json`, and `.opencode/*`.
67
+ - Do **NOT** run `failproofai policies --install` (or any `failproofai` subcommand)
68
+ to regenerate a fixture — it is blocked. Edit fixtures by hand.
69
+
70
+ ## Phase 1 — Fan out one subagent per CLI
71
+
72
+ Spawn one subagent per CLI (seven, in parallel). Each subagent, for its CLI:
73
+
74
+ 1. `WebFetch` the docs URL(s) below and extract the authoritative lists: the hook
75
+ **event names**, the **tool names / payload field names**, and the
76
+ **settings-file schema** (top-level keys, per-entry shape, command field name,
77
+ timeout field name **and unit**, and whether `matcher` / `version` / `$schema`
78
+ are present). **Use upstream casing exactly** — Codex snake_case, Cursor
79
+ camelCase, OpenCode dot.namespaced, Pi snake_case, Claude/Copilot/Gemini
80
+ PascalCase. Do **not** normalize.
81
+ 2. Compare against this repo: the CLI's `*HOOK_EVENT_TYPES` array + paired
82
+ `*EVENT_MAP` (scope 1); its `*_TOOL_MAP` / `*_TOOL_INPUT_MAP` and its branch in
83
+ `src/hooks/policy-evaluator.ts` (scope 2); its literals in
84
+ `src/hooks/integrations.ts` + its dogfood fixture read via `git show` (scope 3).
85
+ 3. Return a structured report: `{ cli, status: up-to-date | drift | unverified,
86
+ scope1: {added, removed}, scope2: {...}, scope3: {...}, sources: [...] }`.
87
+
88
+ If a docs URL 404s, redirects to a stub, or has no parseable list, mark that CLI
89
+ `unverified` and skip its diff. **Do not invent events, tools, or fields. Do not
90
+ guess from prior knowledge.** Pi is the most likely `unverified` candidate (its
91
+ surface is documented in the package source, not a clean enumeration).
92
+
93
+ | CLI | Docs URL(s) |
94
+ |-----------|-------------|
95
+ | Claude | https://code.claude.com/docs/en/hooks (reference) · https://code.claude.com/docs/en/hooks-guide (summary) |
96
+ | Codex | https://developers.openai.com/codex/hooks |
97
+ | Copilot | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks · https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference (tool names) |
98
+ | Cursor | https://cursor.com/docs/hooks |
99
+ | OpenCode | https://opencode.ai/docs/plugins/ |
100
+ | Pi | https://www.npmjs.com/package/@mariozechner/pi-coding-agent |
101
+ | Gemini | https://geminicli.com/docs/hooks/ · https://geminicli.com/docs/reference/tools/ (tool names) |
102
+
103
+ ## Phase 2 — Decide what to do (do this BEFORE editing any files)
104
+
105
+ Merge the subagent reports into one list of detected drift. Then, **before you edit a
106
+ single file**, decide which path you are on — editing first would leave uncommitted
107
+ changes that block a clean stop on the comment / no-op paths below.
108
+
109
+ 1. **No drift in any verified CLI** → do nothing: make no edits, open no PR, post no
110
+ comment. Stop. (`unverified` alone is not drift — the docs may just be unreachable
111
+ today.)
112
+
113
+ 2. **Drift exists — first check for an already-open sync PR:**
114
+ `gh pr list --base main --state open --search "[auto] sync agent CLI harnesses" --json number,url,headRefName`.
115
+
116
+ - **If an open sync PR already exists, do NOT open a second PR and do NOT edit any
117
+ files.** Read what it already covers (`gh pr diff <number>` plus its body) and
118
+ compare against your detected drift. If that PR already covers everything you
119
+ found, stop — nothing to add. If you found drift the PR is **missing**, post ONE
120
+ comment listing exactly what's missing, then stop —
121
+ `gh pr comment <number> --body "..."`, e.g.:
122
+
123
+ > This open sync PR is missing newly-detected drift:
124
+ > - **Cursor**: upstream added event `afterEdit` — append to `CURSOR_HOOK_EVENT_TYPES` (+ a `CURSOR_EVENT_MAP` entry).
125
+ > - **Gemini**: the `run_shell_command` tool was renamed to `shell` — the `GEMINI_TOOL_MAP` key is stale.
126
+ >
127
+ > (hook-sync bot — please fold these into this PR rather than opening a second sync PR.)
128
+
129
+ Never push commits to another PR's branch; only comment, and leave the working
130
+ tree clean so this run stops cleanly.
131
+
132
+ - **If no open sync PR exists → go to Phase 3** to apply the fixes and open one.
133
+
134
+ ## Phase 3 — Apply the fixes and open one PR
135
+
136
+ Only reached when there is real drift **and** no open sync PR. Edit only what drift
137
+ requires.
138
+
139
+ ### Scope 1 (event names)
140
+ - **Append** new events just before `] as const`, preserving upstream casing.
141
+ - **Delete** removed events; if the CLI has an `*EVENT_MAP`, also delete the same
142
+ key from that map (the `Record` is exhaustive — a stale key fails `tsc`).
143
+ - For **map-bearing CLIs** (Codex, Cursor, OpenCode, Pi, Gemini), do **NOT** invent
144
+ the canonical mapping for a newly-added event — leave it out of the `*EVENT_MAP`
145
+ so `tsc` fails intentionally, and add a reviewer-checklist item to the PR body
146
+ (see below). For **Claude and Copilot** (no map), the build stays green.
147
+ - **Fix hardcoded test counts** when the relevant array changed:
148
+ - If `HOOK_EVENT_TYPES` (Claude) changed: in `__tests__/hooks/manager.test.ts`,
149
+ update the `installs hooks for all <N> event types` description AND both
150
+ `expect(Object.keys(written.hooks)).toHaveLength(<N>)` assertions.
151
+ - If `GEMINI_HOOK_EVENT_TYPES` changed: in `__tests__/hooks/integrations.test.ts`,
152
+ update the `expect(gemini.eventTypes).toHaveLength(<N>)` assertion AND the
153
+ matching description string.
154
+ - Locate these by searching for the current count number.
155
+
156
+ ### Scope 2 / Scope 3 (tool schema / settings-file shape)
157
+ - Apply a fix **only when it is unambiguous and needs no human judgement** — e.g.
158
+ an invalid field to remove, a wrong timeout unit, or a renamed tool id (the PR
159
+ #482 class). When you do, move the whole quartet together, exactly like #482:
160
+ the writer in `src/hooks/integrations.ts` (+ a shared helper if it dedupes
161
+ logic), the committed dogfood fixture, the unit test in
162
+ `__tests__/hooks/integrations.test.ts`, and the e2e test in
163
+ `__tests__/e2e/hooks/<cli>-integration.e2e.test.ts` — plus a `types.ts` comment
164
+ if it documents the old shape.
165
+ - If the correct fix needs judgement (which canonical event/tool a new item maps
166
+ to; an ambiguous schema change), **leave the code untouched** and record it as a
167
+ reviewer-checklist item instead. Bias toward reporting over auto-editing core
168
+ integration code.
169
+ - Do **NOT** add `*EVENT_MAP`, `*_TOOL_MAP`, or `*_TOOL_INPUT_MAP` entries for
170
+ newly-added items — the canonical mapping is a human decision.
171
+
172
+ ### CHANGELOG (required)
173
+ Add a single-line entry to `CHANGELOG.md` under `## <version> — <YYYY-MM-DD>` where
174
+ `<version>` is the `version` field in `package.json` (read it) and `<YYYY-MM-DD>` is
175
+ today's UTC date. If that heading does not exist yet, create it above the previous
176
+ version's section. There is no `## Unreleased` section. Use the `### Features` (or
177
+ `### Fixes` for a shape correction) subsection.
178
+
179
+ ### Commit, push, open the PR
180
+ 1. Stage only the files you intentionally edited (never `git add -A`):
181
+ `src/hooks/types.ts`, `src/hooks/integrations.ts`, `src/hooks/policy-evaluator.ts`,
182
+ the touched `__tests__/...` files, the touched dogfood fixtures, and
183
+ `CHANGELOG.md` — whichever actually changed.
184
+ 2. Commit (e.g. `feat: sync agent CLI harnesses with upstream docs`), then
185
+ `git push -u origin "$(git branch --show-current)"`.
186
+ 3. `gh pr create --base main --title "[auto] sync agent CLI harnesses with upstream docs" --body "<body>"`.
187
+
188
+ The PR **body** must contain, in order:
189
+
190
+ 1. **Summary table** — one row per CLI: `| CLI | scope-1 | scope-2 | scope-3 | status |`
191
+ where status is `up to date`, `drift`, or `unverified`.
192
+ 2. **Per-CLI sections** — for each CLI with drift, list the added/removed events,
193
+ tool/schema changes, and settings-shape changes.
194
+ 3. **Reviewer checklist** — one unchecked box per item that needs a human decision:
195
+ - For each newly-added event on a map-bearing CLI:
196
+ `- [ ] Add \`<event>: "???"\` to \`<MAP_NAME>\` in \`src/hooks/types.ts\` (canonical Claude \`HookEventType\` chosen by reviewer)`
197
+ - For each scope-2/scope-3 change you deferred as judgement-heavy.
198
+ 4. **Sources** — the docs URL(s) consulted per CLI.
199
+ 5. **Unverified notes** — one line per `unverified` CLI explaining why.
200
+ 6. **Final note (verbatim):**
201
+ > **CI is expected to fail on this PR if a map-bearing CLI gained new events — a
202
+ > reviewer must add the missing `*EVENT_MAP` entries (replacing `"???"`) before
203
+ > merging. For drift in Claude or Copilot only (no event map), CI should pass on
204
+ > this commit alone. CI must pass and this PR must be reviewed before merging.**
205
+
206
+ ## Constraints
207
+
208
+ - **Only edit** these paths, and only where drift requires it: `src/hooks/types.ts`,
209
+ `src/hooks/integrations.ts`, `src/hooks/policy-evaluator.ts`,
210
+ `__tests__/hooks/manager.test.ts`, `__tests__/hooks/integrations.test.ts`,
211
+ `__tests__/e2e/hooks/<cli>-integration.e2e.test.ts`, the seven dogfood fixtures
212
+ (`.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`,
213
+ `.github/hooks/failproofai.json`, `.opencode/opencode.json`,
214
+ `.opencode/plugins/failproofai.mjs`, `.pi/settings.json`, `.gemini/settings.json`),
215
+ and `CHANGELOG.md`.
216
+ - Do **NOT** edit `.failproofai/policies-config.json` (the entrypoint manages it),
217
+ `src/hooks/handler.ts`, `src/hooks/manager.ts`, or any other source file.
218
+ - Do **NOT** add entries to any `*EVENT_MAP`, `*_TOOL_MAP`, or `*_TOOL_INPUT_MAP`
219
+ for newly-added items. Removing a key when its array entry is removed IS allowed
220
+ (and required to keep the build green).
221
+ - Do **NOT** invent events, tools, or fields. If WebFetch fails or the docs don't
222
+ expose a clean list, mark the CLI `unverified` and move on.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Hermes (hermes-agent) transcript adapter — AUDIT-ONLY (Pillar 2).
3
+ *
4
+ * Hermes keeps every gateway user's sessions in one SQLite DB. We read it
5
+ * directly via the bundled sql.js reader (lib/hermes-projects.ts enumerates the
6
+ * `sessions` table; lib/hermes-sessions.ts parses each session's `messages`),
7
+ * producing the same LogEntry[] shape the other adapters do — so
8
+ * `logEntriesToEvents` handles the rest.
9
+ *
10
+ * Gateway sessions have no `cwd` (Slack/Telegram runs aren't in a repo), so they
11
+ * group by `source` (slack/telegram/cli/cron) instead of working directory.
12
+ */
13
+ import { getHermesSessions } from "../../../lib/hermes-projects";
14
+ import { getHermesSessionLog } from "../../../lib/hermes-sessions";
15
+ import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
16
+ import type { ListOpts } from "./claude";
17
+ import { logEntriesToEvents } from "./shared";
18
+
19
+ export async function listHermesTranscriptMetadata(
20
+ opts: ListOpts = {},
21
+ ): Promise<TranscriptMetadata[]> {
22
+ // `audit --project <cwd>` filters on working directory; gateway sessions have
23
+ // none, so Hermes contributes nothing to a cwd-scoped audit.
24
+ if (opts.projects && opts.projects.length > 0) return [];
25
+
26
+ const sinceMs = opts.sinceMs ?? 0;
27
+ const sessions = await getHermesSessions();
28
+ const out: TranscriptMetadata[] = [];
29
+ for (const s of sessions) {
30
+ if (s.mtimeMs < sinceMs) continue;
31
+ if (s.messageCount <= 0 && !s.hasMessages) continue; // empty → no events (message_count can lag; trust real messages)
32
+ out.push({
33
+ cli: "hermes",
34
+ // Group by channel; gateway sessions are cwd-less.
35
+ projectName: s.source ? `hermes:${s.source}` : "hermes",
36
+ sessionId: s.sessionId,
37
+ transcriptPath: `hermes://${s.sessionId}`,
38
+ mtimeMs: s.mtimeMs,
39
+ // message_count is stable for an ended session, so (mtime, size) forms a
40
+ // real per-transcript cache key — an ended session is parsed once.
41
+ sizeBytes: s.messageCount,
42
+ });
43
+ }
44
+ return out;
45
+ }
46
+
47
+ export async function streamHermesEvents(
48
+ meta: TranscriptMetadata,
49
+ ): Promise<NormalizedToolEvent[]> {
50
+ const log = await getHermesSessionLog(meta.sessionId);
51
+ if (!log) return [];
52
+ return logEntriesToEvents(log.entries, {
53
+ cli: "hermes",
54
+ sessionId: meta.sessionId,
55
+ transcriptPath: meta.transcriptPath,
56
+ cwd: log.cwd ?? "",
57
+ });
58
+ }
@@ -18,6 +18,7 @@ import { listCursorTranscriptMetadata, streamCursorEvents } from "./cursor";
18
18
  import { listOpenCodeTranscriptMetadata, streamOpenCodeEvents } from "./opencode";
19
19
  import { listPiTranscriptMetadata, streamPiEvents } from "./pi";
20
20
  import { listGeminiTranscriptMetadata, streamGeminiEvents } from "./gemini";
21
+ import { listHermesTranscriptMetadata, streamHermesEvents } from "./hermes";
21
22
 
22
23
  export type { ListOpts };
23
24
 
@@ -63,6 +64,11 @@ export const ADAPTERS: Record<IntegrationType, CliAdapter> = {
63
64
  listTranscripts: listGeminiTranscriptMetadata,
64
65
  streamEvents: streamGeminiEvents,
65
66
  },
67
+ hermes: {
68
+ cli: "hermes",
69
+ listTranscripts: listHermesTranscriptMetadata,
70
+ streamEvents: streamHermesEvents,
71
+ },
66
72
  };
67
73
 
68
74
  export function getAdapter(cli: IntegrationType): CliAdapter {
@@ -13,12 +13,14 @@ import type {
13
13
  CursorHookEventType,
14
14
  PiHookEventType,
15
15
  GeminiHookEventType,
16
+ HermesHookEventType,
16
17
  } from "./types";
17
18
  import {
18
19
  CODEX_EVENT_MAP,
19
20
  CURSOR_EVENT_MAP,
20
21
  PI_EVENT_MAP,
21
22
  GEMINI_EVENT_MAP,
23
+ HERMES_EVENT_MAP,
22
24
  } from "./types";
23
25
  import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize";
24
26
  import type { PolicyFunction, PolicyResult } from "./policy-types";
@@ -65,6 +67,12 @@ function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType
65
67
  const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType];
66
68
  if (mapped) return mapped;
67
69
  }
70
+ if (cli === "hermes") {
71
+ // Hermes sends snake_case event names (pre_tool_call, on_session_start, …);
72
+ // map to PascalCase. Has no turn-end Stop event, so no Stop mapping exists.
73
+ const mapped = HERMES_EVENT_MAP[raw as HermesHookEventType];
74
+ if (mapped) return mapped;
75
+ }
68
76
  // claude / copilot / unknown — already PascalCase, pass through.
69
77
  // HOOK_EVENT_TYPES type-checks downstream.
70
78
  return raw as HookEventType;
@@ -11,8 +11,8 @@
11
11
  */
12
12
  import * as readline from "node:readline";
13
13
  import { BUILTIN_POLICIES } from "./builtin-policies";
14
- import { detectInstalledClis, getIntegration } from "./integrations";
15
- import { INTEGRATION_TYPES, type IntegrationType } from "./types";
14
+ import { detectInstalledClis, getIntegration, listInstallableIds } from "./integrations";
15
+ import { type IntegrationType } from "./types";
16
16
  import { trackHookEvent } from "./hook-telemetry";
17
17
  import { getInstanceId } from "../../lib/telemetry-id";
18
18
 
@@ -139,7 +139,11 @@ export function buildCliMenuOptions(
139
139
  ): { options: CliMenuOption[]; undetected: IntegrationType[] } {
140
140
  const undetected: IntegrationType[] =
141
141
  action === "install"
142
- ? INTEGRATION_TYPES.filter((id) => !detected.includes(id))
142
+ ? // Only CLIs that support live-hook install — a future audit-only CLI
143
+ // (one with no INTEGRATIONS entry) has no install path and must not
144
+ // appear as a forward-install option. (hermes IS installable, so it
145
+ // correctly appears here.)
146
+ listInstallableIds().filter((id) => !detected.includes(id))
143
147
  : [];
144
148
 
145
149
  const options: CliMenuOption[] = [];
@@ -11,6 +11,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "
11
11
  import { resolve, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { homedir } from "node:os";
14
+ import { parseDocument, type Document } from "yaml";
14
15
  import {
15
16
  HOOK_EVENT_TYPES,
16
17
  HOOK_SCOPES,
@@ -27,6 +28,8 @@ import {
27
28
  PI_HOOK_SCOPES,
28
29
  GEMINI_HOOK_EVENT_TYPES,
29
30
  GEMINI_HOOK_SCOPES,
31
+ HERMES_HOOK_EVENT_TYPES,
32
+ HERMES_HOOK_SCOPES,
30
33
  FAILPROOFAI_HOOK_MARKER,
31
34
  INTEGRATION_TYPES,
32
35
  type IntegrationType,
@@ -50,6 +53,23 @@ function writeJsonFile(path: string, data: Record<string, unknown>): void {
50
53
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
51
54
  }
52
55
 
56
+ /** Read a YAML file as a `Document` so writes round-trip the user's other keys +
57
+ * comments (a plain parse→stringify would strip comments). Empty / missing /
58
+ * corrupt → an empty Document. Used by the Hermes integration, whose config
59
+ * lives in `~/.hermes/config.yaml`. */
60
+ function readYamlDoc(path: string): Document {
61
+ try {
62
+ return existsSync(path) ? parseDocument(readFileSync(path, "utf8")) : parseDocument("");
63
+ } catch {
64
+ return parseDocument("");
65
+ }
66
+ }
67
+
68
+ function writeYamlDoc(path: string, doc: Document): void {
69
+ mkdirSync(dirname(path), { recursive: true });
70
+ writeFileSync(path, doc.toString(), "utf8");
71
+ }
72
+
53
73
  function isMarkedHook(hook: unknown): boolean {
54
74
  if (!hook || typeof hook !== "object") return false;
55
75
  const h = hook as Record<string, unknown>;
@@ -154,7 +174,10 @@ export const claudeCode: Integration = {
154
174
  return {
155
175
  type: "command",
156
176
  command,
157
- timeout: 60_000,
177
+ // Claude reads `timeout` in SECONDS per https://code.claude.com/docs/en/hooks
178
+ // ("Seconds before canceling. Defaults: 600 for command ...; 60 for agent"),
179
+ // NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h. (#482-class unit fix)
180
+ timeout: 60,
158
181
  [FAILPROOFAI_HOOK_MARKER]: true,
159
182
  };
160
183
  },
@@ -285,8 +308,10 @@ export const codex: Integration = {
285
308
  : `"${binaryPath}" --hook ${eventType} --cli codex`;
286
309
  return {
287
310
  type: "command",
288
- // Codex reads `timeout` in SECONDS (its `timeout_sec` field, default 600)
289
- // NOT milliseconds like Claude/Cursor/Gemini. 60 = 60s, not 60000ms (~16.7h).
311
+ // Codex reads `timeout` in SECONDS (the field is literally `timeout`,
312
+ // 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).
290
315
  command,
291
316
  timeout: 60,
292
317
  [FAILPROOFAI_HOOK_MARKER]: true,
@@ -591,11 +616,13 @@ export const cursor: Integration = {
591
616
  scope === "project"
592
617
  ? `npx -y failproofai --hook ${eventType} --cli cursor`
593
618
  : `"${binaryPath}" --hook ${eventType} --cli cursor`;
594
- // `timeout` is documented as ms in Cursor's schema (matches Claude).
619
+ // `timeout` is documented in SECONDS in Cursor's schema per
620
+ // https://cursor.com/docs/hooks ("Execution timeout in seconds"; doc examples
621
+ // use 30 and 10), NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h.
595
622
  return {
596
623
  type: "command",
597
624
  command,
598
- timeout: 60_000,
625
+ timeout: 60,
599
626
  [FAILPROOFAI_HOOK_MARKER]: true,
600
627
  };
601
628
  },
@@ -1416,9 +1443,146 @@ export const gemini: Integration = {
1416
1443
  },
1417
1444
  };
1418
1445
 
1446
+ // ── Hermes (hermes-agent) — live hooks (Pillar 1) ────────────────────────────
1447
+ //
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;
1460
+ }
1461
+
1462
+ export const hermes: Integration = {
1463
+ id: "hermes",
1464
+ displayName: "Hermes",
1465
+ scopes: HERMES_HOOK_SCOPES,
1466
+ eventTypes: HERMES_HOOK_EVENT_TYPES,
1467
+
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");
1472
+ },
1473
+
1474
+ 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>;
1478
+ },
1479
+
1480
+ writeSettings(settingsPath, settings) {
1481
+ writeYamlDoc(settingsPath, settings as unknown as Document);
1482
+ },
1483
+
1484
+ 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
+ const command =
1489
+ scope === "project"
1490
+ ? `npx -y failproofai --hook ${eventType} --cli hermes`
1491
+ : `"${binaryPath}" --hook ${eventType} --cli hermes`;
1492
+ return {
1493
+ command,
1494
+ timeout: 30,
1495
+ [FAILPROOFAI_HOOK_MARKER]: true,
1496
+ };
1497
+ },
1498
+
1499
+ isFailproofaiHook: isMarkedHook,
1500
+
1501
+ 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 : {};
1508
+
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;
1516
+ }
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
+ },
1524
+
1525
+ 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;
1530
+
1531
+ 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));
1538
+ removed += before - filtered.length;
1539
+ if (filtered.length === 0) delete hooks[eventType];
1540
+ else hooks[eventType] = filtered;
1541
+ }
1542
+ if (removed > 0) {
1543
+ if (Object.keys(hooks).length === 0) doc.delete("hooks");
1544
+ else doc.set("hooks", hooks);
1545
+ }
1546
+ }
1547
+
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);
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 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;
1566
+ }
1567
+ } catch {
1568
+ // Corrupt config — treat as not installed.
1569
+ }
1570
+ return false;
1571
+ },
1572
+
1573
+ detectInstalled() {
1574
+ return binaryExists("hermes");
1575
+ },
1576
+ };
1577
+
1419
1578
  // ── Registry ────────────────────────────────────────────────────────────────
1420
1579
 
1421
- const INTEGRATIONS: Record<IntegrationType, Integration> = {
1580
+ // `Partial` is kept (not every IntegrationType is guaranteed installable for
1581
+ // LIVE hooks / Pillar 1) so a future audit-only CLI can omit its entry without a
1582
+ // type error. `hermes` now has BOTH an audit adapter
1583
+ // (src/audit/cli-adapters/hermes.ts) AND live-hook install support, so it is
1584
+ // registered here.
1585
+ const INTEGRATIONS: Partial<Record<IntegrationType, Integration>> = {
1422
1586
  claude: claudeCode,
1423
1587
  codex,
1424
1588
  copilot,
@@ -1426,21 +1590,38 @@ const INTEGRATIONS: Record<IntegrationType, Integration> = {
1426
1590
  opencode,
1427
1591
  pi,
1428
1592
  gemini,
1593
+ hermes,
1429
1594
  };
1430
1595
 
1431
1596
  export function getIntegration(id: IntegrationType): Integration {
1432
1597
  const integration = INTEGRATIONS[id];
1433
1598
  if (!integration) {
1434
- throw new Error(`Unknown integration: ${id}. Valid: ${INTEGRATION_TYPES.join(", ")}`);
1599
+ // A future audit-only CLI (one with an audit adapter but no INTEGRATIONS
1600
+ // entry) reaches here when someone tries to install live hooks for it. Be
1601
+ // explicit rather than "unknown integration". (hermes is NOT audit-only — it
1602
+ // has a live-hook entry — so it never reaches this branch.)
1603
+ throw new Error(
1604
+ `"${id}" is audit-only — live-hook install is not supported for it yet. Installable: ${listInstallableIds().join(", ")}`,
1605
+ );
1435
1606
  }
1436
1607
  return integration;
1437
1608
  }
1438
1609
 
1610
+ /** IntegrationTypes that support live-hook install (i.e. have an INTEGRATIONS
1611
+ * entry). Any audit-only CLI (one with an audit adapter but no hook install)
1612
+ * is excluded so it never appears in install menus. */
1613
+ export function listInstallableIds(): IntegrationType[] {
1614
+ return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id] !== undefined);
1615
+ }
1616
+
1439
1617
  export function listIntegrations(): Integration[] {
1440
- return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id]);
1618
+ return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id]).filter(
1619
+ (i): i is Integration => i !== undefined,
1620
+ );
1441
1621
  }
1442
1622
 
1443
- /** Detect which agent CLIs are installed on PATH. */
1623
+ /** Detect which agent CLIs are installed on PATH. Only considers CLIs that
1624
+ * support live-hook install (audit-only CLIs have no INTEGRATIONS entry). */
1444
1625
  export function detectInstalledClis(): IntegrationType[] {
1445
- return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id].detectInstalled());
1626
+ return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id]?.detectInstalled());
1446
1627
  }