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.
- package/.next/standalone/.cursor/hooks.json +6 -6
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -2
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +1 -1
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hv08mx._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0obaznk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xvsnvt._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hkdkmz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1i9rknc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0cycwg6._.js → [root-of-the-server]__1tvn4wf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1z062h5._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ntd0fz._.js → [root-of-the-server]__099afy6._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ehe24g._.js → [root-of-the-server]__0ynf7tx._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +245 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vh88nr._.js → [root-of-the-server]__1qlh4jx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0ul5rf_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +3 -0
- 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
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{3jk5y-qgnnti_.js → 0b6rdtcge_3ec.js} +1 -1
- package/.next/standalone/.next/static/chunks/{09l7dfsd4xapi.js → 0pj_6lbbjd000.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ys-mwok1j9kp.js → 0xnrgonfdeivm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{00_ypuitb29l0.js → 105w_oznq-van.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3pb3m-89k7ts2.js → 13tbm6i1swiru.js} +1 -1
- package/.next/standalone/.next/static/chunks/{36yb_gsfgfga4.js → 1fhuj4fjovzp5.js} +1 -1
- package/.next/standalone/.next/static/chunks/209ful1jrphvy.js +1 -0
- package/.next/standalone/.next/static/chunks/{0qw65r3wfs089.js → 23odnajn2ltwu.js} +1 -1
- package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +2 -0
- package/.next/standalone/.next/static/chunks/36r2vgi6854za.js +6 -0
- package/.next/standalone/.next/static/chunks/3b77kulbdbrqw.js +1 -0
- package/.next/standalone/.next/static/chunks/3rjg-1s5ouu5r.js +1 -0
- package/.next/standalone/app/components/sessions-list.tsx +19 -0
- package/.next/standalone/app/project/[name]/page.tsx +9 -4
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +26 -5
- package/.next/standalone/assets/logos/hermes-dark.svg +1 -0
- package/.next/standalone/assets/logos/hermes-light.svg +1 -0
- package/.next/standalone/docker-hook-sync/Dockerfile +75 -0
- package/.next/standalone/docker-hook-sync/entrypoint.sh +109 -0
- package/.next/standalone/lib/cli-registry.ts +6 -1
- package/.next/standalone/lib/download-session.ts +16 -0
- package/.next/standalone/lib/hermes-projects.ts +164 -0
- package/.next/standalone/lib/hermes-sessions.ts +273 -0
- package/.next/standalone/lib/projects.ts +14 -3
- package/.next/standalone/lib/sqlite-reader.ts +102 -0
- package/.next/standalone/package.json +5 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +10 -1
- package/bin/failproofai.mjs +17 -16
- package/dist/cli.mjs +7779 -229
- package/lib/cli-registry.ts +6 -1
- package/lib/download-session.ts +16 -0
- package/lib/hermes-projects.ts +164 -0
- package/lib/hermes-sessions.ts +273 -0
- package/lib/projects.ts +14 -3
- package/lib/sql-js-asm.d.ts +26 -0
- package/lib/sqlite-reader.ts +102 -0
- package/package.json +5 -3
- package/scripts/sync-agent-cli-harnesses-prompt.md +222 -0
- package/src/audit/cli-adapters/hermes.ts +58 -0
- package/src/audit/cli-adapters/index.ts +6 -0
- package/src/hooks/handler.ts +8 -0
- package/src/hooks/install-prompt.ts +7 -3
- package/src/hooks/integrations.ts +191 -10
- package/src/hooks/policy-evaluator.ts +42 -0
- package/src/hooks/resolve-transcript-path.ts +4 -0
- package/src/hooks/tool-name-canonicalize.ts +6 -0
- package/src/hooks/types.ts +107 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dwpg-h._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lnenda._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13i_sva._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m2_4t0._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e4-6d8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f62vu9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0k65l27._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e-x7j4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +0 -221
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1uvfwgr._.js +0 -4
- package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +0 -1
- package/.next/standalone/.next/static/chunks/1o0xa47736gi9.css +0 -2
- package/.next/standalone/.next/static/chunks/2ey-m4-p9z7e0.js +0 -6
- package/.next/standalone/.next/static/chunks/2wbuxnsvux4di.js +0 -1
- package/.next/standalone/.next/static/chunks/3-veva26100-4.js +0 -1
- package/scripts/sync-hook-events-prompt.md +0 -179
- /package/.next/standalone/.next/static/{PdZTBvCBXzDRLe-duL9yC → 31ZoDDYKHEIN_TrgKv85t}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PdZTBvCBXzDRLe-duL9yC → 31ZoDDYKHEIN_TrgKv85t}/_clientMiddlewareManifest.js +0 -0
- /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.
|
|
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 {
|
package/src/hooks/handler.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
?
|
|
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
|
|
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 (
|
|
289
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
1626
|
+
return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id]?.detectInstalled());
|
|
1446
1627
|
}
|