failproofai 1.0.10 → 1.0.11-beta.0

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 (296) 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 +4 -4
  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 +4 -4
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.meta +8 -0
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  19. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.meta +8 -0
  23. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  26. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/invite/route.js +4 -4
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  34. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -4
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -3
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -3
  40. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  44. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/audit/page.js +4 -4
  46. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/index.html +1 -1
  49. package/.next/standalone/.next/server/app/index.meta +8 -0
  50. package/.next/standalone/.next/server/app/index.rsc +14 -14
  51. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  52. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  53. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  55. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  56. package/.next/standalone/.next/server/app/page.js +4 -4
  57. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  60. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  61. package/.next/standalone/.next/server/app/policies/page.js +8 -8
  62. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  65. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/page.js +5 -5
  67. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  70. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  71. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
  73. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  76. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  77. package/.next/standalone/.next/server/app/projects/page.js +5 -5
  78. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +3 -3
  81. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  82. package/.next/standalone/.next/server/app/settings/page.js +9 -9
  83. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/chunks/{[externals]__0qxhecv._.js → [externals]__0d7i9ea._.js} +2 -2
  86. package/.next/standalone/.next/server/chunks/{[externals]__0sg9-3p._.js → [externals]__0hjaw70._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/[externals]__1_9ze9b._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/[externals]__1oenfmn._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ga9hs7._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0moxohc._.js +22 -0
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z2ll-a._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +11 -11
  96. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1autwno._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1v25n-6._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/_1fz8m31._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/_1jrv3hg._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/_1nx8fim._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/_1ogwigf._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1q27azx._.js} +2 -2
  107. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0el6rg8._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0ol5pa3._.js} +2 -2
  109. 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_1rqmk27._.js} +2 -2
  110. 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_1w7tiq4._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-bm5zf._.js → [root-of-the-server]__0502lat._.js} +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_gn5iz._.js +4 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f765xr._.js +4 -0
  116. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01bmjsj._.js → [root-of-the-server]__0h38gzz._.js} +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hoaz1g._.js +4 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ouobn4._.js → [root-of-the-server]__0mxmwtx._.js} +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__19evfi8._.js → [root-of-the-server]__0uescqv._.js} +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1484zv2._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__19dutr7._.js} +4 -4
  122. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0oqrl71._.js → [root-of-the-server]__1_qv8oh._.js} +2 -2
  123. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u6sn69._.js → [root-of-the-server]__1ab4ifb._.js} +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fbrk56._.js +4 -0
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  126. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1um4yxn._.js +4 -0
  127. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wb4yt2._.js +4 -0
  128. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1y3n3cn._.js +4 -0
  129. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/{_16pa9ht._.js → _0lnpwk8._.js} +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/{_0yq5_3a._.js → _15y8myg._.js} +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
  134. package/.next/standalone/.next/server/chunks/ssr/{_042cgl1._.js → _1o95rf0._.js} +2 -2
  135. package/.next/standalone/.next/server/chunks/ssr/{_1zopuov._.js → _1thr5rf._.js} +2 -2
  136. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -14
  137. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0nkkbfv._.js +6 -0
  144. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  146. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  147. package/.next/standalone/.next/server/middleware.js +1 -1
  148. package/.next/standalone/.next/server/pages/404.html +1 -1
  149. package/.next/standalone/.next/server/pages/500.html +1 -1
  150. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  151. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  152. package/.next/standalone/.next/static/chunks/{03h3b-uomi7_d.js → 0pra9i676pym1.js} +1 -1
  153. package/.next/standalone/.next/static/chunks/{2kj826h1w9r47.js → 1-lpi0m-hc1kb.js} +1 -1
  154. package/.next/standalone/.next/static/chunks/{3ob2161ap37ul.js → 17yvyyblr_eb5.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{3nkc2jlgyygda.js → 1mylcr99gccyz.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{2j92xul6y24_v.js → 1wozchtn2etvh.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{3mjfvng11xnoo.js → 23thtelz1a-ur.js} +2 -2
  158. package/.next/standalone/.next/static/chunks/{1fqukzzowjflt.js → 2bc17tohgs4bs.js} +1 -1
  159. package/.next/standalone/.next/static/chunks/{3u1s8dsqiean5.js → 2xq-ejo8kti18.js} +1 -1
  160. package/.next/standalone/.next/static/chunks/3-_pth_c9n938.js +1 -0
  161. package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 3n46e45atic-m.js} +1 -1
  162. package/.next/standalone/.next/static/chunks/{078dofaxvjgpd.js → 3q7sndqftqhw9.js} +1 -1
  163. package/.next/standalone/.next/static/chunks/{0542cuk_va8g1.js → 3sg07wnn969xq.js} +1 -1
  164. package/.next/standalone/.next/static/chunks/{turbopack-11dn2pegmg9_9.js → turbopack-1opxka4dtjbc5.js} +1 -1
  165. package/.next/standalone/app/actions/install-hooks-web.ts +10 -2
  166. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  167. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  168. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  169. package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/256.js +1 -0
  170. package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/package.json +1 -0
  171. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +17 -17
  172. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +18 -18
  173. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +3 -3
  174. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +13 -13
  175. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  176. package/.next/standalone/node_modules/next/dist/server/base-server.js +1 -2
  177. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  178. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  179. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  180. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +265 -53
  181. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  182. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +177 -19
  183. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +36 -17
  184. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +14 -6
  185. package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +3 -1
  186. package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +14 -0
  187. package/.next/standalone/node_modules/next/dist/server/lib/route-cache-key.js +77 -0
  188. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +9 -11
  189. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  190. package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +4 -2
  191. package/.next/standalone/node_modules/next/dist/server/next-server.js +7 -3
  192. package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +32 -16
  193. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +2 -2
  194. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +6 -1
  195. package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +9 -2
  196. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +30 -29
  197. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +18 -1
  198. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  199. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  200. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  201. package/.next/standalone/node_modules/next/package.json +10 -10
  202. package/.next/standalone/package.json +14 -11
  203. package/.next/standalone/sdk/python/README.md +41 -0
  204. package/.next/standalone/sdk/python/docs/README.md +1 -0
  205. package/.next/standalone/sdk/python/docs/otel/README.md +36 -0
  206. package/.next/standalone/sdk/python/docs/otel/examples/agent_trace.py +30 -0
  207. package/.next/standalone/sdk/python/docs/otel/examples/quickstart.py +22 -0
  208. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +8 -0
  209. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +3 -0
  210. package/.next/standalone/sdk/python/failproofai_sdk/integrations/otel.py +189 -0
  211. package/.next/standalone/sdk/python/pyproject.toml +2 -1
  212. package/.next/standalone/sdk/python/tests/test_docs.py +16 -3
  213. package/.next/standalone/sdk/python/tests/test_integrations.py +1 -1
  214. package/.next/standalone/sdk/python/tests/test_otel.py +147 -0
  215. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +4 -1
  216. package/.next/standalone/sdk/python/uv.lock +258 -147
  217. package/.next/standalone/sdk/typescript/README.md +43 -0
  218. package/.next/standalone/sdk/typescript/integration/ai.test.ts +9 -2
  219. package/.next/standalone/sdk/typescript/package-lock.json +57 -5
  220. package/.next/standalone/sdk/typescript/package.json +2 -0
  221. package/.next/standalone/sdk/typescript/src/edge/index.ts +9 -0
  222. package/.next/standalone/sdk/typescript/src/index.ts +2 -0
  223. package/.next/standalone/sdk/typescript/src/integrations/index.ts +4 -1
  224. package/.next/standalone/sdk/typescript/src/integrations/otel.ts +204 -0
  225. package/.next/standalone/sdk/typescript/test/integrations.test.ts +1 -1
  226. package/.next/standalone/sdk/typescript/test/otel.test.ts +125 -0
  227. package/.next/standalone/server.js +1 -1
  228. package/bin/failproofai.mjs +653 -1077
  229. package/dist/cli.mjs +8781 -4050
  230. package/dist/worker.mjs +170 -62
  231. package/hermes-plugin/plugin.yaml +1 -1
  232. package/package.json +14 -11
  233. package/scripts/launch-facts.ts +157 -0
  234. package/scripts/launch-ready.ts +228 -0
  235. package/scripts/launch-screen.ts +202 -0
  236. package/scripts/launch.ts +106 -49
  237. package/scripts/skew-log-filter.ts +38 -0
  238. package/src/audit/cli-login.ts +39 -59
  239. package/src/audit/cli.ts +308 -228
  240. package/src/audit/index.ts +45 -2
  241. package/src/audit/schedule-cli.ts +83 -120
  242. package/src/audit/types.ts +31 -0
  243. package/src/hooks/agent-selection.ts +155 -0
  244. package/src/hooks/backfill-cli.ts +151 -137
  245. package/src/hooks/backfill-request.ts +149 -0
  246. package/src/hooks/cloud-connection.ts +1 -1
  247. package/src/hooks/cloud-enrollment-cli.ts +78 -28
  248. package/src/hooks/cloud-introspect.ts +3 -0
  249. package/src/hooks/collector-config.ts +10 -1
  250. package/src/hooks/config-status.ts +717 -0
  251. package/src/hooks/configure-wizard.ts +967 -988
  252. package/src/hooks/daemon-service.ts +16 -6
  253. package/src/hooks/did-you-mean.ts +185 -0
  254. package/src/hooks/flush-cli.ts +33 -7
  255. package/src/hooks/fp-config.ts +96 -0
  256. package/src/hooks/fp-home.ts +11 -0
  257. package/src/hooks/fp-reset.ts +102 -95
  258. package/src/hooks/handler.ts +4 -0
  259. package/src/hooks/harness-cli.ts +87 -138
  260. package/src/hooks/install-prompt.ts +38 -477
  261. package/src/hooks/integrations.ts +1 -1
  262. package/src/hooks/jev-cli.ts +183 -196
  263. package/src/hooks/manager.ts +554 -358
  264. package/src/hooks/onboarding-attempt.ts +11 -9
  265. package/src/hooks/otel-cli.ts +350 -0
  266. package/src/hooks/otel-output.ts +24 -0
  267. package/src/hooks/otel-writers.ts +215 -0
  268. package/src/hooks/pack-cli.ts +219 -204
  269. package/src/hooks/report-body.ts +50 -0
  270. package/src/hooks/safe-config-write.ts +4 -2
  271. package/src/hooks/session-pause-cli.ts +93 -73
  272. package/src/hooks/tui.ts +849 -389
  273. package/src/hooks/uninstall-cli.ts +136 -149
  274. package/src/hooks/untraced-note.ts +52 -0
  275. package/.next/standalone/.next/server/chunks/[externals]__19_pzeq._.js +0 -3
  276. package/.next/standalone/.next/server/chunks/[externals]__20kzpkf._.js +0 -3
  277. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
  278. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +0 -3
  279. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0igxt32._.js +0 -3
  280. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
  281. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +0 -22
  282. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
  283. package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
  284. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
  285. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
  286. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
  287. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
  288. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
  289. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +0 -4
  290. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +0 -4
  291. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
  292. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +0 -6
  293. package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +0 -1
  294. /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → xtcpqBNBdjQVerPMkI2HI}/_buildManifest.js +0 -0
  295. /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → xtcpqBNBdjQVerPMkI2HI}/_clientMiddlewareManifest.js +0 -0
  296. /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → xtcpqBNBdjQVerPMkI2HI}/_ssgManifest.js +0 -0
@@ -24,27 +24,15 @@ import { trackHookEvent } from "./hook-telemetry";
24
24
  import { getInstanceId, hashToId } from "../../lib/telemetry-id";
25
25
  import { CliError } from "../cli-error";
26
26
  import { hookLogWarn } from "./hook-logger";
27
- import { customPoliciesDir, globalPolicyConfigFile } from "./fp-home";
27
+ import { customPoliciesDir } from "./fp-home";
28
28
  import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
29
29
  import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
30
30
  import type { ResolvedPack } from "./pack-manifest";
31
31
  import { hasInstalledRegexPacks, readInstalledPacks } from "./pack-manifest";
32
32
  import { packPolicyParamKey } from "./policy-evaluator";
33
33
  import { probeDaemonPolicyEvaluation } from "./daemon-service";
34
- import {
35
- chip,
36
- note,
37
- nextStep,
38
- optsFor,
39
- printBlock,
40
- rule,
41
- stack,
42
- table,
43
- title,
44
- warning,
45
- type ChipState,
46
- type TableRow,
47
- } from "./tui";
34
+ import { keepAgentSelectionTrue } from "./agent-selection";
35
+ import { INDENT, colorsEnabled, optsFor, printBlock, screenKit, type ScreenKitOpts } from "./tui";
48
36
 
49
37
  const VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
50
38
 
@@ -77,7 +65,7 @@ function resolveFailproofaiBinary(): string {
77
65
  } catch {
78
66
  throw new CliError(
79
67
  "failproofai binary not found in PATH.\n" +
80
- "Install it globally first: npm install -g failproofai"
68
+ "Install it globally first: `npm install -g failproofai`"
81
69
  );
82
70
  }
83
71
  }
@@ -145,7 +133,7 @@ function resolvePolicyNames(names: string[]): { builtins: string[]; packs: PackP
145
133
  throw new CliError(
146
134
  `"${raw}" is declared by ${direct.length} installed packs.\n` +
147
135
  `Name the one you mean:\n` +
148
- direct.map((m) => ` ${m.packId}:${m.name}`).join("\n"),
136
+ direct.map((m) => ` \`${m.packId}:${m.name}\``).join("\n"),
149
137
  );
150
138
  }
151
139
  // No pack carries it. Falls back to the compiled name set, which is what a
@@ -172,7 +160,7 @@ function resolvePolicyNames(names: string[]): { builtins: string[]; packs: PackP
172
160
  throw new CliError(
173
161
  `"${raw}" is declared by ${matches.length} installed packs.\n` +
174
162
  `Name the one you mean:\n` +
175
- matches.map((m) => ` ${m.packId}:${m.name}`).join("\n"),
163
+ matches.map((m) => ` \`${m.packId}:${m.name}\``).join("\n"),
176
164
  );
177
165
  }
178
166
  unknown.push(raw);
@@ -218,10 +206,9 @@ function applyPackPolicies(
218
206
  writeScopedHooksConfig(next, scope, cwd);
219
207
  }
220
208
  }
209
+ const kit = screenKit(optsFor(process.stdout));
221
210
  for (const ref of refs) {
222
- console.log(
223
- `${on ? "Enabled" : "Disabled"} ${ref.name} from pack ${ref.packId}@${ref.packVersion}.`,
224
- );
211
+ console.log(kit.ok(`${on ? "Enabled" : "Disabled"} ${ref.name} from pack ${ref.packId}@${ref.packVersion}.`));
225
212
  }
226
213
  }
227
214
 
@@ -240,9 +227,7 @@ function rejectAlwaysOnPolicies(names: string[]): void {
240
227
  if (refused.length > 0) {
241
228
  throw new CliError(
242
229
  `Cannot disable: ${refused.join(", ")}\n` +
243
- `This policy stops an agent from switching off failproofai itself, so it ` +
244
- `is always on and ships with the package. A guard the agent can disable ` +
245
- `by the means it is meant to prevent is not a guard.`
230
+ `It stops an agent from switching failproofai off, so it is always on.`
246
231
  );
247
232
  }
248
233
  }
@@ -319,6 +304,119 @@ export function integrationsInstalledAt(scope: HookScope, cwd?: string): Integra
319
304
  });
320
305
  }
321
306
 
307
+ /**
308
+ * Why the policies on this machine are not enforcing, or `null` when they are.
309
+ *
310
+ * ONE answer for every screen that warns about it — `failproofai policies`, the
311
+ * dashboard launch screen — so two screens can never disagree about whether
312
+ * this machine is protected. The redesign shows one generic warning whatever
313
+ * the cause (decision D18); the cause is returned anyway, for tests and so a
314
+ * screen that wants the specific fix can have it.
315
+ *
316
+ * Cheap enough for the launch path on purpose: it reads files and imports no
317
+ * policy file and probes no daemon.
318
+ *
319
+ * Deliberately NOT causes, because each needs its own words:
320
+ * - A refused pack and a down daemon on a daemon-configured machine. Both
321
+ * FAIL CLOSED: they DENY tool calls rather than let them through, so "not
322
+ * enforcing" would tell the user the opposite of what is happening.
323
+ * - A session pause. It belongs to one session, not to the machine, and
324
+ * `config --status` lists it.
325
+ */
326
+ export type NotEnforcingReason = "no-hooks" | "no-policies" | "observe-only";
327
+
328
+ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
329
+ const wired = deduplicateScopes(HOOK_SCOPES, cwd).some((scope) => hooksInstalledInSettings(scope, cwd));
330
+ if (!wired) return "no-hooks";
331
+
332
+ const config = readMergedHooksConfig(cwd);
333
+ const disabled = new Set(config.disabledCustomPolicies ?? []);
334
+ let observing = false;
335
+
336
+ // Installed packs: a policy enforces when its pack is not observe-only, it
337
+ // was taken at install, and nobody switched it off afterwards.
338
+ try {
339
+ const installed = readInstalledPacks();
340
+ // A pack that failed to load FAILS CLOSED: the calls it covers are denied,
341
+ // which is the opposite of "not enforcing" (see the doc comment above). An
342
+ // observe pack that fails does not deny, so it does not count; neither does
343
+ // a Jev-checks-only pack, which has nothing in the regex tier to deny on
344
+ // (pack-failclosed.ts skips it, and so does the listing below).
345
+ if (installed.errors.some((error) => error.effect !== "observe" && !error.semanticOnly)) return null;
346
+ for (const pack of installed.packs) {
347
+ const taken = pack.enabled ?? pack.policies.map((policy) => policy.name);
348
+ const on = taken.filter((name) => !disabled.has(`pack:${pack.id}@${pack.version}:${name}`));
349
+ if (on.length === 0) continue;
350
+ if (pack.effect === "observe") observing = true;
351
+ else return null;
352
+ }
353
+ } catch {
354
+ // An unreadable manifest is reported by the listing itself.
355
+ }
356
+
357
+ // Cloud-managed policies, pushed from a deployment.
358
+ try {
359
+ for (const artifact of readActiveCloudManagedPolicies()) {
360
+ if (artifact.effect === "observe") observing = true;
361
+ else return null;
362
+ }
363
+ } catch {
364
+ // No deployment, or an unreadable one: nothing enforces from there.
365
+ }
366
+
367
+ // A configured custom policy file, or convention files on disk. Loading them
368
+ // to ask what they hold would import user code on every launch, so their
369
+ // presence counts: the listing below reports a file that fails to load.
370
+ if (configuredCustomPolicyPaths(config).length > 0) return null;
371
+ const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
372
+ if (discoverPolicyFiles(projectDir).length > 0 || discoverPolicyFiles(customPoliciesDir()).length > 0) {
373
+ return null;
374
+ }
375
+
376
+ // A machine still on the pre-pack migration shim runs its legacy builtins.
377
+ if (config.enabledPolicies.length > 0 && !hasInstalledRegexPacks()) return null;
378
+
379
+ return observing ? "observe-only" : "no-policies";
380
+ }
381
+
382
+ /**
383
+ * How many policies enforce on this machine, for the line that says so.
384
+ *
385
+ * Counted the same way `notEnforcingReason` decides, so the two never disagree:
386
+ * pack policies that are taken and not switched off, cloud-managed policies, and
387
+ * the legacy builtins of a machine still on the migration shim — observe-only
388
+ * ones excluded. Custom policy files cannot be counted without importing them,
389
+ * so their presence is reported on its own.
390
+ */
391
+ export function enforcingPolicyCount(cwd?: string): { count: number; custom: boolean } {
392
+ const config = readMergedHooksConfig(cwd);
393
+ const disabled = new Set(config.disabledCustomPolicies ?? []);
394
+ let count = 0;
395
+ try {
396
+ for (const pack of readInstalledPacks().packs) {
397
+ if (pack.effect === "observe") continue;
398
+ const taken = pack.enabled ?? pack.policies.map((policy) => policy.name);
399
+ count += taken.filter((name) => !disabled.has(`pack:${pack.id}@${pack.version}:${name}`)).length;
400
+ }
401
+ } catch {
402
+ // An unreadable manifest counts nothing, as it enforces nothing.
403
+ }
404
+ try {
405
+ for (const artifact of readActiveCloudManagedPolicies()) {
406
+ if (artifact.effect !== "observe") count++;
407
+ }
408
+ } catch {
409
+ // No deployment.
410
+ }
411
+ if (!hasInstalledRegexPacks()) count += config.enabledPolicies.length;
412
+ const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
413
+ const custom =
414
+ configuredCustomPolicyPaths(config).length > 0 ||
415
+ discoverPolicyFiles(projectDir).length > 0 ||
416
+ discoverPolicyFiles(customPoliciesDir()).length > 0;
417
+ return { count, custom };
418
+ }
419
+
322
420
  function safeSettingsPath(
323
421
  integration: Integration,
324
422
  scope: HookScope,
@@ -337,6 +435,16 @@ export interface InstallHooksOptions {
337
435
  /** Suppress this module's installation logging (for callers that render their
338
436
  * own UI, like the configure wizard). Errors still surface via console.error. */
339
437
  quiet?: boolean;
438
+ /**
439
+ * Trace every agent this call hooks, at whatever scope, and say so in one line
440
+ * after everything else it prints (decision D2, `./agent-selection`).
441
+ * Otherwise the next `failproofai config` takes these hooks out again.
442
+ *
443
+ * Asked for by the commands a person runs — `policies --install`,
444
+ * `policies add`, the dashboard — and off by default, because the one other
445
+ * caller is `failproofai config`, which writes the selection itself.
446
+ */
447
+ syncAgentSelection?: boolean;
340
448
  }
341
449
 
342
450
  /**
@@ -359,10 +467,11 @@ export async function installHooks(
359
467
  cli?: IntegrationType[],
360
468
  options: InstallHooksOptions = {},
361
469
  ): Promise<void> {
362
- const { replace = false, quiet = false } = options;
470
+ const { replace = false, quiet = false, syncAgentSelection = false } = options;
363
471
  if (!quiet) {
364
472
  return installHooksImpl(
365
473
  policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace,
474
+ syncAgentSelection,
366
475
  );
367
476
  }
368
477
  // Quiet mode: this module logs exclusively via console.log, so muting it for
@@ -373,6 +482,7 @@ export async function installHooks(
373
482
  try {
374
483
  return await installHooksImpl(
375
484
  policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace,
485
+ syncAgentSelection,
376
486
  );
377
487
  } finally {
378
488
  console.log = origLog;
@@ -389,6 +499,7 @@ async function installHooksImpl(
389
499
  removeCustomHooks = false,
390
500
  cli?: IntegrationType[],
391
501
  replace = false,
502
+ syncAgentSelection = false,
392
503
  ): Promise<void> {
393
504
  // Validate user input first before any system checks
394
505
  if (policyNames !== undefined && policyNames.length > 0) {
@@ -439,7 +550,7 @@ async function installHooksImpl(
439
550
  // Then check if "all" is mixed with valid specific names
440
551
  throw new CliError(
441
552
  `"all" cannot be combined with specific policy names.\n` +
442
- `Use either: --install all or --install block-sudo sanitize-jwt ...`
553
+ "Use either `failproofai policies --install all` or `failproofai policies --install block-sudo sanitize-jwt`"
443
554
  );
444
555
  }
445
556
  }
@@ -464,8 +575,8 @@ async function installHooksImpl(
464
575
  });
465
576
  } catch {}
466
577
  throw new CliError(
467
- `Scope "${scope}" is not supported by ${integration.displayName}. ` +
468
- `Valid scopes: ${integration.scopes.join(", ")}`
578
+ `Scope "${scope}" is not supported by ${integration.displayName}.\n` +
579
+ `Use one of: ${integration.scopes.join(", ")}`
469
580
  );
470
581
  }
471
582
  }
@@ -483,7 +594,7 @@ async function installHooksImpl(
483
594
  const verb = daemonRequiredBy.length === 1 ? "requires" : "require";
484
595
  throw new CliError(
485
596
  `${names} ${verb} a compatible failproofaid daemon with native policy evaluation before FailproofAI enforcement can be enabled.\n` +
486
- "Run `failproofai config` to install or update the daemon, then retry.",
597
+ "Install or update the daemon with `failproofai config`, then run this again.",
487
598
  );
488
599
  }
489
600
 
@@ -587,7 +698,7 @@ async function installHooksImpl(
587
698
  error_type: /not found/i.test(msg) ? "file_not_found" : "load_error",
588
699
  });
589
700
  } catch {}
590
- console.error(`Error: ${msg}`);
701
+ process.stderr.write(`${screenKit({ color: colorsEnabled(process.stderr) }).notice("fail", msg).join("\n")}\n`);
591
702
  process.exit(1);
592
703
  }
593
704
  const semanticCount = getSemanticRegistrations().length;
@@ -604,9 +715,14 @@ async function installHooksImpl(
604
715
  error_type: "no_hooks_registered",
605
716
  });
606
717
  } catch {}
607
- console.error(
608
- `Error: no hooks registered in ${path}. ` +
609
- `Make sure your file calls customPolicies.add(...) at least once.`,
718
+ process.stderr.write(
719
+ `${screenKit({ color: colorsEnabled(process.stderr) })
720
+ .notice(
721
+ "fail",
722
+ `No hooks are registered in ${path}.\n` +
723
+ "Make sure the file calls `customPolicies.add(...)` at least once.",
724
+ )
725
+ .join("\n")}\n`,
610
726
  );
611
727
  process.exit(1);
612
728
  }
@@ -665,8 +781,10 @@ async function installHooksImpl(
665
781
  await addPack(CORE_SOURCE, { only: fromPack });
666
782
  } catch (err) {
667
783
  console.log(
668
- `\nWarning: could not fetch the policy pack (${err instanceof Error ? err.message : String(err)}).\n` +
669
- `Run \`failproofai policies add FailproofAI/policies\` once you are online.`,
784
+ screenKit(optsFor(process.stdout)).caution(
785
+ `Couldn't fetch the policy pack (${err instanceof Error ? err.message : String(err)}). Once you are online:`,
786
+ "failproofai policies add FailproofAI/policies",
787
+ ),
670
788
  );
671
789
  }
672
790
  } else {
@@ -685,13 +803,14 @@ async function installHooksImpl(
685
803
  // with an empty list described work it had not done — or worse, re-stated a
686
804
  // stale key as though it were this run's decision.
687
805
  if (policyNames !== undefined && selectedPolicies.length > 0) {
688
- console.log(`\nEnabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}\n`);
806
+ const n = selectedPolicies.length;
807
+ console.log(screenKit(optsFor(process.stdout)).ok(`Enabled ${n} ${n === 1 ? "policy" : "policies"}: ${selectedPolicies.join(", ")}`));
689
808
  }
690
809
  if (removeCustomHooks) {
691
- console.log("Custom hooks path cleared.");
810
+ console.log(screenKit(optsFor(process.stdout)).ok("Custom hooks path cleared."));
692
811
  } else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {
693
812
  const paths = configToWrite.customPoliciesPaths ?? [configToWrite.customPoliciesPath!];
694
- console.log(`Custom hooks paths: ${paths.join(", ")}`);
813
+ for (const line of screenKit(optsFor(process.stdout)).kv([["custom", paths.join(", ")]])) console.log(line);
695
814
  }
696
815
 
697
816
  // Write hooks for each selected CLI
@@ -768,38 +887,37 @@ async function installHooksImpl(
768
887
  // Telemetry is best-effort — never block the operation
769
888
  }
770
889
 
890
+ // One ✓ per agent, with where it was written under it; then how the hooks
891
+ // call failproofai. Drawn with the kit, so colour follows the terminal and a
892
+ // pipe gets plain text.
893
+ const kit = screenKit(optsFor(process.stdout));
771
894
  for (const { cli: cliId, path } of writtenSettingsPaths) {
772
895
  const integration = getIntegration(cliId);
773
- if (cliId === "hermes") {
774
- console.log(
775
- `Failproof AI native plugin installed for ${integration.displayName} ` +
776
- `(8 registered hooks, scope: ${scope}).`
777
- );
778
- } else {
779
- console.log(
780
- `Failproof AI hooks installed for ${integration.displayName} ` +
781
- `(${integration.eventTypes.length} event types, scope: ${scope}).`
782
- );
783
- }
784
- console.log(`Settings: ${path}`);
896
+ console.log(
897
+ kit.ok(
898
+ cliId === "hermes"
899
+ ? `${integration.displayName} plugin installed (8 hooks, ${scope} scope).`
900
+ : `${integration.displayName} hooks installed (${integration.eventTypes.length} events, ${scope} scope).`,
901
+ ),
902
+ );
903
+ for (const line of kit.kv([["settings", path]])) console.log(line);
785
904
  }
786
- if (scope === "project") {
787
- console.log(`Command: npx -y failproofai`);
788
- console.log(`\nThis file can be committed to git — no machine-specific paths.`);
789
- } else {
790
- console.log(`Binary: ${binaryPath}`);
905
+ for (const line of kit.kv(scope === "project" ? [["command", "npx -y failproofai"]] : [["binary", binaryPath]])) {
906
+ console.log(line);
791
907
  }
908
+ if (scope === "project") console.log(`${INDENT}This file can be committed to git: it has no machine-specific paths.`);
792
909
 
793
910
  // Warn about duplicate-scope installations (Claude Code only — uses HOOK_SCOPES)
794
911
  const otherScopes = deduplicateScopes(HOOK_SCOPES, cwd).filter((s) => s !== scope);
795
912
  const duplicates = otherScopes.filter((s) => hooksInstalledInSettings(s, cwd));
796
913
  if (duplicates.length > 0) {
797
914
  const scopeList = duplicates.map((s) => `${s} (${scopeLabel(s)})`).join(", ");
798
- console.log();
799
- console.log(`\x1B[33mWarning: Failproof AI hooks are also installed at ${scopeList}.\x1B[0m`);
800
- console.log(`Having hooks in multiple scopes may cause duplicate policy evaluation.`);
801
- console.log(`Use \`failproofai policies --uninstall --scope ${duplicates[0]}\` to remove the other installation,`);
802
- console.log(`or \`failproofai policies\` to see all scopes.`);
915
+ console.log(
916
+ kit.caution(
917
+ `Hooks are also installed at ${scopeList}, so each policy may run twice.`,
918
+ `failproofai policies --uninstall --scope ${duplicates[0]}`,
919
+ ),
920
+ );
803
921
  try {
804
922
  await trackHookEvent(getInstanceId(), "multi_scope_warning_shown", {
805
923
  new_scope: scope,
@@ -808,6 +926,14 @@ async function installHooksImpl(
808
926
  });
809
927
  } catch {}
810
928
  }
929
+
930
+ // Last, after everything above: the agents whose hooks were actually written.
931
+ // A call that returned early (only a third-party pack's policies were named)
932
+ // or threw wrote none and changes nothing.
933
+ if (syncAgentSelection) {
934
+ const note = keepAgentSelectionTrue("installed", writtenSettingsPaths.map((written) => written.cli));
935
+ if (note) console.log(note);
936
+ }
811
937
  }
812
938
 
813
939
  /**
@@ -818,8 +944,18 @@ async function installHooksImpl(
818
944
  * - `["block-sudo"]` → disable specific policies in config, keep hooks installed
819
945
  * @param scope — settings scope to remove from (default: "user"), or "all" to remove from all scopes
820
946
  * @param opts.betaOnly — set to true when removing only beta policies (adds beta_only flag to telemetry)
947
+ * @param opts.syncAgentSelection — when hooks are taken out at user scope or
948
+ * every scope, stop tracing those agents and say so in one line (decision D2,
949
+ * `./agent-selection`). Asked for by `policies --uninstall`, `policies remove`
950
+ * and the dashboard; `failproofai uninstall` leaves the selection alone.
951
+ * @param opts.silent — print nothing; for a caller that reports the outcome itself
952
+ * (`failproofai uninstall` says what it removed in its own lines, under its
953
+ * question, and these old-style rows landed between the two)
821
954
  */
822
- export async function removeHooks(policyNames?: string[], scope: HookScope | "all" = "user", cwd?: string, opts?: { betaOnly?: boolean; source?: string; removeCustomHooks?: boolean; cli?: IntegrationType[] }): Promise<void> {
955
+ export async function removeHooks(policyNames?: string[], scope: HookScope | "all" = "user", cwd?: string, opts?: { betaOnly?: boolean; source?: string; removeCustomHooks?: boolean; cli?: IntegrationType[]; syncAgentSelection?: boolean; silent?: boolean }): Promise<void> {
956
+ const say = (line: string): void => {
957
+ if (!opts?.silent) console.log(line);
958
+ };
823
959
  // Resolve the effective config scope ("all" falls back to "user" for config reads/writes)
824
960
  const configScope: HookScope = scope === "all" ? "user" : scope;
825
961
  // Back-compat default: ["claude"]. The bin layer prompts for CLI selection
@@ -833,7 +969,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
833
969
  delete config.customPoliciesPath;
834
970
  delete config.customPoliciesPaths;
835
971
  writeScopedHooksConfig(config, configScope, cwd);
836
- console.log("Custom hooks path cleared.");
972
+ say("Custom hooks path cleared.");
837
973
  }
838
974
 
839
975
  // Remove specific policies from config (keep hooks installed)
@@ -855,7 +991,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
855
991
  const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
856
992
  const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
857
993
  if (notEnabled.length > 0) {
858
- console.log(`Warning: policy(ies) not currently enabled: ${notEnabled.join(", ")}`);
994
+ say(`Warning: policy(ies) not currently enabled: ${notEnabled.join(", ")}`);
859
995
  }
860
996
  const { policyParams: existingParams, ...baseConfig } = config;
861
997
  const filteredParams = existingParams
@@ -889,14 +1025,25 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
889
1025
  // Telemetry is best-effort — never block the operation
890
1026
  }
891
1027
 
892
- console.log(`Disabled ${policyNames.length - notEnabled.length} policy(ies).`);
893
- console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
1028
+ say(`Disabled ${policyNames.length - notEnabled.length} policy(ies).`);
1029
+ say(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
894
1030
  return;
895
1031
  }
896
1032
 
897
1033
  // Capture enabled policies before clearing (used for accurate telemetry below)
898
1034
  const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
899
1035
 
1036
+ // Every agent this removal acted on stops being traced, including one that
1037
+ // turned out to have nothing to remove: the command still said "not this
1038
+ // agent", and leaving it traced would let the next `failproofai config` hook
1039
+ // it. Project and local scope never count; the selection is per machine.
1040
+ const untraceRemoved = (): void => {
1041
+ if (!opts?.syncAgentSelection || (scope !== "user" && scope !== "all")) return;
1042
+ const acted = selectedClis.filter((id) => scope === "all" || getIntegration(id).scopes.includes("user"));
1043
+ const note = keepAgentSelectionTrue("removed", acted);
1044
+ if (note) say(note);
1045
+ };
1046
+
900
1047
  // Remove failproofai hooks from each selected CLI's settings file(s)
901
1048
  let totalRemoved = 0;
902
1049
  let nothingToReport = false;
@@ -923,7 +1070,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
923
1070
 
924
1071
  if (existing.length === 0) {
925
1072
  if (scope !== "all" && selectedClis.length === 1) {
926
- console.log("No settings file found. Nothing to remove.");
1073
+ say(screenKit(optsFor(process.stdout)).ok("No settings file found, so there is nothing to remove."));
927
1074
  nothingToReport = true;
928
1075
  }
929
1076
  continue;
@@ -934,13 +1081,13 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
934
1081
  const removed = integration.removeHooksFromFile(settingsPath);
935
1082
  removedHere += removed;
936
1083
  if (removed > 0 && scope !== "all") {
937
- console.log(`Removed ${removed} failproofai hook(s) from ${integration.displayName} settings.`);
938
- console.log(`Settings: ${settingsPath}`);
1084
+ say(screenKit(optsFor(process.stdout)).ok(`Removed ${removed} failproofai ${removed === 1 ? "hook" : "hooks"} from ${integration.displayName} settings.`));
1085
+ for (const line of screenKit(optsFor(process.stdout)).kv([["settings", settingsPath]])) say(line);
939
1086
  }
940
1087
  }
941
1088
 
942
1089
  if (removedHere === 0 && scope !== "all" && selectedClis.length === 1) {
943
- console.log("No hooks found in settings. Nothing to remove.");
1090
+ say(screenKit(optsFor(process.stdout)).ok("No hooks found in settings, so there is nothing to remove."));
944
1091
  nothingToReport = true;
945
1092
  continue;
946
1093
  }
@@ -948,15 +1095,18 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
948
1095
  }
949
1096
  }
950
1097
 
951
- if (nothingToReport && totalRemoved === 0) return;
1098
+ if (nothingToReport && totalRemoved === 0) {
1099
+ untraceRemoved();
1100
+ return;
1101
+ }
952
1102
 
953
1103
  if (scope === "all") {
954
- console.log(`Removed ${totalRemoved} failproofai hook(s) from all scopes.`);
1104
+ say(screenKit(optsFor(process.stdout)).ok(`Removed ${totalRemoved} failproofai ${totalRemoved === 1 ? "hook" : "hooks"} from every scope.`));
955
1105
  for (const cliId of selectedClis) {
956
1106
  const integration = getIntegration(cliId);
957
1107
  for (const s of integration.scopes) {
958
1108
  for (const p of settingsPathsFor(integration, s, cwd)) {
959
- console.log(` ${integration.displayName} / ${s}: ${p}`);
1109
+ say(`${INDENT}${integration.displayName}, ${s}: ${p}`);
960
1110
  }
961
1111
  }
962
1112
  }
@@ -996,29 +1146,215 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
996
1146
  const { customPoliciesPath: _drop, customPoliciesPaths: _dropMany, policyParams: _dropParams, ...rest } = existing;
997
1147
  writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
998
1148
  }
1149
+
1150
+ untraceRemoved();
1151
+ }
1152
+
1153
+ /** A row in one of the listing's plain sections: custom, convention, cloud. */
1154
+ export interface PoliciesScreenRow {
1155
+ state: "on" | "off" | "failed";
1156
+ name: string;
1157
+ description: string;
1158
+ /** Grey qualifier after the description — `observe` on a cloud policy. */
1159
+ tag?: string;
1160
+ }
1161
+
1162
+ /**
1163
+ * Everything `failproofai policies` draws, gathered by {@link listHooks} and
1164
+ * drawn by {@link renderPoliciesScreen}.
1165
+ *
1166
+ * Split in two so the drawing is a pure function of this value: it can be
1167
+ * asserted with colour on and off, at any width and with any version, without
1168
+ * a terminal, a home directory or a pack on disk.
1169
+ */
1170
+ export interface PoliciesScreen {
1171
+ /** Installed packs that loaded, in `installed.json` order. */
1172
+ packs: Array<{
1173
+ id: string;
1174
+ version: string;
1175
+ /** Where it was fetched from — shown only when it is not the repository the id names. */
1176
+ source: string;
1177
+ observe: boolean;
1178
+ /** Jev checks this pack brings that Jev asks. Zero for an observe pack, whose checks are never asked. */
1179
+ jevChecks: number;
1180
+ policies: Array<{ name: string; description: string; category: string; on: boolean }>;
1181
+ }>;
1182
+ /** Packs that are installed and refused to load. */
1183
+ refused: Array<{ id: string | null; reason: string }>;
1184
+ /** The one line that needs attention, already chosen by severity. */
1185
+ attention: { failed: boolean; text: string; fix?: string } | null;
1186
+ custom: PoliciesScreenRow[];
1187
+ convention: Array<{ scope: string; rows: PoliciesScreenRow[] }>;
1188
+ cloud: { deployment: number; rows: PoliciesScreenRow[] } | null;
1189
+ /**
1190
+ * Builtins this build enforces itself because no pack is installed: the
1191
+ * migration shim (`handler.ts` registers `enabledPolicies` when
1192
+ * `hasInstalledRegexPacks()` is false). Listed so the screen does not show
1193
+ * nothing for a machine enforcing all of them.
1194
+ */
1195
+ legacy?: Array<{ name: string; description: string }>;
999
1196
  }
1000
1197
 
1001
1198
  /**
1002
- * List all available policies with their per-scope enabled status.
1003
- * Layout adapts to the number of installed scopes:
1004
- * 0 scopes: compact "not installed" summary
1005
- * 1 scope: table with header + checkmarks, beta policies in a separate section
1006
- * 2+ scopes: column table with per-scope status, beta policies in a separate section
1199
+ * The policies screen (decision D14: "the policies list exactly as designed").
1007
1200
  *
1008
- * Also shows:
1009
- * - Configured policyParams values beneath each policy
1010
- * - Warnings for unknown policyParams keys
1011
- * - Custom Hooks section if customPoliciesPath is set
1201
+ * Packs say where their policies come from once, as `pack` rows at the top,
1202
+ * and every policy is ● on or ○ off. Off policies fold into one "○ n more off"
1203
+ * line per category; `all` lists every one instead.
1204
+ *
1205
+ * Where the design is silent on a real machine, a pack row carries a grey tag
1206
+ * rather than a new state: `observe` for a pack that evaluates and blocks
1207
+ * nothing, so it can never read as enforcing; its Jev checks; the source it
1208
+ * came from when that is not the repository its self-declared id names; and
1209
+ * `failed to load` with the reason for a pack that is installed and refused.
1012
1210
  */
1013
- export async function listHooks(cwd?: string): Promise<void> {
1211
+ export function renderPoliciesScreen(
1212
+ screen: PoliciesScreen,
1213
+ opts: ScreenKitOpts & { all?: boolean } = {},
1214
+ ): string[] {
1215
+ const kit = screenKit(opts);
1216
+ const count = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`;
1217
+ const glyph = (state: PoliciesScreenRow["state"]): string =>
1218
+ state === "on" ? kit.on : state === "off" ? kit.off : kit.failed;
1219
+ const out: string[] = [kit.header("Policies")];
1220
+
1221
+ const kvRows: Array<[string, string]> = [];
1222
+ for (const pack of screen.packs) {
1223
+ const tags: string[] = [];
1224
+ if (pack.observe) tags.push("observe");
1225
+ if (pack.jevChecks > 0) tags.push(count(pack.jevChecks, "Jev check", "Jev checks"));
1226
+ // The id is whatever the manifest claims; the source is the repository this
1227
+ // CLI actually fetched it from. Equal on every ordinary install, so the row
1228
+ // is exactly the design's — and when they differ, saying so is the point.
1229
+ const repo = pack.source.replace(/^github:/i, "").replace(/@[^@]*$/, "");
1230
+ if (repo.toLowerCase() !== pack.id.toLowerCase()) tags.push(`from ${pack.source}`);
1231
+ kvRows.push(["pack", `${pack.id}@${pack.version}${tags.map((t) => `${kit.sep}${kit.meta(t)}`).join("")}`]);
1232
+ }
1233
+ for (const pack of screen.refused) {
1234
+ kvRows.push(["pack", `${pack.id ?? "(unnamed)"}${kit.sep}${kit.meta(`failed to load: ${pack.reason}`)}`]);
1235
+ }
1236
+ const total = screen.packs.reduce((n, pack) => n + pack.policies.length, 0);
1237
+ const enabled = screen.packs.reduce((n, pack) => n + pack.policies.filter((p) => p.on).length, 0);
1238
+ if (total > 0) kvRows.push(["enabled", `${enabled} of ${total}`]);
1239
+ if (kvRows.length > 0) out.push("", ...kit.kv(kvRows));
1240
+
1241
+ const empty =
1242
+ screen.packs.length === 0 &&
1243
+ screen.refused.length === 0 &&
1244
+ screen.custom.length === 0 &&
1245
+ screen.convention.length === 0 &&
1246
+ !screen.cloud &&
1247
+ !screen.legacy?.length;
1248
+ if (screen.attention || empty) out.push("");
1249
+ if (screen.attention) {
1250
+ const { failed, text, fix } = screen.attention;
1251
+ out.push(failed ? kit.fail(text, fix) : kit.caution(text, fix));
1252
+ }
1253
+ if (empty) {
1254
+ // Ours first is a convenience, not a channel: a screen that named only ours
1255
+ // would read as the place policies come from.
1256
+ out.push(
1257
+ `${INDENT}Turn on ours: ${kit.cmd(`failproofai policies add ${CORE_SOURCE}`)}`,
1258
+ `${INDENT}Or anyone's: ${kit.cmd("failproofai policies add <owner>/<repo>")}`,
1259
+ );
1260
+ }
1261
+
1262
+ // Category headings name their pack only when two packs could each have a
1263
+ // category of the same name. A pack of Jev checks alone has no categories, so
1264
+ // the shipped FailproofAI/policies + FailproofAI/jev-policies pairing still
1265
+ // reads exactly as designed.
1266
+ const sectioned = screen.packs.filter((pack) => pack.policies.length > 0);
1267
+ const prefixed = sectioned.length > 1;
1268
+ let folded = 0;
1269
+ const sections: Array<{ heading: string; meta: string; items: Array<[string, string?]> }> = [];
1270
+ for (const pack of sectioned) {
1271
+ // First appearance orders the categories, and a category is gathered
1272
+ // wherever it appears: "Dangerous Commands" comes back after "Infra
1273
+ // Commands" in our own pack, and two headings for it would read as two.
1274
+ for (const category of [...new Set(pack.policies.map((p) => p.category))]) {
1275
+ const inCategory = pack.policies.filter((p) => p.category === category);
1276
+ const on = inCategory.filter((p) => p.on).length;
1277
+ const shown = opts.all ? inCategory : inCategory.filter((p) => p.on);
1278
+ const items: Array<[string, string?]> = shown.map((p) => [
1279
+ `${p.on ? kit.on : kit.off} ${p.name}`,
1280
+ p.description,
1281
+ ]);
1282
+ const hidden = inCategory.length - shown.length;
1283
+ if (hidden > 0) items.push([`${kit.off} ${hidden} more off`]);
1284
+ folded += hidden;
1285
+ sections.push({ heading: prefixed ? `${pack.id} · ${category}` : category, meta: `${on} of ${inCategory.length}`, items });
1286
+ }
1287
+ }
1288
+ // One description column for every pack section, so they line up down the
1289
+ // screen: at least the design's 32 for the name, always two spaces past the
1290
+ // longest one shown.
1291
+ const columnFor = (names: string[]): number => Math.max(34, ...names.map((name) => name.length + 4));
1292
+ const packColumn = columnFor(
1293
+ sectioned.flatMap((pack) => pack.policies.filter((p) => opts.all || p.on).map((p) => p.name)),
1294
+ );
1295
+ for (const section of sections) {
1296
+ out.push("", kit.head(section.heading, section.meta), ...kit.rows(section.items, packColumn));
1297
+ }
1298
+
1299
+ const plain = (heading: string, meta: string | undefined, rows: PoliciesScreenRow[]): void => {
1300
+ const items = rows.map((row): [string, string] => [
1301
+ `${glyph(row.state)} ${row.name}`,
1302
+ row.tag ? `${row.description}${kit.sep}${kit.meta(row.tag)}` : row.description,
1303
+ ]);
1304
+ const column = columnFor(rows.filter((row) => row.description).map((row) => row.name));
1305
+ out.push("", kit.head(heading, meta), ...kit.rows(items, column));
1306
+ };
1307
+ // A count, not the names: since packs carry the policies, this screen lists
1308
+ // no builtin by name (#738 pins that), but it must not draw a machine that is
1309
+ // enforcing thirty of them as if nothing were there.
1310
+ if (screen.legacy?.length) {
1311
+ out.push(
1312
+ "",
1313
+ kit.head("Built in", `${screen.legacy.length} on, from before packs`),
1314
+ `${INDENT}Move them into a pack: ${kit.cmd(`failproofai policies add ${CORE_SOURCE}`)}`,
1315
+ );
1316
+ }
1317
+ if (screen.custom.length > 0) plain("Custom policies", undefined, screen.custom);
1318
+ for (const { scope, rows } of screen.convention) plain("Convention policies", scope, rows);
1319
+ if (screen.cloud) plain("Cloud-managed", `deployment ${screen.cloud.deployment}`, screen.cloud.rows);
1320
+
1321
+ if (folded > 0) out.push("", `See all ${total} with ${kit.cmd("failproofai policies --all")}`);
1322
+ return out;
1323
+ }
1324
+
1325
+ /**
1326
+ * `failproofai policies`: every policy on this machine and whether it is on.
1327
+ *
1328
+ * Gathers the screen and draws it with {@link renderPoliciesScreen}. At most
1329
+ * one line asks for attention, the most severe first: a pack that will not load
1330
+ * and so DENIES what it covers, then the machine not enforcing at all
1331
+ * ({@link notEnforcingReason}, the same answer the launch screen gives), then a
1332
+ * custom policy file that is not running, hooks in more than one scope, and an
1333
+ * unknown `policyParams` key.
1334
+ *
1335
+ * `all` lists every policy instead of folding the off ones.
1336
+ */
1337
+ export async function listHooks(cwd?: string, { all = false }: { all?: boolean } = {}): Promise<void> {
1014
1338
  const config = readMergedHooksConfig(cwd);
1015
1339
  const disabledCustomSet = new Set(config.disabledCustomPolicies ?? []);
1016
- const opts = optsFor(process.stdout);
1017
1340
 
1018
1341
  // Determine which scopes have hooks installed (deduplicate when paths overlap, e.g. cwd === home)
1019
1342
  const uniqueScopes = deduplicateScopes(HOOK_SCOPES, cwd);
1020
1343
  const installedScopes = uniqueScopes.filter((s) => hooksInstalledInSettings(s, cwd));
1021
1344
 
1345
+ // One read of the pack store for the whole listing. It never throws by
1346
+ // contract; the guard is for a listing never being the thing that turns an
1347
+ // unreadable manifest into a broken command.
1348
+ const packRead: Pick<ReturnType<typeof readInstalledPacks>, "packs" | "errors"> = (() => {
1349
+ try {
1350
+ const read = readInstalledPacks();
1351
+ return { packs: read?.packs ?? [], errors: read?.errors ?? [] };
1352
+ } catch {
1353
+ return { packs: [], errors: [] };
1354
+ }
1355
+ })();
1356
+ const packErrors = packRead.errors;
1357
+
1022
1358
  // Names a `policyParams` key may legitimately use: every policy an installed
1023
1359
  // pack carries. Previously the compiled catalog, which no longer describes
1024
1360
  // what runs.
@@ -1032,7 +1368,7 @@ export async function listHooks(cwd?: string): Promise<void> {
1032
1368
  // helper rather than a third copy of the `pack/<id>/<name>` format.
1033
1369
  const knownPolicyNames = new Set<string>();
1034
1370
  try {
1035
- for (const pack of readInstalledPacks().packs) {
1371
+ for (const pack of packRead.packs) {
1036
1372
  for (const policy of pack.policies) {
1037
1373
  knownPolicyNames.add(policy.name);
1038
1374
  knownPolicyNames.add(packPolicyParamKey(pack.id, policy.name));
@@ -1042,157 +1378,14 @@ export async function listHooks(cwd?: string): Promise<void> {
1042
1378
  // Unreadable manifest: skip the typo warning rather than invent one.
1043
1379
  }
1044
1380
 
1045
- const groups: Array<string[] | null> = [];
1046
- const packCount = (() => {
1047
- try {
1048
- return readInstalledPacks().packs.reduce(
1049
- (n, pack) => n + (pack.enabled ?? pack.policies.map((p) => p.name)).length,
1050
- 0,
1051
- );
1052
- } catch {
1053
- return 0;
1054
- }
1055
- })();
1056
- // `installedScopes` is about HOOKS — whether any agent CLI is wired to call
1057
- // us — and it says nothing about whether policies exist. Reporting it as
1058
- // "not installed" above a listing of an installed pack was a flat
1059
- // contradiction, and it hid the state that actually matters: a machine can
1060
- // hold thirty-eight policies and enforce none of them, because nothing is
1061
- // calling failproofai at all. That is the worst of the three states and it
1062
- // used to read like the emptiest.
1063
- const packsPresent = (() => {
1064
- try {
1065
- return readInstalledPacks().packs.length > 0;
1066
- } catch {
1067
- return false;
1068
- }
1069
- })();
1070
- const subtitle =
1071
- installedScopes.length > 0
1072
- ? `${installedScopes.join(" + ")} · ${packCount} on`
1073
- : packsPresent
1074
- ? `${packCount} on · NOT ENFORCING`
1075
- : "nothing installed";
1076
- groups.push(title("failproofai policies", subtitle, opts));
1077
-
1078
- if (installedScopes.length === 0) {
1079
- groups.push(
1080
- packsPresent
1081
- ? warning(
1082
- [
1083
- // ONE string, not four hand-broken ones. `warning` wraps each
1084
- // element to the terminal, so pre-wrapped prose keeps its author's
1085
- // line breaks as paragraph breaks and then wraps again inside
1086
- // them — which at 60 columns left the word "them." alone on a
1087
- // line. The command stays separate because it is not prose and
1088
- // must never be split across a wrap.
1089
- "These policies are installed but NOTHING is running them. No agent " +
1090
- "CLI on this machine is wired to call failproofai, so every policy " +
1091
- "below is inert. Wire it up with:",
1092
- " failproofai config",
1093
- ],
1094
- opts,
1095
- )
1096
- : nextStep(
1097
- "failproofai config",
1098
- config.enabledPolicies.length > 0
1099
- ? "These are configured but NOT installed — no hook is running them:"
1100
- : "Nothing is set up yet. Get started with:",
1101
- opts,
1102
- ),
1103
- );
1104
- }
1105
- // Held back to the end rather than printed here. Everything below this point
1106
- // is another section of the same listing, and a footer in the middle of it
1107
- // reads as the end of the output — while a warning at the very end is the one
1108
- // a reader scrolling back from their prompt actually sees.
1109
- const footer: Array<string[] | null> = [note(`Config: ${globalPolicyConfigFile()}`, opts)];
1110
-
1111
- // A machine with hooks wired and no policies to run is the normal state right
1112
- // after setup — setup installs the hooks and deliberately chooses nothing —
1113
- // and this listing said NOTHING about how to leave it. A reader who had just
1114
- // finished `failproofai config` saw a header, a config path, and no next
1115
- // step. Ours is named in full, the same way anyone else's pack is typed,
1116
- // because it IS one.
1117
- // `packCount` counts ENABLED policies, so a pack installed with everything
1118
- // switched off reaches zero the same way an empty machine does — and the
1119
- // advice for the two could not be more different. Telling somebody who has
1120
- // just unticked all 39 to `policies add FailproofAI/policies` sends them to
1121
- // install what they already have, and doing it would change nothing: the
1122
- // selection is what is empty, not the shelf.
1123
- //
1124
- // Nor is it "nothing" while a refused pack fails closed (the warning above
1125
- // says what is denied), or while a pack's Jev checks are live with no regex
1126
- // policy on.
1127
- const { packsInstalled, jevChecks, failClosed } = (() => {
1128
- try {
1129
- const { packs, errors } = readInstalledPacks();
1130
- return {
1131
- packsInstalled: packs.length,
1132
- jevChecks: packs.reduce((n, p) => n + (p.effect === "observe" ? 0 : (p.semantic?.length ?? 0)), 0),
1133
- // The refusals `missingGuards` denies over (pack-failclosed.ts).
1134
- failClosed: errors.some((e) => e.effect !== "observe" && !e.semanticOnly),
1135
- };
1136
- } catch {
1137
- return { packsInstalled: 0, jevChecks: 0, failClosed: false };
1138
- }
1139
- })();
1140
- if (failClosed) {
1141
- // Said by the pack section's warning, where the refused pack is named.
1142
- } else if (packCount === 0 && packsInstalled === 0) {
1143
- footer.unshift(
1144
- nextStep(
1145
- `failproofai policies add ${CORE_SOURCE}`,
1146
- "Nothing is enforcing yet. Take ours, or anyone's:",
1147
- opts,
1148
- ),
1149
- note("Someone else's: failproofai policies add <owner>/<repo>", opts),
1150
- note("Look first: failproofai policies show <owner>/<repo>", opts),
1151
- );
1152
- } else if (packCount === 0 && jevChecks > 0) {
1153
- footer.unshift(
1154
- note(
1155
- `No regex policy is on; ${jevChecks} Jev check${jevChecks === 1 ? "" : "s"} from ` +
1156
- `${packsInstalled === 1 ? "the pack" : "the packs"} above apply where Jev is configured (failproofai jev status).`,
1157
- opts,
1158
- ),
1159
- );
1160
- } else if (packCount === 0) {
1161
- footer.unshift(
1162
- nextStep(
1163
- "failproofai policies add",
1164
- `Nothing is enforcing: ${packsInstalled === 1 ? "the pack above is" : "the packs above are"} installed with everything switched off. Pick what should be on:`,
1165
- opts,
1166
- ),
1167
- note("Or by name: failproofai policies add <policy>", opts),
1168
- );
1169
- }
1170
-
1171
- if (installedScopes.length > 1) {
1172
- footer.push(
1173
- warning(
1174
- [
1175
- `Hooks in multiple scopes (${installedScopes.join(", ")}).`,
1176
- "Consider keeping one. Remove with: failproofai policies --uninstall --scope <scope>",
1177
- ],
1178
- opts,
1179
- ),
1180
- );
1181
- }
1182
-
1183
- // Warn about unknown policyParams keys
1381
+ // Unknown policyParams keys. The event fires whenever there is one, whether or
1382
+ // not a more severe line takes the screen's one attention slot.
1383
+ const unknownKeys: string[] = [];
1184
1384
  if (config.policyParams) {
1185
- const unknownKeys: string[] = [];
1186
1385
  for (const key of Object.keys(config.policyParams)) {
1187
1386
  if (knownPolicyNames.size > 0 && !knownPolicyNames.has(key)) unknownKeys.push(key);
1188
1387
  }
1189
1388
  if (unknownKeys.length > 0) {
1190
- footer.push(
1191
- warning(
1192
- unknownKeys.map((key) => `unknown policyParams key "${key}" — possible typo`),
1193
- opts,
1194
- ),
1195
- );
1196
1389
  try {
1197
1390
  await trackHookEvent(getInstanceId(), "policy_params_validation_warning", {
1198
1391
  unknown_keys_count: unknownKeys.length,
@@ -1202,40 +1395,31 @@ export async function listHooks(cwd?: string): Promise<void> {
1202
1395
  }
1203
1396
  }
1204
1397
 
1205
- // Explicit Custom Policies section
1206
- const explicitPaths = configuredCustomPolicyPaths(config);
1207
- if (explicitPaths.length > 0) {
1208
- groups.push(rule("Custom Policies", opts));
1209
- for (const path of explicitPaths) {
1210
- // Enforcement resolves configured paths from the project config root.
1211
- // Use the same canonical path here so the ID checked by the CLI exactly
1212
- // matches the ID written by the dashboard.
1213
- const absPath = resolve(findProjectConfigDir(cwd ?? process.cwd()), path);
1214
- groups.push(note(absPath, opts));
1215
- if (!existsSync(absPath)) {
1216
- groups.push(warning([`file not found: ${absPath}`], opts));
1217
- continue;
1218
- }
1219
- const hooks = await loadCustomHooks(absPath);
1220
- if (hooks.length === 0) {
1221
- groups.push(
1222
- warning(["failed to load (check ~/.failproofai/logs/hooks.log)"], opts),
1223
- );
1224
- } else {
1225
- groups.push(
1226
- table(
1227
- {
1228
- head: ["", "Name", "Description"],
1229
- rows: hooks.map((hook) => [
1230
- chip(disabledCustomSet.has(`custom:${absPath}:${hook.name}`) ? "off" : "on", opts),
1231
- hook.name,
1232
- hook.description ?? "",
1233
- ]),
1234
- },
1235
- opts,
1236
- ),
1237
- );
1238
- }
1398
+ // Explicit custom policy files.
1399
+ const custom: PoliciesScreenRow[] = [];
1400
+ let customProblem: string | null = null;
1401
+ for (const path of configuredCustomPolicyPaths(config)) {
1402
+ // Enforcement resolves configured paths from the project config root.
1403
+ // Use the same canonical path here so the ID checked by the CLI exactly
1404
+ // matches the ID written by the dashboard.
1405
+ const absPath = resolve(findProjectConfigDir(cwd ?? process.cwd()), path);
1406
+ if (!existsSync(absPath)) {
1407
+ custom.push({ state: "failed", name: absPath, description: "not found" });
1408
+ customProblem ??= "A custom policy file was not found, so its policies are not running.";
1409
+ continue;
1410
+ }
1411
+ const hooks = await loadCustomHooks(absPath);
1412
+ if (hooks.length === 0) {
1413
+ custom.push({ state: "failed", name: absPath, description: "failed to load" });
1414
+ customProblem ??= "A custom policy file failed to load, so its policies are not running.";
1415
+ continue;
1416
+ }
1417
+ for (const hook of hooks) {
1418
+ custom.push({
1419
+ state: disabledCustomSet.has(`custom:${absPath}:${hook.name}`) ? "off" : "on",
1420
+ name: hook.name,
1421
+ description: hook.description ?? "",
1422
+ });
1239
1423
  }
1240
1424
  }
1241
1425
 
@@ -1248,14 +1432,14 @@ export async function listHooks(cwd?: string): Promise<void> {
1248
1432
  const projectDir = resolve(base, ".failproofai", "policies");
1249
1433
  const userDir = customPoliciesDir();
1250
1434
  const sameDir = userDir === projectDir;
1251
- const conventionDirs: { label: string; dir: string }[] = [
1252
- { label: sameDir ? "Project + User" : "Project", dir: projectDir },
1435
+ const conventionDirs: { scope: string; dir: string }[] = [
1436
+ { scope: sameDir ? "project + user" : "project", dir: projectDir },
1253
1437
  // Running from $HOME makes both paths identical. Listing the directory
1254
1438
  // twice printed every file a second time as "failed to load" — the file was
1255
1439
  // already imported by the first pass, so the module cache short-circuits
1256
1440
  // `customPolicies.add` and `loadCustomHooks` legitimately returns 0 hooks.
1257
1441
  // Nothing was wrong with the policy; the second listing was.
1258
- ...(sameDir ? [] : [{ label: "User", dir: userDir }]),
1442
+ ...(sameDir ? [] : [{ scope: "user", dir: userDir }]),
1259
1443
  ];
1260
1444
 
1261
1445
  // Record of what was found, mirrored into policies-config.json below so the
@@ -1265,7 +1449,8 @@ export async function listHooks(cwd?: string): Promise<void> {
1265
1449
  user: [],
1266
1450
  };
1267
1451
 
1268
- for (const { label, dir } of conventionDirs) {
1452
+ const convention: PoliciesScreen["convention"] = [];
1453
+ for (const { scope, dir } of conventionDirs) {
1269
1454
  const files = discoverPolicyFiles(dir);
1270
1455
  if (files.length === 0) continue;
1271
1456
 
@@ -1280,127 +1465,87 @@ export async function listHooks(cwd?: string): Promise<void> {
1280
1465
  for (const t of targets) discovered[t].push({ file, hooks });
1281
1466
  };
1282
1467
 
1283
- groups.push(rule(`Convention Policies — ${label}`, opts));
1284
- groups.push(note(dir, opts));
1285
- const rows: TableRow[] = [];
1468
+ // One row per FILE: ● while any of its hooks is on, ○ once all of them are
1469
+ // off, and a count of the ones switched off when it is some of them.
1470
+ const rows: PoliciesScreenRow[] = [];
1286
1471
  for (const file of files) {
1287
1472
  const filename = basename(file);
1288
1473
  try {
1289
1474
  const hooks = await loadCustomHooks(file);
1290
1475
  record(filename, hooks.map((h) => h.name));
1291
1476
  if (hooks.length === 0) {
1292
- rows.push({ cells: [chip("failed", opts), filename, "failed to load"] });
1477
+ rows.push({ state: "failed", name: filename, description: "failed to load" });
1293
1478
  continue;
1294
1479
  }
1295
- const hookStates = hooks.map((hook) => ({
1296
- hook,
1297
- disabled: disabledCustomSet.has(`convention:${policyScope}:${filename}:${hook.name}`),
1298
- }));
1299
- const disabledCount = hookStates.filter((entry) => entry.disabled).length;
1300
- const state: ChipState =
1301
- disabledCount === 0 ? "on" : disabledCount === hooks.length ? "off" : "mixed";
1302
- const hookSummary = hookStates
1303
- .map(({ hook, disabled }) => `${hook.name}${disabled ? " (OFF)" : ""}`)
1304
- .join(", ");
1480
+ const off = hooks.filter((hook) =>
1481
+ disabledCustomSet.has(`convention:${policyScope}:${filename}:${hook.name}`),
1482
+ ).length;
1305
1483
  rows.push({
1306
- cells: [chip(state, opts), filename, `${hooks.length} hook(s): ${hookSummary}`],
1484
+ state: off === hooks.length ? "off" : "on",
1485
+ name: filename,
1486
+ description:
1487
+ `${hooks.length} ${hooks.length === 1 ? "hook" : "hooks"}` +
1488
+ (off > 0 && off < hooks.length ? ` (${off} off)` : ""),
1307
1489
  });
1308
1490
  } catch {
1309
1491
  record(filename, []);
1310
- rows.push({ cells: [chip("failed", opts), filename, "error"] });
1492
+ rows.push({ state: "failed", name: filename, description: "failed to load" });
1311
1493
  }
1312
1494
  }
1313
- groups.push(table({ head: ["", "File", "Hooks"], rows }, opts));
1495
+ convention.push({ scope, rows });
1314
1496
  }
1315
1497
 
1316
1498
  // Installed packs. They enforce on this machine exactly like every section
1317
1499
  // above, and until now the only way to see one was `failproofai policies` —
1318
1500
  // so the command that answers "what is enforcing here?" answered it with a
1319
1501
  // subset, for the one source a person had to go out of their way to install.
1502
+ const packs: PoliciesScreen["packs"] = [];
1320
1503
  try {
1321
- const { packs, errors } = readInstalledPacks();
1322
- for (const pack of packs) {
1504
+ for (const pack of packRead.packs) {
1323
1505
  const taken = pack.enabled ?? pack.policies.map((p) => p.name);
1324
- // The source too: the id is whatever the manifest claims, the source is
1325
- // the repository this CLI actually fetched it from.
1326
- groups.push(rule(`Pack — ${pack.id}@${pack.version} · ${pack.source}`, opts));
1327
- groups.push(
1328
- table(
1329
- {
1330
- head: ["", "Name", "Description"],
1331
- rows: pack.policies.map((policy) => {
1332
- const disabled = disabledCustomSet.has(
1333
- `pack:${pack.id}@${pack.version}:${policy.name}`,
1334
- );
1335
- // `observe` evaluates and discards its verdict, so a row reading
1336
- // ON would claim enforcement the pack deliberately is not doing.
1337
- const state: ChipState =
1338
- pack.effect === "observe"
1339
- ? "observe"
1340
- : !taken.includes(policy.name) || disabled
1341
- ? "off"
1342
- : "pack";
1343
- return [chip(state, opts), policy.name, policy.description];
1344
- }),
1345
- },
1346
- opts,
1347
- ),
1348
- );
1349
- }
1350
- if (errors.length > 0) {
1351
- groups.push(
1352
- warning(
1353
- errors.flatMap((err) => [
1354
- `pack ${err.id ?? "(unnamed)"} will not load: ${err.reason}`,
1355
- ...(err.effect !== "observe" && !err.semanticOnly
1356
- ? [
1357
- "Until it loads, the tool calls its policies cover are DENIED. Fix it, or remove it with: " +
1358
- `failproofai policies remove ${err.id ?? "<id>"}`,
1359
- ]
1360
- : []),
1361
- ]),
1362
- opts,
1363
- ),
1364
- );
1506
+ packs.push({
1507
+ id: pack.id,
1508
+ version: pack.version,
1509
+ source: pack.source,
1510
+ observe: pack.effect === "observe",
1511
+ jevChecks: pack.effect === "observe" ? 0 : (pack.semantic?.length ?? 0),
1512
+ policies: pack.policies.map((policy) => ({
1513
+ name: policy.name,
1514
+ description: policy.description,
1515
+ category: policy.category,
1516
+ on:
1517
+ taken.includes(policy.name) &&
1518
+ !disabledCustomSet.has(`pack:${pack.id}@${pack.version}:${policy.name}`),
1519
+ })),
1520
+ });
1365
1521
  }
1366
1522
  } catch {
1367
1523
  // Same rule as the cloud section below: a listing must not be the thing
1368
1524
  // that turns an unreadable manifest into a broken command.
1369
1525
  }
1526
+ const refused = packErrors.map((err) => ({ id: err.id, reason: err.reason }));
1370
1527
 
1371
- // Cloud-managed policies. These enforce on this machine exactly like the two
1528
+ // Cloud-managed policies. These enforce on this machine exactly like the
1372
1529
  // sections above, but nothing here listed them — so `failproofai policies`
1373
1530
  // answered "what is enforcing?" with a subset, and the policies an operator
1374
1531
  // pushed to a fleet were the ones invisible to the person running the
1375
- // command on it.
1376
- //
1377
- // Read-only on purpose: these are owned by the deployment, not by local
1378
- // config. `--uninstall <name>` cannot switch one off, and printing them
1379
- // beside toggleable rows without saying so would imply it can.
1532
+ // command on it. Read-only: they belong to the deployment, which the heading
1533
+ // says, and `--uninstall <name>` cannot switch one off.
1534
+ let cloud: PoliciesScreen["cloud"] = null;
1380
1535
  try {
1381
- const cloud = readActiveCloudManagedPolicies();
1382
- if (cloud.length > 0) {
1383
- groups.push(rule(`Cloud-managed — deployment ${cloud[0].deployment}`, opts));
1384
- groups.push(
1385
- table(
1386
- {
1387
- head: ["", "Policy", "Version"],
1388
- rows: cloud.map((artifact) => [
1389
- // `observe` is evaluated and then has its verdict discarded, so a
1390
- // row that read "ON" would claim enforcement this policy
1391
- // deliberately is not doing.
1392
- chip(artifact.effect === "observe" ? "observe" : "cloud", opts),
1393
- artifact.id,
1394
- `v${artifact.version}`,
1395
- ]),
1396
- flex: 1,
1397
- },
1398
- opts,
1399
- ),
1400
- );
1401
- groups.push(
1402
- note("Managed from the dashboard — not switchable with `failproofai policies`.", opts),
1403
- );
1536
+ const active = readActiveCloudManagedPolicies();
1537
+ if (active.length > 0) {
1538
+ cloud = {
1539
+ deployment: active[0].deployment,
1540
+ rows: active.map((artifact) => ({
1541
+ state: "on" as const,
1542
+ name: artifact.id,
1543
+ description: `v${artifact.version}`,
1544
+ // `observe` is evaluated and then has its verdict discarded, so the
1545
+ // row says so rather than reading as plain enforcement.
1546
+ ...(artifact.effect === "observe" ? { tag: "observe" } : {}),
1547
+ })),
1548
+ };
1404
1549
  }
1405
1550
  } catch {
1406
1551
  // A machine with no deployment, or an unreadable manifest, simply has no
@@ -1408,7 +1553,58 @@ export async function listHooks(cwd?: string): Promise<void> {
1408
1553
  // the thing that turns a bad manifest into a broken command.
1409
1554
  }
1410
1555
 
1411
- printBlock(process.stdout, stack(...groups, ...footer));
1556
+ // The one attention line, most severe first. A refused pack that fails closed
1557
+ // is a FAILURE, not "not enforcing": it denies the calls its policies cover
1558
+ // (pack-failclosed.ts), so it is named before anything else.
1559
+ const failClosed = packErrors.filter((err) => err.effect !== "observe" && !err.semanticOnly);
1560
+ let attention: PoliciesScreen["attention"] = null;
1561
+ if (failClosed.length > 0) {
1562
+ const [first] = failClosed;
1563
+ const many = failClosed.length > 1;
1564
+ attention = {
1565
+ failed: true,
1566
+ text:
1567
+ `${first.id ?? "A pack"}${many ? ` and ${failClosed.length - 1} more` : ""} failed to load, ` +
1568
+ `so the calls ${many ? "they cover" : "it covers"} are denied.`,
1569
+ fix: `failproofai policies remove ${first.id ?? "<id>"}`,
1570
+ };
1571
+ } else if (notEnforcingReason(cwd) !== null) {
1572
+ // One generic line whatever the cause (decision D18).
1573
+ attention = { failed: false, text: "Policies are not enforcing yet.", fix: "failproofai config" };
1574
+ } else if (customProblem) {
1575
+ attention = { failed: false, text: customProblem };
1576
+ } else if (installedScopes.length > 1) {
1577
+ attention = {
1578
+ failed: false,
1579
+ text: `Hooks are installed in multiple scopes: ${installedScopes.join(", ")}.`,
1580
+ fix: "failproofai policies --uninstall --scope <scope>",
1581
+ };
1582
+ } else if (unknownKeys.length > 0) {
1583
+ attention = {
1584
+ failed: false,
1585
+ text:
1586
+ unknownKeys.length === 1
1587
+ ? `Unknown policyParams key "${unknownKeys[0]}", possibly a typo.`
1588
+ : `Unknown policyParams keys ${unknownKeys.map((key) => `"${key}"`).join(", ")}, possibly typos.`,
1589
+ };
1590
+ }
1591
+
1592
+ // The same test the hook path uses to decide it enforces these itself.
1593
+ const legacy = hasInstalledRegexPacks()
1594
+ ? []
1595
+ : BUILTIN_POLICIES.filter((p) => config.enabledPolicies.includes(p.name)).map((p) => ({
1596
+ name: p.name,
1597
+ description: p.description,
1598
+ }));
1599
+
1600
+ printBlock(
1601
+ process.stdout,
1602
+ renderPoliciesScreen(
1603
+ { packs, refused, attention, custom, convention, cloud, legacy },
1604
+ // Descriptions are shortened only for a terminal; a pipe keeps every word.
1605
+ { ...optsFor(process.stdout), fit: Boolean(process.stdout.isTTY), all },
1606
+ ),
1607
+ );
1412
1608
 
1413
1609
  // Mirror what was just listed into the USER config. Safe here because
1414
1610
  // `failproofai policies` is a one-shot command — never do this on the hook