failproofai 1.0.2-beta.3 → 1.0.2-beta.5

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 (210) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  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/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.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 +6 -6
  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/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -3
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -6
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -4
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  39. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/index.html +1 -1
  43. package/.next/standalone/.next/server/app/index.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  45. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  46. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  48. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  49. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  52. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  53. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  54. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  57. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +3 -3
  72. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0e32-7c._.js → [externals]__1qzqcio._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03r-lqq._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0igxt32._.js → [root-of-the-server]__1p8poxd._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zkek3z._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/_12moi5-._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +6 -5
  89. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js} +1 -1
  94. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1m84s36._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +4 -4
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__08zpry-._.js → [root-of-the-server]__0stn0gx._.js} +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/{src_hooks_08o7bqo._.js → [root-of-the-server]__15r6r7w._.js} +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tag8_-._.js +5 -0
  113. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
  116. package/.next/standalone/.next/server/chunks/ssr/{_11opd9e._.js → _1kesqs-._.js} +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/{_15xtekm._.js → _1km9-gn._.js} +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/lib_1cf5ya2._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  128. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  129. package/.next/standalone/.next/server/middleware.js +1 -1
  130. package/.next/standalone/.next/server/pages/404.html +1 -1
  131. package/.next/standalone/.next/server/pages/500.html +1 -1
  132. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  134. package/.next/standalone/.next/static/chunks/0tyt-3a5rtzy8.js +1 -0
  135. package/.next/standalone/.next/static/chunks/{1mk3vmmc673hu.js → 21r7iw3rmixf5.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/{10pok-mrqv426.js → 26lfjhb2kzzgb.js} +1 -1
  137. package/.next/standalone/.next/static/chunks/2_27jraat038x.js +1 -0
  138. package/.next/standalone/.next/static/chunks/2hwc8e923g5df.js +1 -0
  139. package/.next/standalone/.next/static/chunks/{3cs_rf5p765v0.js → 33np9nrh1mvj9.js} +1 -1
  140. package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 354-n7p7labpn.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/36-8i_vbkh85w.js +1 -0
  142. package/.next/standalone/.next/static/chunks/{2jsomd79g7kn0.js → 391tpdqcas7rq.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/{1yywl_a87108w.js → 3_b73tc78jwsq.js} +2 -2
  144. package/.next/standalone/.next/static/chunks/{1za08zjcctyil.js → 3i8rg2fv9nd6e.js} +2 -2
  145. package/.next/standalone/.next/static/chunks/{2ey_ih6j6cm15.js → 3jqm8ubpv3i-4.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/turbopack-2x-o099q64i8h.js +1 -0
  147. package/.next/standalone/SECURITY.md +18 -3
  148. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +7 -0
  149. package/.next/standalone/fp-cloud-cli/uv.lock +6 -6
  150. package/.next/standalone/lib/opencode-db.ts +77 -0
  151. package/.next/standalone/lib/opencode-projects.ts +37 -41
  152. package/.next/standalone/lib/opencode-sessions.ts +59 -36
  153. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  154. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  155. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache.js +23 -34
  156. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  157. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  158. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
  159. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  160. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  161. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  162. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  163. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  164. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  165. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  166. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  167. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  168. package/.next/standalone/node_modules/next/package.json +10 -10
  169. package/.next/standalone/package.json +19 -19
  170. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  171. package/.next/standalone/sdk/python/uv.lock +23 -23
  172. package/.next/standalone/server.js +1 -1
  173. package/bin/failproofai.mjs +151 -60
  174. package/dist/cli.mjs +3670 -3005
  175. package/dist/worker.mjs +189 -164
  176. package/lib/opencode-db.ts +77 -0
  177. package/lib/opencode-projects.ts +37 -41
  178. package/lib/opencode-sessions.ts +59 -36
  179. package/package.json +19 -19
  180. package/src/audit/cache.ts +201 -40
  181. package/src/audit/cli-adapters/claude.ts +111 -1
  182. package/src/audit/cli-adapters/index.ts +31 -1
  183. package/src/audit/index.ts +131 -11
  184. package/src/audit/replay.ts +15 -91
  185. package/src/hooks/configure-wizard.ts +16 -48
  186. package/src/hooks/handler.ts +8 -1
  187. package/src/hooks/manager.ts +72 -3
  188. package/src/hooks/pack-cli.ts +793 -45
  189. package/src/hooks/pack-manifest.ts +28 -0
  190. package/src/hooks/pack-store.ts +8 -25
  191. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
  192. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +0 -3
  193. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +0 -3
  194. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
  195. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
  196. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
  197. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
  198. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +0 -3
  199. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +0 -3
  200. package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
  201. package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +0 -12
  202. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +0 -5
  203. package/.next/standalone/.next/static/chunks/01nuy-aqshahr.js +0 -1
  204. package/.next/standalone/.next/static/chunks/1pl7h8vllvoi2.js +0 -1
  205. package/.next/standalone/.next/static/chunks/2efnffq2ipgr_.js +0 -1
  206. package/.next/standalone/.next/static/chunks/2gd07pinz0i7f.js +0 -1
  207. package/.next/standalone/.next/static/chunks/turbopack-3kjgdgwci4tqf.js +0 -1
  208. /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_buildManifest.js +0 -0
  209. /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_clientMiddlewareManifest.js +0 -0
  210. /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_ssgManifest.js +0 -0
@@ -15,7 +15,7 @@ import { INTEGRATION_TYPES, type IntegrationType } from "../hooks/types";
15
15
  import { ADAPTERS } from "./cli-adapters";
16
16
  import { AUDIT_DETECTORS } from "./detectors";
17
17
  import { severityForBuiltin } from "./features";
18
- import { readCachedTranscriptResult, writeCachedTranscriptResult } from "./cache";
18
+ import { readCachedTranscript, writeCachedTranscriptResult } from "./cache";
19
19
  import { initReplay, replayEvent, restoreReplay } from "./replay";
20
20
  import {
21
21
  AUDIT_EXAMPLE_MAX_CHARS,
@@ -87,7 +87,24 @@ function parseSinceOpt(since: string | undefined): number | undefined {
87
87
  throw new Error(`Invalid --since value: "${since}" (expected e.g. "7d", "30d", or "2026-04-01")`);
88
88
  }
89
89
 
90
- async function scanOneTranscript(meta: TranscriptMetadata): Promise<TranscriptAuditResult> {
90
+ /**
91
+ * What a scan covered, alongside the result — so the caller can record a resume
92
+ * point without re-deriving it.
93
+ *
94
+ * `bytesScanned` is 0 when the source cannot be resumed at all (a database, or
95
+ * an adapter with no `streamEventsFrom`), which is what stops a resume point
96
+ * being written for something that has no bytes to resume from.
97
+ */
98
+ interface ScanOutcome {
99
+ result: TranscriptAuditResult;
100
+ bytesScanned: number;
101
+ detectorState: DetectorSessionState;
102
+ }
103
+
104
+ async function scanOneTranscript(
105
+ meta: TranscriptMetadata,
106
+ resume?: { fromByte: number; detectorState: DetectorSessionState },
107
+ ): Promise<ScanOutcome> {
91
108
  const empty: TranscriptAuditResult = {
92
109
  transcriptPath: meta.transcriptPath,
93
110
  cli: meta.cli,
@@ -102,17 +119,54 @@ async function scanOneTranscript(meta: TranscriptMetadata): Promise<TranscriptAu
102
119
  rangeByName: {},
103
120
  };
104
121
 
122
+ const adapter = ADAPTERS[meta.cli];
105
123
  // Stream failures must propagate so the orchestrator counts them in
106
124
  // `errors` rather than silently returning an empty hits map.
107
- const events = await ADAPTERS[meta.cli].streamEvents(meta);
108
- if (events.length === 0) return empty;
125
+ let events: NormalizedToolEvent[];
126
+ let bytesScanned = 0;
127
+ // A resumed scan carries the stateful detectors' state forward across the
128
+ // boundary. Starting them empty would silently change what they find — the
129
+ // re-read detector's countdown spans tool calls, so an edit before the offset
130
+ // and its wasteful re-read after it would stop being a pair.
131
+ const sessionState: DetectorSessionState = resume?.detectorState ?? {};
132
+
133
+ if (adapter.streamEventsFrom) {
134
+ // A full scan is a resume from zero, deliberately — not a separate branch.
135
+ //
136
+ // The separate branch is what got this wrong: it recorded `meta.sizeBytes`
137
+ // as scanned, which is the whole FILE, including a trailing partial line
138
+ // that was never parsed. Transcripts are appended to while the audit reads
139
+ // them, so that tail is routinely half a line — and the next run then
140
+ // resumed past an event nobody had read. Only the reader knows where the
141
+ // last complete line ended, so only the reader gets to say.
142
+ const scan = await adapter.streamEventsFrom(meta, resume?.fromByte ?? 0);
143
+ // Null means it could not be read from there after all — truncated,
144
+ // unreadable, unparseable. A thrown error would count as a scan failure,
145
+ // so fall back to a full read instead and keep the transcript scannable.
146
+ if (!scan) {
147
+ events = await adapter.streamEvents(meta);
148
+ bytesScanned = 0;
149
+ } else {
150
+ events = scan.events;
151
+ bytesScanned = scan.bytesConsumed;
152
+ empty.cwd = scan.cwd ?? "";
153
+ }
154
+ } else {
155
+ // A source with no byte offsets to speak of — a database. Scanned whole,
156
+ // every time, and given no resume point to mislead the next run with.
157
+ events = await adapter.streamEvents(meta);
158
+ bytesScanned = 0;
159
+ }
160
+
161
+ if (events.length === 0) {
162
+ return { result: empty, bytesScanned, detectorState: sessionState };
163
+ }
109
164
 
110
165
  const result = empty;
111
166
  result.eventsScanned = events.length;
112
167
  // Capture the session's cwd from the first event that carried one — every
113
168
  // event in a single transcript shares the same cwd by construction.
114
- result.cwd = events[0].cwd || "";
115
- const sessionState: DetectorSessionState = {};
169
+ result.cwd = result.cwd || events[0].cwd || "";
116
170
 
117
171
  for (const event of events) {
118
172
  // Run audit detectors first (stateful, must see every event).
@@ -146,7 +200,7 @@ async function scanOneTranscript(meta: TranscriptMetadata): Promise<TranscriptAu
146
200
  }
147
201
  }
148
202
 
149
- return result;
203
+ return { result, bytesScanned, detectorState: sessionState };
150
204
  }
151
205
 
152
206
  function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): string {
@@ -159,6 +213,55 @@ function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): s
159
213
  return `${event.toolName}`;
160
214
  }
161
215
 
216
+ /**
217
+ * Fold a tail-scan into the result the cache already held for the same
218
+ * transcript.
219
+ *
220
+ * Mirrors `recordHit` exactly, and has to: counts add, examples stay capped at
221
+ * the same limit, ranges widen. The one asymmetry is `cwd` — the cached value
222
+ * came from the FIRST event in the file and the tail's came from the first
223
+ * event after the offset, so the older one wins.
224
+ */
225
+ function mergeIncremental(
226
+ cached: TranscriptAuditResult,
227
+ tail: TranscriptAuditResult,
228
+ ): TranscriptAuditResult {
229
+ const out: TranscriptAuditResult = {
230
+ ...cached,
231
+ mtimeMs: tail.mtimeMs,
232
+ sizeBytes: tail.sizeBytes,
233
+ cwd: cached.cwd || tail.cwd || "",
234
+ eventsScanned: (cached.eventsScanned ?? 0) + (tail.eventsScanned ?? 0),
235
+ hitsByName: { ...cached.hitsByName },
236
+ examplesByName: {},
237
+ rangeByName: { ...cached.rangeByName },
238
+ };
239
+ for (const [name, list] of Object.entries(cached.examplesByName)) {
240
+ out.examplesByName[name] = [...list];
241
+ }
242
+ for (const [name, count] of Object.entries(tail.hitsByName)) {
243
+ out.hitsByName[name] = (out.hitsByName[name] ?? 0) + count;
244
+ }
245
+ for (const [name, list] of Object.entries(tail.examplesByName)) {
246
+ const exs = out.examplesByName[name] ?? [];
247
+ for (const ex of list) {
248
+ if (exs.length >= AUDIT_MAX_EXAMPLES_PER_NAME) break;
249
+ exs.push(ex);
250
+ }
251
+ out.examplesByName[name] = exs;
252
+ }
253
+ for (const [name, range] of Object.entries(tail.rangeByName)) {
254
+ const existing = out.rangeByName[name];
255
+ if (!existing) {
256
+ out.rangeByName[name] = { ...range };
257
+ } else {
258
+ if (range.first < existing.first) existing.first = range.first;
259
+ if (range.last > existing.last) existing.last = range.last;
260
+ }
261
+ }
262
+ return out;
263
+ }
264
+
162
265
  function recordHit(
163
266
  result: TranscriptAuditResult,
164
267
  name: string,
@@ -300,14 +403,31 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
300
403
  let skipped = 0;
301
404
  let errors = 0;
302
405
  const tasks = allTranscripts.map((meta) => async (): Promise<TranscriptAuditResult> => {
406
+ let resume: { fromByte: number; detectorState: DetectorSessionState } | undefined;
407
+ let cachedPrefix: TranscriptAuditResult | null = null;
303
408
  if (!opts.noCache) {
304
- const cached = readCachedTranscriptResult(meta.transcriptPath, meta.mtimeMs, meta.sizeBytes);
305
- if (cached) return cached;
409
+ const found = readCachedTranscript(meta.transcriptPath, meta.mtimeMs, meta.sizeBytes);
410
+ if (found?.kind === "hit") return found.result;
411
+ resume = found?.kind === "resume"
412
+ ? { fromByte: found.fromByte, detectorState: found.detectorState }
413
+ : undefined;
414
+ cachedPrefix = found?.kind === "resume" ? found.result : null;
306
415
  }
307
416
  try {
308
- const fresh = await scanOneTranscript(meta);
417
+ const scan = await scanOneTranscript(meta, resume);
418
+ // A resumed scan produced hits for the TAIL only; the cached result holds
419
+ // everything before it. Merging is what makes the two halves one answer.
420
+ const fresh = cachedPrefix ? mergeIncremental(cachedPrefix, scan.result) : scan.result;
309
421
  if (!opts.noCache) {
310
- writeCachedTranscriptResult(meta.transcriptPath, meta.mtimeMs, meta.sizeBytes, fresh);
422
+ writeCachedTranscriptResult(
423
+ meta.transcriptPath,
424
+ meta.mtimeMs,
425
+ meta.sizeBytes,
426
+ fresh,
427
+ scan.bytesScanned > 0
428
+ ? { bytesScanned: scan.bytesScanned, detectorState: scan.detectorState }
429
+ : undefined,
430
+ );
311
431
  }
312
432
  return fresh;
313
433
  } catch {
@@ -13,19 +13,13 @@
13
13
  * execSync against live git, so they never fire on PreToolUse/PostToolUse
14
14
  * replay — no explicit skip needed.
15
15
  */
16
- import { resolve } from "node:path";
17
- import { existsSync } from "node:fs";
18
16
  import type { EvaluationResult } from "../hooks/policy-evaluator";
19
17
  import { evaluatePolicies } from "../hooks/policy-evaluator";
20
18
  import { BUILTIN_POLICIES, registerBuiltinPolicies } from "../hooks/builtin-policies";
21
- import { PACK_ENTRY_ASSET, bundledPackDir } from "../hooks/pack-store";
22
- import { loadCustomHooks } from "../hooks/custom-hooks-loader";
23
- import { clearCustomHooks } from "../hooks/custom-hooks-registry";
24
19
  import {
25
20
  clearPolicies,
26
21
  getAllPolicies,
27
22
  normalizePolicyName,
28
- registerPolicy,
29
23
  setAllPolicies,
30
24
  } from "../hooks/policy-registry";
31
25
  import type { RegisteredPolicy } from "../hooks/policy-types";
@@ -46,10 +40,20 @@ let initialized = false;
46
40
  * (e.g. the Next.js dashboard) doesn't wipe any prior policy registrations. */
47
41
  let savedSnapshot: RegisteredPolicy[] | null = null;
48
42
 
49
- /** Register every builtin policy (regardless of user config) so the replay
50
- * shows what *could* be caught, not just what's currently enabled. Called
51
- * once per `runAudit` invocation. Snapshots the existing registry so it can
52
- * be restored by `restoreReplay()` once the audit is done. */
43
+ /**
44
+ * Register every builtin policy (regardless of user config) so the replay shows
45
+ * what *could* be caught, not just what is currently enabled. Called once per
46
+ * `runAudit` invocation. Snapshots the existing registry so `restoreReplay()`
47
+ * can put it back.
48
+ *
49
+ * The implementations compiled into this build, and ONLY those. The audit used
50
+ * to prefer a vendored `policy-pack/` copy where one existed, which was
51
+ * meaningful while the package shipped that directory; it no longer ships it, so
52
+ * that branch could not fire in any published build and existed only to be
53
+ * misread as "the audit scores against installed packs". It does not, and must
54
+ * not: an audit is a fixed yardstick, and one that changed shape with whatever
55
+ * pack a machine happened to have could not be compared against its own history.
56
+ */
53
57
  export async function initReplay(): Promise<void> {
54
58
  if (initialized) return;
55
59
  savedSnapshot = getAllPolicies();
@@ -57,90 +61,10 @@ export async function initReplay(): Promise<void> {
57
61
  const enabled = BUILTIN_POLICIES
58
62
  .map((p) => p.name)
59
63
  .filter((n) => !SKIP_POLICIES.has(normalizePolicyName(n)));
60
- if (!(await registerFromVendoredPack(enabled))) registerBuiltinPolicies(enabled);
64
+ registerBuiltinPolicies(enabled);
61
65
  initialized = true;
62
66
  }
63
67
 
64
- /**
65
- * Register the audit's policies from the pack that ships inside the package,
66
- * rather than from implementations compiled into this build.
67
- *
68
- * The audit scores by RUNNING the policies — three of its four penalty buckets
69
- * are replay hits — so the day the builtins stop being compiled in, this is
70
- * what keeps `failproofai audit` reporting the same findings and the same score
71
- * for the same transcripts. The pack carries the identical functions:
72
- * `builtin-pack-conformance.test.ts` replays a corpus through both and asserts
73
- * the verdicts do not diverge.
74
- *
75
- * Two things it must get right, or the audit quietly changes:
76
- *
77
- * - The pack cannot carry `block-failproofai-commands`, because `alwaysOn` is
78
- * refused by the pack loader by design — a downloaded file that no local
79
- * command can switch off. It is registered from the compiled side, so the
80
- * replayed set is the same 39 either way.
81
- * - The skip list still applies. `warn-repeated-tool-calls` writes a per-session
82
- * sidecar into the user's real transcript directory on every evaluation.
83
- *
84
- * Returns false when there is no vendored pack to read — a source checkout that
85
- * has not run `build:pack`, or a tarball packed without it — and the caller
86
- * falls back to the compiled implementations. An audit that silently scored on
87
- * fewer policies would be worse than either.
88
- */
89
- async function registerFromVendoredPack(enabled: string[]): Promise<boolean> {
90
- const dir = bundledPackDir();
91
- if (!dir) return false;
92
- const entry = resolve(dir, PACK_ENTRY_ASSET);
93
- if (!existsSync(entry)) return false;
94
-
95
- const wanted = new Set(enabled);
96
- let hooks;
97
- try {
98
- clearCustomHooks();
99
- hooks = await loadCustomHooks(entry, { strict: true });
100
- } catch {
101
- return false;
102
- } finally {
103
- clearCustomHooks();
104
- }
105
- if (hooks.length === 0) return false;
106
- const fromPack = new Map(hooks.map((hook) => [hook.name, hook]));
107
-
108
- // Registered in CATALOG ORDER, taking the pack's function where it has one and
109
- // the compiled one for the guard the pack may not carry.
110
- //
111
- // Order is not cosmetic here. `getPoliciesForEvent` sorts by priority only and
112
- // V8's sort is stable, so registration order decides EVALUATION order, and
113
- // `evaluatePolicies` stops at the first deny — which means the order decides
114
- // which policy is CREDITED for a hit. Appending the always-on guard after the
115
- // pack instead of at its catalog position moved it from index 11 to last, and
116
- // re-attributed 3 events in a 23,477-event corpus: 456 -> 453 for the guard,
117
- // with the denies it used to shadow surfacing as block-force-push and
118
- // block-gh-pipeline. The audit's per-policy counts are what a user reads, so
119
- // that is a changed audit, not a changed internal.
120
- let registered = 0;
121
- for (const policy of BUILTIN_POLICIES) {
122
- if (!wanted.has(policy.name)) continue;
123
- const packHook = fromPack.get(policy.name);
124
- if (policy.alwaysOn || !packHook) {
125
- registerPolicy(policy.name, policy.description, policy.fn, policy.match, 0, policy.params);
126
- continue;
127
- }
128
- registerPolicy(
129
- packHook.name,
130
- packHook.description ?? policy.description,
131
- packHook.fn,
132
- packHook.match ?? policy.match,
133
- 0,
134
- policy.params,
135
- );
136
- registered += 1;
137
- }
138
- // Nothing came from the pack: it loaded but carried none of the names the
139
- // audit replays. Falling back is more honest than scoring on the compiled set
140
- // while claiming the pack lane ran.
141
- return registered > 0;
142
- }
143
-
144
68
  /** Restore the registry to whatever was there before `initReplay()`. Safe to
145
69
  * call when not initialized (no-op). Always paired with `initReplay()` in a
146
70
  * try/finally inside `runAudit()`. */
@@ -32,7 +32,6 @@ import { dirname, resolve, sep } from "node:path";
32
32
 
33
33
  import {
34
34
  selectOne,
35
- multiSelect,
36
35
  promptText,
37
36
  intro,
38
37
  outro,
@@ -223,7 +222,6 @@ export function buildAgentChoices(scope: HookScope, cwd: string): MultiChoice<In
223
222
  });
224
223
  }
225
224
 
226
- const ALL_CLIS = "__all_clis__";
227
225
 
228
226
  const DIM_NOTE = "(auto-loaded)";
229
227
 
@@ -938,54 +936,24 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
938
936
  // An "Everything available" row protects every supported CLI (detected +
939
937
  // set-up-ahead); when ticked it wins over the individual boxes.
940
938
  //
941
- // Always asked, never inferred. This is the one decision setup genuinely
942
- // cannot make for you: which agents on this machine should be guarded. The
943
- // detected ones are pre-ticked, so the common answer is a single keypress.
939
+ // Every supported agent, detected or not — setup asks nothing about this.
944
940
  //
945
- // No `allowBack`: with the policy step and the scope fork both gone, there is
946
- // nothing inside setup to return to.
947
- let clisSel: string[];
948
- {
949
- const picked = await multiSelect<string>({
950
- message: "Which harnesses should it protect?",
951
- choices: [
952
- {
953
- label: "Everything available",
954
- value: ALL_CLIS,
955
- // Counts only what this scope can actually take — expanding to all 12
956
- // under project scope is what crashed the apply on Hermes.
957
- hint: `protect all ${clisSupportingScope(primaryScope).length} CLIs configurable here`,
958
- // A selector, not a harness. Counting it gave "13 harnesses" for
959
- // the 12 supported CLIs, and listed "Everything available" among them.
960
- summaryExclude: true,
961
- },
962
- ...buildAgentChoices(primaryScope, cwd),
963
- ],
964
- minSelected: 1,
965
- summaryNoun: "harnesses",
966
- hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ↵ confirm",
967
- stdin,
968
- stdout,
969
- });
970
- if (picked === null) return cancel();
971
- clisSel = picked;
972
- }
973
- // Filter to what the chosen scopes support in BOTH branches: "Everything
974
- // available" must not expand to CLIs that cannot take any selected scope,
975
- // and a locked row can't be ticked but belt-and-braces keeps the invariant
976
- // local to the one place `clis` is built.
941
+ // It used to be the one question left, on the reasoning that which agents to
942
+ // guard is a real choice. It stopped being one when this package stopped
943
+ // shipping policies: hooks alone enforce nothing, so wiring them everywhere
944
+ // costs a config entry and changes no behaviour until a pack arrives. What it
945
+ // buys is that an agent installed NEXT WEEK is guarded from its first tool
946
+ // call, instead of running unguarded until somebody remembers to re-run setup
947
+ // — and nobody remembers, because nothing tells them to.
948
+ //
949
+ // ALL twelve, not the detected ones, for exactly that reason. The cost is
950
+ // honest and worth naming: hook config appears under `~/.cursor/`,
951
+ // `~/.factory/` and the rest for agents that may never be installed.
977
952
  //
978
- // The union across scopes, not the intersection: under "Both", a user-scope-
979
- // only gateway like Hermes is still installable via the user half, and
980
- // dropping it because project scope cannot take it would silently protect
981
- // less than the user asked for. `installHooks` is called per scope below and
982
- // skips what a given scope cannot take.
983
- const supported = new Set(scopes.flatMap((s) => clisSupportingScope(s)));
984
- const clis: IntegrationType[] = (
985
- clisSel.includes(ALL_CLIS)
986
- ? [...INTEGRATION_TYPES]
987
- : (clisSel.filter((v) => v !== ALL_CLIS) as IntegrationType[])
988
- ).filter((id) => supported.has(id));
953
+ // Which policies run, and on which of these agents, is chosen at
954
+ // `failproofai policies add` — where the user is looking at a real list
955
+ // instead of answering in the abstract.
956
+ const clis: IntegrationType[] = [...clisSupportingScope(primaryScope)];
989
957
 
990
958
  // 4 — Connect this machine? Last, because by this point the user has decided
991
959
  // what to protect, so "would you like to see it in a dashboard?" follows
@@ -425,9 +425,16 @@ export async function evaluateHookEvent(
425
425
  if (!cloudManaged && policyId && disabledCustomPolicies.has(policyId)) continue;
426
426
  // Same rule for a session pause — it suspends local policy, never cloud.
427
427
  if (!cloudManaged && activePause) continue;
428
+ // A pack narrowed to particular agents does not fire on the others.
429
+ // Setup wires hooks into every supported CLI — hooks alone enforce
430
+ // nothing — so the choice of which agents a pack guards is made when the
431
+ // pack is installed, and this is where it takes effect. `clis: null`
432
+ // means every agent, which is what an install with no narrowing writes
433
+ // and what every pack installed before this field existed reads as.
434
+ if (pack?.clis && !pack.clis.includes(cli)) continue;
428
435
  // A pack's artifact registers everything it contains; the user may have
429
436
  // taken only some of it. `enabled: null` means the whole pack, which is
430
- // what `pack add` writes when no selection was made.
437
+ // what `policies add` writes when no selection was made.
431
438
  if (pack?.enabled && !pack.enabled.includes(hook.name)) continue;
432
439
  // A pack policy whose name is an ENABLED BUILTIN is the same guard
433
440
  // twice. The two register under different keys — `failproofai/block-sudo`
@@ -9,12 +9,12 @@ import { execSync } from "node:child_process";
9
9
  import { existsSync } from "node:fs";
10
10
  import { resolve, basename } from "node:path";
11
11
  import { platform, arch, release, hostname } from "node:os";
12
- import {
12
+ import { INTEGRATION_TYPES,
13
13
  HOOK_SCOPES,
14
14
  type HookScope,
15
15
  type IntegrationType,
16
16
  } from "./types";
17
- import { claudeCode, getIntegration, settingsPathsFor } from "./integrations";
17
+ import { claudeCode, getIntegration, settingsPathsFor, type Integration } from "./integrations";
18
18
  import { promptPolicySelection } from "./install-prompt";
19
19
  import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
20
20
  import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
@@ -256,8 +256,77 @@ function deduplicateScopes(scopes: readonly HookScope[], cwd?: string): HookScop
256
256
  });
257
257
  }
258
258
 
259
+ /**
260
+ * Is ANY agent CLI wired to call failproofai at this scope?
261
+ *
262
+ * Asked Claude Code and only Claude Code, which made it wrong for every machine
263
+ * guarded through one of the other eleven. A user with hooks in `~/.codex/` was
264
+ * told nothing was installed — quietly, while this only tinted a subtitle, and
265
+ * loudly once the listing started warning that every policy shown was inert.
266
+ * Reported from a real machine set up for codex.
267
+ *
268
+ * Any one integration answering yes is enough: the question is whether
269
+ * enforcement can reach this machine at all, not whether a particular agent is
270
+ * covered. Which agents specifically is a different question, and the per-CLI
271
+ * rows of the listing already answer it.
272
+ */
259
273
  export function hooksInstalledInSettings(scope: HookScope, cwd?: string): boolean {
260
- return claudeCode.hooksInstalledInSettings(scope, cwd);
274
+ return integrationsInstalledAt(scope, cwd).length > 0;
275
+ }
276
+
277
+ /**
278
+ * Which integrations are wired at EXACTLY this scope — the honest form of the
279
+ * question above, and the one the multi-scope warning needs.
280
+ *
281
+ * Two ways an integration answers yes for a scope it is not actually installed
282
+ * at, both of which made `failproofai policies` warn about "hooks in multiple
283
+ * scopes" on a machine whose hooks are in exactly one file:
284
+ *
285
+ * - It does not SUPPORT the scope. Hermes and OpenClaw are user-scope only
286
+ * (`HERMES_HOOK_SCOPES = ["user"]`) and their `getSettingsPath` ignores the
287
+ * scope argument entirely, so they hand back the user file — and report it
288
+ * installed — for `project` and `local` alike. `scopes` already declares
289
+ * this; nothing was reading it.
290
+ * - Its project path RESOLVES to its user path. Run this from `$HOME` and
291
+ * `<cwd>/.claude/settings.json` is `~/.claude/settings.json`: one file,
292
+ * counted as two scopes. Cheap to detect and impossible to get right by
293
+ * asking each integration separately.
294
+ */
295
+ export function integrationsInstalledAt(scope: HookScope, cwd?: string): IntegrationType[] {
296
+ return INTEGRATION_TYPES.filter((id) => {
297
+ try {
298
+ const integration = getIntegration(id);
299
+ if (!integration.scopes.includes(scope)) return false;
300
+ if (!integration.hooksInstalledInSettings(scope, cwd)) return false;
301
+ // A narrower scope that lands on the same file as a wider one is that
302
+ // wider one, seen twice. Attribute it to the widest scope that resolves
303
+ // there so exactly one of them counts.
304
+ const here = integration.getSettingsPath(scope, cwd);
305
+ return !HOOK_SCOPES.some(
306
+ (other) =>
307
+ other !== scope &&
308
+ HOOK_SCOPES.indexOf(other) < HOOK_SCOPES.indexOf(scope) &&
309
+ integration.scopes.includes(other) &&
310
+ safeSettingsPath(integration, other, cwd) === here,
311
+ );
312
+ } catch {
313
+ // An integration whose settings file is unreadable is not evidence that
314
+ // nothing is installed — keep asking the rest.
315
+ return false;
316
+ }
317
+ });
318
+ }
319
+
320
+ function safeSettingsPath(
321
+ integration: Integration,
322
+ scope: HookScope,
323
+ cwd?: string,
324
+ ): string | null {
325
+ try {
326
+ return integration.getSettingsPath(scope, cwd);
327
+ } catch {
328
+ return null;
329
+ }
261
330
  }
262
331
 
263
332
  export interface InstallHooksOptions {