failproofai 0.0.13-beta.0 → 0.0.13-beta.2

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 (198) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -2
  31. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
  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 +2 -2
  37. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
  39. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
  41. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  46. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/index.html +1 -1
  49. package/.next/standalone/.next/server/app/index.rsc +15 -15
  50. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  52. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  53. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  54. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  55. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  56. package/.next/standalone/.next/server/app/page.js +1 -1
  57. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  60. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  61. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
  65. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  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 +3 -2
  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/server-reference-manifest.json +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  74. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hv08mx._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0obaznk._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xvsnvt._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hkdkmz._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1i9rknc._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0cycwg6._.js → [root-of-the-server]__1tvn4wf._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/_1z062h5._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
  93. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bbpv2d._.js → [root-of-the-server]__09u1pcy._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +3 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1uvfwgr._.js → [root-of-the-server]__0hdsupo._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +4 -0
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ehe24g._.js → [root-of-the-server]__0ynf7tx._.js} +3 -3
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +245 -0
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-m1rhq._.js → [root-of-the-server]__1rlmypf._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/_0ul5rf_._.js +3 -0
  112. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1u91ynr.js → node_modules_html-to-image_es_index_09fe3kb.js} +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  122. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  123. package/.next/standalone/.next/server/pages/404.html +1 -1
  124. package/.next/standalone/.next/server/pages/500.html +1 -1
  125. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  126. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  127. package/.next/standalone/.next/static/chunks/{39rn602wszcbd.js → 06cw8nbn5c4_r.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/0fa7j157kg9_d.js +1 -0
  129. package/.next/standalone/.next/static/chunks/{03zg3ms7uaoq3.js → 0i0joc040j999.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{1h569gia53ant.js → 0l2n5ftar65ox.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/1c8i78p7t1i-5.js +1 -0
  132. package/.next/standalone/.next/static/chunks/{2ks4pzu5b-l_q.js → 1sg8kx5szx42y.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{3jka0hvhqouyv.js → 23t-i9s-q--nu.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{0byua6ng1f_ka.js → 25qvspmcbb_5y.js} +1 -1
  135. package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +2 -0
  136. package/.next/standalone/.next/static/chunks/{41mhnhfkmglpp.js → 2a-k91nsa1lgf.js} +1 -1
  137. package/.next/standalone/.next/static/chunks/{0azkpcy8eb1gr.js → 2oz8ok8fzxaj3.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{1zxtyef-iqede.js → 2rkvn4znvuy4o.js} +1 -1
  139. package/.next/standalone/app/components/sessions-list.tsx +19 -0
  140. package/.next/standalone/app/project/[name]/page.tsx +9 -4
  141. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +26 -5
  142. package/.next/standalone/assets/logos/hermes-dark.svg +1 -0
  143. package/.next/standalone/assets/logos/hermes-light.svg +1 -0
  144. package/.next/standalone/lib/cli-registry.ts +6 -1
  145. package/.next/standalone/lib/download-session.ts +16 -0
  146. package/.next/standalone/lib/hermes-projects.ts +164 -0
  147. package/.next/standalone/lib/hermes-sessions.ts +273 -0
  148. package/.next/standalone/lib/projects.ts +14 -3
  149. package/.next/standalone/lib/sqlite-reader.ts +102 -0
  150. package/.next/standalone/package.json +5 -3
  151. package/.next/standalone/server.js +1 -1
  152. package/README.md +10 -1
  153. package/bin/failproofai.mjs +17 -16
  154. package/dist/cli.mjs +7774 -225
  155. package/lib/cli-registry.ts +6 -1
  156. package/lib/download-session.ts +16 -0
  157. package/lib/hermes-projects.ts +164 -0
  158. package/lib/hermes-sessions.ts +273 -0
  159. package/lib/projects.ts +14 -3
  160. package/lib/sql-js-asm.d.ts +26 -0
  161. package/lib/sqlite-reader.ts +102 -0
  162. package/package.json +5 -3
  163. package/src/audit/cli-adapters/hermes.ts +58 -0
  164. package/src/audit/cli-adapters/index.ts +6 -0
  165. package/src/hooks/builtin-policies.ts +16 -0
  166. package/src/hooks/handler.ts +8 -0
  167. package/src/hooks/install-prompt.ts +7 -3
  168. package/src/hooks/integrations.ts +179 -5
  169. package/src/hooks/policy-evaluator.ts +42 -0
  170. package/src/hooks/resolve-transcript-path.ts +4 -0
  171. package/src/hooks/tool-name-canonicalize.ts +6 -0
  172. package/src/hooks/types.ts +67 -1
  173. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dwpg-h._.js +0 -3
  174. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lnenda._.js +0 -3
  175. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13i_sva._.js +0 -3
  176. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +0 -3
  177. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m2_4t0._.js +0 -3
  178. package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +0 -3
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +0 -4
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e4-6d8._.js +0 -4
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f62vu9._.js +0 -3
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +0 -4
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0k65l27._.js +0 -3
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +0 -4
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +0 -4
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e-x7j4._.js +0 -3
  191. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +0 -221
  192. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +0 -4
  193. package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +0 -1
  194. package/.next/standalone/.next/static/chunks/1o0xa47736gi9.css +0 -2
  195. package/.next/standalone/.next/static/chunks/2wbuxnsvux4di.js +0 -1
  196. /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → HgG0mizCfOM3XyN1EW-sL}/_buildManifest.js +0 -0
  197. /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → HgG0mizCfOM3XyN1EW-sL}/_clientMiddlewareManifest.js +0 -0
  198. /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → HgG0mizCfOM3XyN1EW-sL}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.13-beta.0",
3
+ "version": "0.0.13-beta.2",
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,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 {
@@ -1088,7 +1088,19 @@ function warnBackgroundProcess(ctx: PolicyContext): PolicyResult {
1088
1088
 
1089
1089
  // -- Workflow (Stop event) policies --
1090
1090
 
1091
+ /**
1092
+ * Claude Code plan mode (permission_mode: "plan") is research-and-plan-only — the
1093
+ * agent makes no commits, pushes, or PRs by design. The Stop-workflow gates below
1094
+ * all assume the agent produced code changes, so in plan mode they demand actions
1095
+ * plan mode forbids (e.g. a push with nothing to push). Skip them there. Only Claude
1096
+ * reports "plan" today; every other CLI resolves to "default".
1097
+ */
1098
+ function isPlanMode(ctx: PolicyContext): boolean {
1099
+ return ctx.session?.permissionMode === "plan";
1100
+ }
1101
+
1091
1102
  function requireCommitBeforeStop(ctx: PolicyContext): PolicyResult {
1103
+ if (isPlanMode(ctx)) return allow("Plan mode — no changes made, skipping commit check.");
1092
1104
  const cwd = ctx.session?.cwd;
1093
1105
  if (!cwd) return allow("No working directory available, skipping commit check.");
1094
1106
 
@@ -1111,6 +1123,7 @@ function requireCommitBeforeStop(ctx: PolicyContext): PolicyResult {
1111
1123
  }
1112
1124
 
1113
1125
  function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
1126
+ if (isPlanMode(ctx)) return allow("Plan mode — no changes made, skipping push check.");
1114
1127
  const cwd = ctx.session?.cwd;
1115
1128
  if (!cwd) return allow("No working directory available, skipping push check.");
1116
1129
 
@@ -1205,6 +1218,7 @@ function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
1205
1218
  }
1206
1219
 
1207
1220
  function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1221
+ if (isPlanMode(ctx)) return allow("Plan mode — no changes made, skipping PR check.");
1208
1222
  const cwd = ctx.session?.cwd;
1209
1223
  if (!cwd) return allow("No working directory available, skipping PR check.");
1210
1224
 
@@ -1298,6 +1312,7 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1298
1312
  }
1299
1313
 
1300
1314
  function requireNoConflictsBeforeStop(ctx: PolicyContext): PolicyResult {
1315
+ if (isPlanMode(ctx)) return allow("Plan mode — no changes made, skipping conflict check.");
1301
1316
  const cwd = ctx.session?.cwd;
1302
1317
  if (!cwd) return allow("No working directory available, skipping conflict check.");
1303
1318
 
@@ -1396,6 +1411,7 @@ function requireNoConflictsBeforeStop(ctx: PolicyContext): PolicyResult {
1396
1411
  }
1397
1412
 
1398
1413
  function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1414
+ if (isPlanMode(ctx)) return allow("Plan mode — no changes made, skipping CI check.");
1399
1415
  const cwd = ctx.session?.cwd;
1400
1416
  if (!cwd) return allow("No working directory available, skipping CI check.");
1401
1417
 
@@ -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>;
@@ -1423,9 +1443,146 @@ export const gemini: Integration = {
1423
1443
  },
1424
1444
  };
1425
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
+
1426
1578
  // ── Registry ────────────────────────────────────────────────────────────────
1427
1579
 
1428
- 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>> = {
1429
1586
  claude: claudeCode,
1430
1587
  codex,
1431
1588
  copilot,
@@ -1433,21 +1590,38 @@ const INTEGRATIONS: Record<IntegrationType, Integration> = {
1433
1590
  opencode,
1434
1591
  pi,
1435
1592
  gemini,
1593
+ hermes,
1436
1594
  };
1437
1595
 
1438
1596
  export function getIntegration(id: IntegrationType): Integration {
1439
1597
  const integration = INTEGRATIONS[id];
1440
1598
  if (!integration) {
1441
- 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
+ );
1442
1606
  }
1443
1607
  return integration;
1444
1608
  }
1445
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
+
1446
1617
  export function listIntegrations(): Integration[] {
1447
- return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id]);
1618
+ return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id]).filter(
1619
+ (i): i is Integration => i !== undefined,
1620
+ );
1448
1621
  }
1449
1622
 
1450
- /** 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). */
1451
1625
  export function detectInstalledClis(): IntegrationType[] {
1452
- return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id].detectInstalled());
1626
+ return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id]?.detectInstalled());
1453
1627
  }
@@ -256,6 +256,25 @@ export async function evaluatePolicies(
256
256
  };
257
257
  }
258
258
 
259
+ // Hermes: the block contract is `{"decision":"block","reason"}` on stdout;
260
+ // Hermes IGNORES exit codes, so the JSON is the only channel. This one
261
+ // return covers every Hermes event we install (PreToolUse / PostToolUse /
262
+ // SubagentStop) — there is no Stop/turn-end event to special-case (Hermes
263
+ // has no turn boundary; the 5 require-*-before-stop builtins never fire for
264
+ // it — see CLAUDE.md / the audit plan). A block on PreToolUse stops the
265
+ // tool before it runs, regardless of the originating platform
266
+ // (slack/telegram/cli/cron) or subagent.
267
+ if (session?.cli === "hermes") {
268
+ return {
269
+ exitCode: 0,
270
+ stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
271
+ stderr: "",
272
+ policyName: policy.name,
273
+ reason,
274
+ decision: "deny",
275
+ };
276
+ }
277
+
259
278
  // OpenCode: `session.idle` is a notification-only bus event — by the
260
279
  // time the plugin handler fires, OpenCode has already gone idle and
261
280
  // throwing from the handler does not force-retry. The only working
@@ -552,6 +571,29 @@ export async function evaluatePolicies(
552
571
  };
553
572
  }
554
573
 
574
+ // Hermes: no additional-context channel on any event (the only actionable
575
+ // response is `{"decision":"block"}`). So instruct degrades to allow +
576
+ // log — we emit a non-blocking `{decision:"allow", reason}` (Hermes lets
577
+ // the tool run) and surface the note on stderr for the operator's logs.
578
+ // Documented limitation; there is no Stop event to force-retry into.
579
+ if (session?.cli === "hermes") {
580
+ const stderrMsg = instructEntries
581
+ .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
582
+ .join("\n");
583
+ return {
584
+ exitCode: 0,
585
+ stdout: JSON.stringify({
586
+ decision: "allow",
587
+ reason: `Instruction from failproofai: ${combined}`,
588
+ }),
589
+ stderr: stderrMsg + "\n",
590
+ policyName: policyNames[0],
591
+ policyNames,
592
+ reason: combined,
593
+ decision: "instruct",
594
+ };
595
+ }
596
+
555
597
  // OpenCode: same rationale as the deny branch above — emit
556
598
  // additionalContext so the shim submits a follow-up via
557
599
  // client.session.prompt instead of throwing into a dead handler.
@@ -63,6 +63,10 @@ export function resolveTranscriptPath(
63
63
  return findGeminiTranscript(sessionId) ?? undefined;
64
64
  case "opencode":
65
65
  return `opencode-db://${sessionId}`;
66
+ case "hermes":
67
+ // Hermes live-hook payloads carry no transcript file; the audit path
68
+ // reads sessions straight from ~/.hermes/state.db (see hermes-sessions.ts).
69
+ return undefined;
66
70
  default:
67
71
  return undefined;
68
72
  }
@@ -15,6 +15,8 @@ import {
15
15
  PI_TOOL_MAP,
16
16
  PI_TOOL_INPUT_MAP,
17
17
  GEMINI_TOOL_MAP,
18
+ HERMES_TOOL_MAP,
19
+ HERMES_TOOL_INPUT_MAP,
18
20
  } from "./types";
19
21
 
20
22
  /**
@@ -33,6 +35,7 @@ export function canonicalizeToolName(
33
35
  if (cli === "gemini") return GEMINI_TOOL_MAP[raw] ?? raw;
34
36
  if (cli === "opencode") return OPENCODE_TOOL_MAP[raw] ?? raw;
35
37
  if (cli === "pi") return PI_TOOL_MAP[raw] ?? raw;
38
+ if (cli === "hermes") return HERMES_TOOL_MAP[raw] ?? raw;
36
39
  return raw;
37
40
  }
38
41
 
@@ -56,6 +59,9 @@ export function canonicalizeToolInput(
56
59
  let perToolMap: Record<string, string> | undefined;
57
60
  if (cli === "opencode") perToolMap = OPENCODE_TOOL_INPUT_MAP[toolName];
58
61
  else if (cli === "pi") perToolMap = PI_TOOL_INPUT_MAP[toolName];
62
+ // Hermes read_file/write_file/patch deliver the file path as `path`; map it to
63
+ // `file_path` so path/content builtins fire (verified against a live state.db).
64
+ else if (cli === "hermes") perToolMap = HERMES_TOOL_INPUT_MAP[toolName];
59
65
  if (!perToolMap) return rawInput;
60
66
  const out: Record<string, unknown> = {};
61
67
  for (const [k, v] of Object.entries(rawInput as Record<string, unknown>)) {
@@ -5,7 +5,7 @@
5
5
  export const HOOK_SCOPES = ["user", "project", "local"] as const;
6
6
  export type HookScope = (typeof HOOK_SCOPES)[number];
7
7
 
8
- export const INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"] as const;
8
+ export const INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"] as const;
9
9
  export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
10
10
 
11
11
  export const CODEX_HOOK_SCOPES = ["user", "project"] as const;
@@ -61,6 +61,72 @@ export const CODEX_TOOL_MAP: Record<string, string> = {
61
61
  write_stdin: "Bash",
62
62
  };
63
63
 
64
+ // ── Hermes (hermes-agent) ───────────────────────────────────────────────────
65
+ //
66
+ // Hermes supports BOTH audit (Pillar 2) and live hooks (Pillar 1). This tool
67
+ // map is consumed by the audit adapter (via `logEntriesToEvents`) AND the
68
+ // live-hook handler, both through `canonicalizeToolName`. Tool names are the
69
+ // granular toolset tools verified
70
+ // against a live ~/.hermes/state.db (frequency in a real gateway session:
71
+ // terminal 574, read_file 124, patch 94, write_file 54, web_search 42, …).
72
+ // Names with a Claude canonical are mapped so builtin policies fire; Hermes-
73
+ // specific tools (skill_view, cronjob, browser_*, memory, session_search,
74
+ // clarify, process) pass through unchanged so they still appear in the audit,
75
+ // just unmatched by builtin policies.
76
+ export const HERMES_TOOL_MAP: Record<string, string> = {
77
+ terminal: "Bash",
78
+ bash: "Bash",
79
+ read_file: "Read",
80
+ write_file: "Write",
81
+ patch: "Edit",
82
+ web_search: "WebSearch",
83
+ web_extract: "WebFetch",
84
+ search_files: "Grep",
85
+ todo: "TodoWrite",
86
+ };
87
+
88
+ // Hermes tool-INPUT key canonicalization, keyed by the *canonical* tool name
89
+ // (the handler canonicalizes the name before calling canonicalizeToolInput).
90
+ // Verified against a live ~/.hermes/state.db: read_file / write_file / patch
91
+ // deliver the file path as `path`, but Claude builtins read `file_path`
92
+ // (block-env-files, block-secrets-write, block-read-outside-cwd) — so map it.
93
+ // write_file's `content`, patch's `old_string`/`new_string`, and search_files'
94
+ // `pattern`/`path` are already canonical, so Grep needs no entry. Mirrors
95
+ // PI_TOOL_INPUT_MAP (Pi has the same `path` → `file_path` shape).
96
+ export const HERMES_TOOL_INPUT_MAP: Record<string, Record<string, string>> = {
97
+ Read: { path: "file_path" },
98
+ Write: { path: "file_path" },
99
+ Edit: { path: "file_path" },
100
+ };
101
+
102
+ // Hermes live-hook (Pillar 1) events + scopes. Hermes fires these snake_case
103
+ // events with a JSON payload on stdin; the command we install runs
104
+ // `failproofai --hook <event> --cli hermes`. Config is USER-scope only
105
+ // (`~/.hermes/config.yaml`; Hermes has no project scope). `pre_tool_call` is the
106
+ // core deny point — it fires for tool calls from every source
107
+ // (slack/telegram/cli/cron) and internal subagents, so a single install
108
+ // intercepts all platforms. Hermes has NO turn-end `Stop` event, so the
109
+ // `require-*-before-stop` builtins never fire for it (see the audit plan).
110
+ export const HERMES_HOOK_SCOPES = ["user"] as const;
111
+ export type HermesHookScope = (typeof HERMES_HOOK_SCOPES)[number];
112
+
113
+ export const HERMES_HOOK_EVENT_TYPES = [
114
+ "pre_tool_call",
115
+ "post_tool_call",
116
+ "on_session_start",
117
+ "on_session_end",
118
+ "subagent_stop",
119
+ ] as const;
120
+ export type HermesHookEventType = (typeof HERMES_HOOK_EVENT_TYPES)[number];
121
+
122
+ export const HERMES_EVENT_MAP: Record<HermesHookEventType, HookEventType> = {
123
+ pre_tool_call: "PreToolUse",
124
+ post_tool_call: "PostToolUse",
125
+ on_session_start: "SessionStart",
126
+ on_session_end: "SessionEnd",
127
+ subagent_stop: "SubagentStop",
128
+ };
129
+
64
130
  // ── GitHub Copilot CLI ─────────────────────────────────────────────────────
65
131
  //
66
132
  // Copilot CLI accepts two payload formats. We install with PascalCase event