failproofai 1.0.10 → 1.0.11-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) 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]__0oqrl71._.js → [root-of-the-server]__042u-jn._.js} +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  114. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-bm5zf._.js → [root-of-the-server]__0502lat._.js} +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_gn5iz._.js +4 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f765xr._.js +4 -0
  117. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01bmjsj._.js → [root-of-the-server]__0h38gzz._.js} +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hoaz1g._.js +4 -0
  119. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ouobn4._.js → [root-of-the-server]__0mxmwtx._.js} +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__19evfi8._.js → [root-of-the-server]__0uescqv._.js} +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1484zv2._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__19dutr7._.js} +4 -4
  124. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u6sn69._.js → [root-of-the-server]__1ab4ifb._.js} +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fbrk56._.js +4 -0
  126. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  127. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1um4yxn._.js +4 -0
  128. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wb4yt2._.js +4 -0
  129. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1y3n3cn._.js +4 -0
  130. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/{_0yq5_3a._.js → _1ffxwxz._.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/{_16pa9ht._.js → _1v1o2ya._.js} +2 -2
  137. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -14
  138. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0nkkbfv._.js +6 -0
  145. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  147. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  148. package/.next/standalone/.next/server/middleware.js +1 -1
  149. package/.next/standalone/.next/server/pages/404.html +1 -1
  150. package/.next/standalone/.next/server/pages/500.html +1 -1
  151. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  152. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  153. package/.next/standalone/.next/static/chunks/08tmc_7n-rba1.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{3mjfvng11xnoo.js → 09tlrmucn6oq3.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{078dofaxvjgpd.js → 0i69k917myh7b.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{03h3b-uomi7_d.js → 0u_nhsujkactp.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{2j92xul6y24_v.js → 0zp51hzx62i-u.js} +1 -1
  158. package/.next/standalone/.next/static/chunks/{3u1s8dsqiean5.js → 1fz7q0gtnmeod.js} +1 -1
  159. package/.next/standalone/.next/static/chunks/{3ob2161ap37ul.js → 1j75le8wln-26.js} +1 -1
  160. package/.next/standalone/.next/static/chunks/{2kj826h1w9r47.js → 1z4km7tw0_50h.js} +1 -1
  161. package/.next/standalone/.next/static/chunks/{1fqukzzowjflt.js → 239ay_80-xr8z.js} +1 -1
  162. package/.next/standalone/.next/static/chunks/{3nkc2jlgyygda.js → 2c9pc5xco6rnu.js} +1 -1
  163. package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 3n46e45atic-m.js} +1 -1
  164. package/.next/standalone/.next/static/chunks/{0542cuk_va8g1.js → 3sg07wnn969xq.js} +1 -1
  165. package/.next/standalone/.next/static/chunks/{turbopack-11dn2pegmg9_9.js → turbopack-1opxka4dtjbc5.js} +1 -1
  166. package/.next/standalone/app/actions/install-hooks-web.ts +10 -2
  167. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  168. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  169. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  170. package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/256.js +1 -0
  171. package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/package.json +1 -0
  172. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +17 -17
  173. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +18 -18
  174. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +3 -3
  175. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +13 -13
  176. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  177. package/.next/standalone/node_modules/next/dist/server/base-server.js +1 -2
  178. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  179. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  180. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  181. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +265 -53
  182. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  183. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +177 -19
  184. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +36 -17
  185. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +14 -6
  186. package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +3 -1
  187. package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +14 -0
  188. package/.next/standalone/node_modules/next/dist/server/lib/route-cache-key.js +77 -0
  189. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +9 -11
  190. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  191. package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +4 -2
  192. package/.next/standalone/node_modules/next/dist/server/next-server.js +7 -3
  193. package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +32 -16
  194. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +2 -2
  195. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +6 -1
  196. package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +9 -2
  197. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +30 -29
  198. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +18 -1
  199. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  200. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  201. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  202. package/.next/standalone/node_modules/next/package.json +10 -10
  203. package/.next/standalone/package.json +14 -11
  204. package/.next/standalone/sdk/python/README.md +41 -0
  205. package/.next/standalone/sdk/python/docs/README.md +1 -0
  206. package/.next/standalone/sdk/python/docs/otel/README.md +36 -0
  207. package/.next/standalone/sdk/python/docs/otel/examples/agent_trace.py +30 -0
  208. package/.next/standalone/sdk/python/docs/otel/examples/quickstart.py +22 -0
  209. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +8 -0
  210. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +3 -0
  211. package/.next/standalone/sdk/python/failproofai_sdk/integrations/otel.py +189 -0
  212. package/.next/standalone/sdk/python/pyproject.toml +2 -1
  213. package/.next/standalone/sdk/python/tests/test_docs.py +16 -3
  214. package/.next/standalone/sdk/python/tests/test_integrations.py +1 -1
  215. package/.next/standalone/sdk/python/tests/test_otel.py +147 -0
  216. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +4 -1
  217. package/.next/standalone/sdk/python/uv.lock +258 -147
  218. package/.next/standalone/sdk/typescript/README.md +43 -0
  219. package/.next/standalone/sdk/typescript/integration/ai.test.ts +9 -2
  220. package/.next/standalone/sdk/typescript/package-lock.json +57 -5
  221. package/.next/standalone/sdk/typescript/package.json +2 -0
  222. package/.next/standalone/sdk/typescript/src/edge/index.ts +9 -0
  223. package/.next/standalone/sdk/typescript/src/index.ts +2 -0
  224. package/.next/standalone/sdk/typescript/src/integrations/index.ts +4 -1
  225. package/.next/standalone/sdk/typescript/src/integrations/otel.ts +204 -0
  226. package/.next/standalone/sdk/typescript/test/integrations.test.ts +1 -1
  227. package/.next/standalone/sdk/typescript/test/otel.test.ts +125 -0
  228. package/.next/standalone/server.js +1 -1
  229. package/bin/failproofai.mjs +653 -1077
  230. package/dist/cli.mjs +8174 -3930
  231. package/dist/worker.mjs +170 -62
  232. package/hermes-plugin/plugin.yaml +1 -1
  233. package/package.json +14 -11
  234. package/scripts/launch-facts.ts +157 -0
  235. package/scripts/launch-ready.ts +228 -0
  236. package/scripts/launch-screen.ts +202 -0
  237. package/scripts/launch.ts +106 -49
  238. package/scripts/skew-log-filter.ts +42 -0
  239. package/src/audit/cli-login.ts +39 -59
  240. package/src/audit/cli.ts +308 -228
  241. package/src/audit/index.ts +45 -2
  242. package/src/audit/schedule-cli.ts +83 -120
  243. package/src/audit/types.ts +31 -0
  244. package/src/hooks/agent-selection.ts +155 -0
  245. package/src/hooks/backfill-cli.ts +151 -137
  246. package/src/hooks/backfill-request.ts +149 -0
  247. package/src/hooks/cloud-connection.ts +1 -1
  248. package/src/hooks/cloud-enrollment-cli.ts +78 -28
  249. package/src/hooks/cloud-introspect.ts +3 -0
  250. package/src/hooks/collector-config.ts +10 -1
  251. package/src/hooks/config-status.ts +606 -0
  252. package/src/hooks/configure-wizard.ts +915 -989
  253. package/src/hooks/daemon-service.ts +16 -6
  254. package/src/hooks/did-you-mean.ts +185 -0
  255. package/src/hooks/flush-cli.ts +33 -7
  256. package/src/hooks/fp-config.ts +96 -0
  257. package/src/hooks/fp-home.ts +11 -0
  258. package/src/hooks/fp-reset.ts +102 -95
  259. package/src/hooks/handler.ts +4 -0
  260. package/src/hooks/harness-cli.ts +87 -138
  261. package/src/hooks/install-prompt.ts +38 -477
  262. package/src/hooks/integrations.ts +1 -1
  263. package/src/hooks/jev-cli.ts +183 -196
  264. package/src/hooks/manager.ts +554 -358
  265. package/src/hooks/onboarding-attempt.ts +11 -9
  266. package/src/hooks/otel-cli.ts +344 -0
  267. package/src/hooks/otel-output.ts +24 -0
  268. package/src/hooks/otel-writers.ts +225 -0
  269. package/src/hooks/pack-cli.ts +219 -204
  270. package/src/hooks/report-body.ts +50 -0
  271. package/src/hooks/safe-config-write.ts +4 -2
  272. package/src/hooks/session-pause-cli.ts +93 -73
  273. package/src/hooks/tui.ts +849 -389
  274. package/src/hooks/uninstall-cli.ts +136 -149
  275. package/src/hooks/untraced-note.ts +52 -0
  276. package/.next/standalone/.next/server/chunks/[externals]__19_pzeq._.js +0 -3
  277. package/.next/standalone/.next/server/chunks/[externals]__20kzpkf._.js +0 -3
  278. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
  279. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +0 -3
  280. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0igxt32._.js +0 -3
  281. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
  282. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +0 -22
  283. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
  284. package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
  285. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
  286. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
  287. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
  288. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
  289. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
  290. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +0 -4
  291. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +0 -4
  292. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
  293. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +0 -6
  294. package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +0 -1
  295. /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_buildManifest.js +0 -0
  296. /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_clientMiddlewareManifest.js +0 -0
  297. /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_ssgManifest.js +0 -0
@@ -120,9 +120,16 @@ async function exitAfterFlush(code) {
120
120
  const hookIdx = args.indexOf("--hook");
121
121
  if (hookIdx >= 0) {
122
122
  if (!args[hookIdx + 1]) {
123
- console.error("Error: Missing event type after --hook");
124
- console.error("Usage: failproofai --hook <event> [--cli <claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose>]");
125
- process.exit(1);
123
+ // The notice shape, written out by hand and never coloured: importing the
124
+ // kit here — the first module this file imports — moves `tui.ts` ahead of
125
+ // the builtin policies in the bundle, so its top-level `ctx` renames their
126
+ // `ctx` parameters and changes the audit cache key (`engineVersion`) for
127
+ // every user. It also keeps the hook path an agent spawns per tool call
128
+ // loading nothing new. Written, not `console.error`, which Bun paints red.
129
+ process.stderr.write(
130
+ "✕ Missing event type after --hook.\n Run failproofai help hook for the events and agent ids.\n",
131
+ );
132
+ await exitAfterFlush(1);
126
133
  }
127
134
  const eventType = args[hookIdx + 1];
128
135
  const cliIdx = args.indexOf("--cli");
@@ -305,8 +312,8 @@ if (hookIdx >= 0) {
305
312
  * had all three. The words are still each screen's own — this owns the shape,
306
313
  * so a screen cannot drift out of the family without editing the family.
307
314
  *
308
- * Capped at 80 columns by `helpOptsFor`, so help reads the same in a maximised
309
- * window as in a tmux pane, and narrows on a terminal smaller than that.
315
+ * Drawn by `screenKit().helpPage` at the terminal's width; descriptions are
316
+ * prose and may be shortened on a terminal, never in a pipe.
310
317
  */
311
318
  /**
312
319
  * Lines a module ALREADY laid out with the kit — `harness`, `publish`,
@@ -334,46 +341,119 @@ async function printLines(lines, ok = true) {
334
341
  * flatten the structure they were expressing.
335
342
  */
336
343
  async function printReport(command, lines, opts = {}) {
337
- const { title, wrap, stack, printBlock, optsFor, INDENT, brandAnsi, ANSI_RESET } =
338
- await import("../src/hooks/tui");
344
+ const { printBlock, optsFor, screenKit, paint: palette } = await import("../src/hooks/tui");
345
+ const { reportBody } = await import("../src/hooks/report-body");
339
346
  const ok = opts.ok !== false;
340
347
  const stream = ok ? process.stdout : process.stderr;
341
348
  const o = optsFor(stream);
349
+ const kit = screenKit(o);
350
+ const c = palette(o.color);
342
351
  // `\`like this\`` becomes pink, and loses the backticks. These messages name
343
352
  // the command to run next more often than not, and pink is what you type
344
- // everywhere else on the CLI now — the help screens, the bullets, the next
345
- // steps. Applied AFTER wrapping, because an escape sequence has no width and
346
- // colouring first would make every wrap measure the wrong length.
347
- const paint = (line) =>
348
- o.color && (line.match(/`/g) || []).length % 2 === 0
349
- ? line.replace(/`([^`]+)`/g, `${brandAnsi("pink")}$1${ANSI_RESET}`)
350
- : line;
351
- const body = [];
352
- for (const line of lines) {
353
- if (line.trim() === "") body.push("");
354
- else if (line.startsWith(" ")) body.push(paint(`${INDENT}${line}`));
355
- else {
356
- for (const w of wrap(line, Math.max(20, o.cols - INDENT.length * 2))) {
357
- body.push(paint(`${INDENT}${w}`));
358
- }
359
- }
360
- }
361
- printBlock(stream, stack(title(`failproofai ${command}`, opts.meta, o), body));
353
+ // everywhere. Applied AFTER wrapping, because an escape sequence has no
354
+ // width and colouring first would make every wrap measure the wrong length.
355
+ //
356
+ // A line that opens on a state glyph gets that glyph in its role colour, the
357
+ // way a notice draws it: `update` reports a daemon that needs root that way.
358
+ const glyphRoles = { "✓": c.guide, "▲": c.warn, "✕": c.err };
359
+ const paint = (line) => {
360
+ const typed =
361
+ (line.match(/`/g) || []).length % 2 === 0 ? line.replace(/`([^`]+)`/g, (_, cmd) => c.pink(cmd)) : line;
362
+ const lead = /^(\s*)([✓▲✕]) /.exec(typed);
363
+ return lead ? `${lead[1]}${glyphRoles[lead[2]](lead[2])}${typed.slice(lead[1].length + 1)}` : typed;
364
+ };
365
+ // The 2026-10 shape: the header names the command, top-level lines start at
366
+ // column 0, and lines a module indented on purpose keep their indent. A
367
+ // failed run opens with ✕, so it reads as the failure it is — unless its
368
+ // lines already mark their own state; see report-body.ts.
369
+ const name = command.charAt(0).toUpperCase() + command.slice(1);
370
+ const body = reportBody(lines, { ok, width: o.cols - 2 }).map(paint);
371
+ printBlock(stream, [kit.header(opts.meta ? `${name} · ${opts.meta}` : name), "", ...body]);
362
372
  }
363
373
 
364
374
  async function printHelp(spec) {
365
- const { helpScreen, helpOptsFor, printBlock } = await import("../src/hooks/tui");
366
- printBlock(process.stdout, helpScreen({ version, ...spec }, helpOptsFor(process.stdout)));
375
+ const { screenKit, optsFor, printBlock } = await import("../src/hooks/tui");
376
+ // Descriptions are prose and may be shortened to fit a terminal; nothing is
377
+ // ever cut in a pipe, and a command, flag or path is never cut anywhere.
378
+ const kit = screenKit({ ...optsFor(process.stdout), fit: !!process.stdout.isTTY });
379
+ printBlock(process.stdout, kit.helpPage(spec));
367
380
  }
368
381
 
369
382
  async function runCli() {
383
+ // One page for `policies --help` and `policies add|remove|show --help`: it
384
+ // carries every flag of every policies lane, so `help policies add` lands on
385
+ // the right page with no routing of its own. Defined here, inside runCli,
386
+ // so the bundle gains no top-level name.
387
+ const POLICIES_HELP = {
388
+ name: "policies",
389
+ usage: [
390
+ ["failproofai policies [--list]", "List every policy and whether it is on"],
391
+ ["failproofai policies add [name | owner/repo]", "Turn one on, install a pack, or pick from a list"],
392
+ ["failproofai policies remove [name | pack-id]", "Turn one off, uninstall a pack, or pick from a list"],
393
+ ["failproofai policies show <owner/repo>", "See what a pack holds"],
394
+ ["failproofai policies --install [names...]", "Install hooks and turn on any policies named"],
395
+ ["failproofai policies --uninstall [names...]", "Turn named policies off; with none, remove hooks"],
396
+ ],
397
+ options: [
398
+ ["-i, -u", "Short for --install and --uninstall"],
399
+ ["--policy, --only <a,b>", "Install only these policies from a pack"],
400
+ ["--category <x,y>", "Install only these categories from a pack"],
401
+ ["--all", "List every policy; with a pack, install all of it"],
402
+ ["--cli <agent...>", "Apply to these agents only"],
403
+ ["", "claude, codex, copilot, cursor, opencode, pi,"],
404
+ ["", "hermes, openclaw, factory, devin, antigravity, goose"],
405
+ ["--scope <scope>", "user, project, local (default user); all when removing"],
406
+ ["--beta", "Include beta policies"],
407
+ ["-c, --custom <file>", "With --install, add a custom policy file (repeatable)"],
408
+ ["", "--uninstall -c removes every custom policy file"],
409
+ ["--releases", "With show: list its releases, and which one is here"],
410
+ ],
411
+ examples: [
412
+ "failproofai policies add FailproofAI/policies",
413
+ "failproofai policies add FailproofAI/policies --category git",
414
+ "failproofai policies remove block-sudo",
415
+ ],
416
+ };
417
+ // `--cli` in the three policies lanes: one or more space-separated agents,
418
+ // optionally repeated, read until the next flag or the first word that is
419
+ // not an agent, since a policy name may follow the list. One wording for
420
+ // every mistake, built from INTEGRATION_TYPES: the message this replaced
421
+ // named eight of the twelve agents, and said "Missing" for a typo too.
422
+ async function readCliFlag(list) {
423
+ const { INTEGRATION_TYPES } = await import("../src/hooks/types");
424
+ const agents = new Set(INTEGRATION_TYPES);
425
+ const cliFlagValues = [];
426
+ const cliConsumedIdxs = new Set();
427
+ for (let idx = 0; idx < list.length; idx++) {
428
+ if (list[idx] !== "--cli") continue;
429
+ let consumed = 0;
430
+ for (let j = idx + 1; j < list.length && !list[j].startsWith("-") && agents.has(list[j]); j++) {
431
+ cliFlagValues.push(list[j]);
432
+ cliConsumedIdxs.add(j);
433
+ consumed++;
434
+ }
435
+ if (consumed > 0) continue;
436
+ const half = Math.ceil(INTEGRATION_TYPES.length / 2);
437
+ const everyAgent =
438
+ `Use any of: ${INTEGRATION_TYPES.slice(0, half).join(", ")},\n` +
439
+ ` ${INTEGRATION_TYPES.slice(half).join(", ")}`;
440
+ const given = list[idx + 1];
441
+ if (given === undefined || given.startsWith("-")) {
442
+ throw new CliError(`Missing agent name after --cli.\n${everyAgent}`);
443
+ }
444
+ const { nearestName } = await import("../src/hooks/did-you-mean");
445
+ const near = nearestName(given, INTEGRATION_TYPES);
446
+ throw new CliError(`Not an agent: ${given}${near ? ` (did you mean ${near}?)` : "."}\n${everyAgent}`);
447
+ }
448
+ return { cliFlagValues, cliConsumedIdxs };
449
+ }
370
450
  // --help / -h (only when not inside a subcommand that handles its own --help)
371
451
  // `update` and `migrate` were missing here, so `failproofai update --help`
372
452
  // exited 1 with "Unexpected argument" — both commands had no reachable help
373
453
  // at all, only the paragraph in the top-level dump that this rewrite moved.
374
454
  // `help` and `publish` are new. `policy` and `pack` are canonicalized to
375
455
  // `policies` above and never reach this list.
376
- const SUBCOMMANDS = ["policies", "audit", "config", "uninstall", "backfill", "flush", "harness", "jev", "publish", "update", "migrate", "help"];
456
+ const SUBCOMMANDS = ["policies", "audit", "config", "uninstall", "backfill", "flush", "harness", "otel", "jev", "publish", "update", "migrate", "help"];
377
457
  // ── help ─────────────────────────────────────────────────────────────────
378
458
  //
379
459
  // The index and the reference manual used to be the same document: 152 lines,
@@ -393,33 +473,12 @@ async function runCli() {
393
473
  // machine-facing flag would only take space from the human-facing commands.
394
474
  if (helpTopic === "hook") {
395
475
  await printHelp({
396
- command: "--hook",
397
- tagline: "the entry point your agent CLI spawns, once per tool call",
398
- sections: [
399
- {
400
- label: "usage",
401
- entries: [["failproofai --hook <event> [--cli <name>]"]],
402
- after: [
403
- "You do not run this; `failproofai config` writes it into each CLI's",
404
- "hook configuration for you.",
405
- ],
406
- },
407
- {
408
- label: "options",
409
- entries: [
410
- ["--hook <event>", "PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, SessionStart, SessionEnd, PreCompact, Notification, PermissionRequest"],
411
- ["--cli <name>", "claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose. Defaults to claude. It selects which payload shape to expect: each CLI names its events and tool arguments differently, and failproofai canonicalizes them."],
412
- ],
413
- },
414
- {
415
- label: "how it answers",
416
- lines: [
417
- "It reads the event as JSON on stdin and answers on stdout, in whatever",
418
- "shape that CLI honours. Exit codes and response shapes differ per CLI by",
419
- "necessity — see docs.befailproof.ai. Denials are reported to the agent,",
420
- "never to you.",
421
- ],
422
- },
476
+ name: "hook",
477
+ usage: [["failproofai --hook <event> [--cli <agent>]", "Called by your agents on every tool call"]],
478
+ options: [
479
+ ["--hook <event>", "PreToolUse, PostToolUse, UserPromptSubmit, Stop and more"],
480
+ ["--cli <agent>", "claude (default), codex, copilot, cursor, opencode, pi,"],
481
+ ["", "hermes, openclaw, factory, devin, antigravity, goose"],
423
482
  ],
424
483
  });
425
484
  process.exit(0);
@@ -435,8 +494,8 @@ async function runCli() {
435
494
  : helpTopic;
436
495
  if (!SUBCOMMANDS.includes(canonical)) {
437
496
  throw new CliError(
438
- `No help for: ${helpTopic}\n` +
439
- `Run \`failproofai help\` to see every command.`,
497
+ `There is no help for '${helpTopic}'.\n` +
498
+ "Run `failproofai help` to see every command.",
440
499
  );
441
500
  }
442
501
  // `policies add|remove|show` has its own help, distinct from the listing's.
@@ -449,56 +508,69 @@ async function runCli() {
449
508
  ) {
450
509
  const extraArgs = args.filter((a) => a !== "--help" && a !== "-h" && a !== "help");
451
510
  if (extraArgs.length > 0) {
452
- throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai help\` for usage.`);
453
- }
454
- // The index is DATA, not a template literal: one renderer draws it and the
455
- // eleven `<command> --help` screens, so a row added here cannot end up in a
456
- // different dialect from the screen it points at. Colour is decoration and
457
- // never meaning — the column position already says which half is a command,
458
- // so this reads identically under NO_COLOR, piped to a file, or on a
459
- // terminal that has never heard of 24-bit.
460
- await printHelp({
461
- tagline: "guardrails for the coding agents on this machine",
462
- sections: [
463
- {
464
- label: "get it running",
465
- entries: [
466
- ["config", "Set this machine up: agents, daemon, cloud"],
467
- ["config --token <key>", "Set up and connect to Cloud, no questions asked"],
468
- ["update", "Finish an npm upgrade: migrate home, match the daemon"],
469
- ],
470
- },
471
- {
472
- label: "choose what it enforces",
473
- entries: [
474
- ["policies", "Every policy on this machine, and whether it is on"],
475
- ["policies add", "Turn one on, or install a pack: <owner>/<repo>"],
476
- ["policies remove", "Turn one off, or uninstall a whole pack"],
477
- ["publish", "Ship your own policies as a pack anyone can install"],
478
- ["jev setup", "Let Jev judge calls with your own key, above regex"],
479
- ],
480
- },
481
- {
482
- label: "see what it caught",
483
- entries: [
484
- ["(no args)", "Open the policy dashboard on localhost:8020"],
485
- ["audit", "Scan your agents' history, then open the audit view"],
486
- ["config --status", "Cloud connection, daemon version, pause state"],
487
- ],
488
- },
489
- // Named, not described. Everything here is real and reachable through
490
- // `help <command>`; none of it is what anyone types on the first day,
491
- // and a full row each is what made this screen read as a manual.
492
- {
493
- label: "less often",
494
- lines: ["policies show, harness, flush, backfill, migrate, uninstall, config --pause"],
495
- },
496
- ],
497
- footer: [
498
- "failproofai <command> [options] failproofai help <command> for detail",
499
- "docs.befailproof.ai discord.befailproof.ai -h this screen -v version",
500
- ],
501
- });
511
+ throw new CliError(`Unexpected argument: ${extraArgs[0]}.\nRun \`failproofai help\` for usage.`);
512
+ }
513
+ // The index and every `<command> --help` page are built from the same kit,
514
+ // so a row added here cannot end up in a different dialect from the page it
515
+ // points at. Colour is decoration and never meaning: the column already says
516
+ // which half is a command, so this reads the same under NO_COLOR, piped to a
517
+ // file, or on a terminal that has never heard of 24-bit.
518
+ {
519
+ // The index, in the design's shape: what to do first, then every command by
520
+ // what it is for. Composed here from the kit rather than a builder in tui.ts,
521
+ // so the bundle gains no top-level name (see the audit-cache note there).
522
+ const { screenKit, optsFor, printBlock } = await import("../src/hooks/tui");
523
+ const kit = screenKit({ ...optsFor(process.stdout), fit: !!process.stdout.isTTY });
524
+ // The command sits in the SECOND column here, so these rows never go
525
+ // through rows()' description slot, which is the one shortened to fit.
526
+ const steps = [
527
+ ["Set up this machine", "failproofai config"],
528
+ ["Turn on policies", "failproofai policies add FailproofAI/policies"],
529
+ ["See what your agents did", "failproofai"],
530
+ ];
531
+ const section = (heading, entries) => [
532
+ "",
533
+ kit.head(heading),
534
+ ...kit.rows(entries.map(([name, description]) => [name === "(no args)" ? name : kit.cmd(name), description]), 12),
535
+ ];
536
+ await printBlock(process.stdout, [
537
+ kit.header("Guardrails for coding agents on this machine"),
538
+ "",
539
+ kit.head("Get started"),
540
+ ...steps.map(([label, command], i) => ` ${i + 1} ${label.padEnd(27)} ${kit.cmd(command)}`),
541
+ ...section("Set up", [
542
+ ["config", "Connect to cloud or use open source, and pick agents to trace"],
543
+ ["update", "Finish an npm upgrade: migrate this machine and match the daemon"],
544
+ ["uninstall", "Remove hooks and the daemon from this machine"],
545
+ ]),
546
+ ...section("Enforce", [
547
+ ["policies", "List policies, turn them on or off, install packs"],
548
+ ["publish", "Ship your own policies as a pack anyone can install"],
549
+ ["jev", "Have Jev judge tool calls on top of the regex policies"],
550
+ ]),
551
+ ...section("Observe", [
552
+ ["(no args)", "Open the policy dashboard on localhost:8020"],
553
+ ["audit", "Scan your agents' history for risky and wasteful patterns"],
554
+ ["otel", "Opt in to OpenTelemetry from agents and apps"],
555
+ ]),
556
+ ...section("Less often", [
557
+ ["harness", "Collect sessions from extra locations per agent"],
558
+ ["flush", "Send queued events to cloud now"],
559
+ ["backfill", "Re-send history the collector already read"],
560
+ ["migrate", "Bring ~/.failproofai up to this version's layout"],
561
+ ]),
562
+ "",
563
+ kit.head("Examples"),
564
+ ...kit.rows([
565
+ [kit.cmd("failproofai config --status"), "Check the daemon, cloud and pauses"],
566
+ [kit.cmd("failproofai policies add block-sudo"), "Turn on one policy"],
567
+ [kit.cmd("failproofai policies show acme/guards"), "Preview a pack before installing it"],
568
+ ]),
569
+ "",
570
+ `Run ${kit.cmd("failproofai help <command>")} for its options, ${kit.cmd("failproofai -v")} for the version.`,
571
+ "docs.befailproof.ai · discord.befailproof.ai",
572
+ ]);
573
+ }
502
574
  process.exit(0);
503
575
  }
504
576
 
@@ -507,7 +579,7 @@ async function runCli() {
507
579
  if ((args.includes("--version") || args.includes("-v")) && !SUBCOMMANDS.includes(args[0])) {
508
580
  const extraArgs = args.filter((a) => a !== "--version" && a !== "-v");
509
581
  if (extraArgs.length > 0) {
510
- throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai --help\` for usage.`);
582
+ throw new CliError(`Unexpected argument: ${extraArgs[0]}.\nRun \`failproofai help\` for usage.`);
511
583
  }
512
584
  console.log(version);
513
585
  process.exit(0);
@@ -582,8 +654,9 @@ async function runCli() {
582
654
  // entry point can see this ordering.
583
655
  const { checkLayoutForCli } = await import("../src/hooks/fp-reset");
584
656
  const check = await checkLayoutForCli();
585
- for (const line of check.lines) console.error(line);
586
- if (check.fatal) process.exit(1);
657
+ // Written rather than `console.error`, which Bun paints red on a terminal.
658
+ if (check.lines.length > 0) process.stderr.write(`${check.lines.join("\n")}\n`);
659
+ if (check.fatal) await exitAfterFlush(1);
587
660
  // `check.didReset` is deliberately not read. It used to force the wizard
588
661
  // below; see the note at `shouldOfferFirstRun` for why a migrated machine
589
662
  // no longer needs setup re-run, and `didReset` in `fp-reset.ts` for what
@@ -613,6 +686,9 @@ async function runCli() {
613
686
  // never block a command on reporting
614
687
  }
615
688
 
689
+ // True once the setup wizard has run in this process. It opens with the
690
+ // logomark, so the dashboard launch at the end must not draw a second one.
691
+ let setupWizardRan = false;
616
692
  const { shouldOfferFirstRun } = await import("../src/hooks/first-run-gate");
617
693
  // `|| layoutWasReset` stood here, with `force: layoutWasReset` below, because a
618
694
  // migration used to delete the home's policy config while leaving the agent
@@ -632,7 +708,7 @@ async function runCli() {
632
708
  const { maybeFirstRunConfigure } = await import("../src/hooks/configure-wizard");
633
709
  // `audit` runs its own scan immediately after this returns; firing the
634
710
  // post-setup audit too would scan the whole history twice in a row.
635
- await maybeFirstRunConfigure({}, { postSetupAudit: args[0] !== "audit" });
711
+ setupWizardRan = await maybeFirstRunConfigure({}, { postSetupAudit: args[0] !== "audit" });
636
712
  } catch {
637
713
  // Onboarding is never allowed to block the command the user actually typed.
638
714
  }
@@ -649,33 +725,15 @@ async function runCli() {
649
725
  const subArgs = args.slice(1);
650
726
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
651
727
  await printHelp({
652
- command: "flush",
653
- tagline: "deliver what is already spooled, now",
654
- sections: [
655
- { label: "usage", entries: [["failproofai flush [--wait] [--timeout <secs>]"]] },
656
- {
657
- label: "why",
658
- lines: [
659
- "The collector is unhurried on purpose: a batch is swept once it is older",
660
- "than two minutes, at most 64 per pass, on a 60-second cadence. That pacing",
661
- "keeps a backlog from stampeding the server, and it is exactly wrong when",
662
- "you are standing at a dashboard waiting to see your own events — \"not",
663
- "delivered yet\" and \"not working\" look identical from there.",
664
- "",
665
- "This asks the daemon to make a pass right now, with no minimum age and no",
666
- "per-pass cap. It re-sends nothing: only batches already spooled and not",
667
- "yet delivered. For history the collector has already read past, use",
668
- "`failproofai backfill`.",
669
- ],
670
- },
671
- {
672
- label: "options",
673
- entries: [
674
- ["--wait", "Block until the spool drains, or --timeout elapses."],
675
- ["--timeout <secs>", "How long --wait waits. Default: 60."],
676
- ],
677
- },
728
+ name: "flush",
729
+ usage: [["failproofai flush [options]", "Send queued events to cloud now"]],
730
+ options: [
731
+ // The queue on THIS machine: a batch set aside after repeated failures
732
+ // leaves it empty without having been delivered.
733
+ ["--wait", "Wait until the queue is empty"],
734
+ ["--timeout <secs>", "How long --wait waits (default 60)"],
678
735
  ],
736
+ optionsCol: 20,
679
737
  });
680
738
  process.exit(0);
681
739
  }
@@ -685,19 +743,21 @@ async function runCli() {
685
743
  (a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--timeout",
686
744
  );
687
745
  if (unknown) {
688
- throw new CliError(`Unexpected argument: ${unknown}\nRun \`failproofai flush --help\` for usage.`);
746
+ throw new CliError(`Unexpected argument: ${unknown}.\nRun \`failproofai flush --help\` for usage.`);
689
747
  }
690
748
 
691
749
  let timeoutSecs;
692
750
  const tIdx = subArgs.indexOf("--timeout");
693
751
  if (tIdx >= 0) {
694
752
  const raw = subArgs[tIdx + 1];
695
- if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --timeout.");
753
+ if (!raw || raw.startsWith("-")) {
754
+ throw new CliError("Missing value after --timeout.\nFor example: `failproofai flush --wait --timeout 60`");
755
+ }
696
756
  const n = Number(raw);
697
757
  // Rejected rather than coerced: NaN would silently become "wait forever
698
758
  // or not at all" depending on the comparison, and neither is what was asked.
699
759
  if (!Number.isFinite(n) || n <= 0) {
700
- throw new CliError(`Could not read --timeout ${raw}. Give a number of seconds.`);
760
+ throw new CliError(`Could not read --timeout ${raw}.\nGive a number of seconds, for example \`--timeout 60\``);
701
761
  }
702
762
  timeoutSecs = n;
703
763
  }
@@ -720,45 +780,38 @@ async function runCli() {
720
780
  const subArgs = args.slice(1);
721
781
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
722
782
  await printHelp({
723
- command: "backfill",
724
- tagline: "re-send history the collector has already read past",
725
- sections: [
726
- { label: "usage", entries: [["failproofai backfill [--since <when>] [--dry-run]"]] },
727
- {
728
- label: "why",
729
- lines: [
730
- "The collector never re-reads a file it has a cursor for, which is right",
731
- "until the dashboard's data is cleared, a machine is re-enrolled, or",
732
- "cursors advanced before there was anywhere to send. Then the history",
733
- "exists on disk and nowhere else, with no way to ask for it again.",
734
- "",
735
- "Re-sending is safe: redaction is deterministic, so a re-sent event hashes",
736
- "identically to its first send and collapses into the row already there.",
737
- ],
738
- },
739
- {
740
- label: "options",
741
- entries: [
742
- ["--since <when>", "How far back: `30d`, `6m`, `YYYY-MM-DD`. Default 30 days."],
743
- ["--dry-run", "Report what would be re-read and change nothing."],
744
- ],
745
- },
746
- {
747
- label: "note",
748
- lines: [
749
- "Which streams are sent follows [collector] in ~/.failproofai/config.toml —",
750
- "a backfill never sends something your config says you do not want.",
751
- ],
752
- },
783
+ name: "backfill",
784
+ usage: [["failproofai backfill [options]", "Re-send history the collector already read"]],
785
+ options: [
786
+ ["--since <when>", "How far back: 30d, 6m, 1y or YYYY-MM-DD (default 30d)"],
787
+ ["--agents <a,b>", "Only these traced agents (default: every traced agent)"],
788
+ ["--dry-run", "Show what would be sent, send nothing"],
753
789
  ],
790
+ examples: ["failproofai backfill --since 6m", "failproofai backfill --agents claude,codex --dry-run"],
791
+ optionsCol: 20,
754
792
  });
755
793
  process.exit(0);
756
794
  }
757
795
 
758
- const KNOWN = new Set(["--since", "--dry-run"]);
759
- const unknown = subArgs.find((a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--since");
796
+ const KNOWN = new Set(["--since", "--dry-run", "--agents"]);
797
+ const TAKES_VALUE = new Set(["--since", "--agents"]);
798
+ // Every word is checked, not only the ones that start with "-": a bare
799
+ // `backfill 6m` used to be ignored, and ran the default 30 days instead of
800
+ // the six months that were typed.
801
+ const unknown = subArgs.find((a, i) => !KNOWN.has(a) && !TAKES_VALUE.has(subArgs[i - 1]));
760
802
  if (unknown) {
761
- throw new CliError(`Unexpected argument: ${unknown}\nRun \`failproofai backfill --help\` for usage.`);
803
+ const looksLikeWindow = /^\d+[dmy]$/.test(unknown) || !Number.isNaN(Date.parse(unknown));
804
+ throw new CliError(
805
+ `Unexpected argument: ${unknown}.\n` +
806
+ (looksLikeWindow ? `Did you mean \`failproofai backfill --since ${unknown}\`?` : "See its options: `failproofai backfill --help`"),
807
+ );
808
+ }
809
+ let agents;
810
+ const agentsIdx = subArgs.indexOf("--agents");
811
+ if (agentsIdx >= 0) {
812
+ const raw = subArgs[agentsIdx + 1];
813
+ if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --agents.");
814
+ agents = raw.split(",").map((a) => a.trim()).filter(Boolean);
762
815
  }
763
816
 
764
817
  let sinceMs;
@@ -785,9 +838,23 @@ async function runCli() {
785
838
 
786
839
  lastSubcommand = "backfill";
787
840
  const { runBackfillCommand } = await import("../src/hooks/backfill-cli");
788
- const result = runBackfillCommand({ sinceMs, dryRun: subArgs.includes("--dry-run") });
789
- await printReport("backfill", result.lines, { ok: result.exitCode === 0 });
790
- await track("cli_backfill", { ok: result.exitCode === 0, dry_run: subArgs.includes("--dry-run"), explicit_since: sinceIdx >= 0 });
841
+ const { optsFor } = await import("../src/hooks/tui");
842
+ const result = await runBackfillCommand({
843
+ sinceMs,
844
+ since: sinceIdx >= 0 ? subArgs[sinceIdx + 1] : undefined,
845
+ dryRun: subArgs.includes("--dry-run"),
846
+ agents,
847
+ // Drawn for stdout. A refusal goes to stderr, and printBlock drops the
848
+ // colour there when stderr cannot show it.
849
+ render: optsFor(process.stdout),
850
+ });
851
+ await printLines(result.lines, result.exitCode === 0);
852
+ await track("cli_backfill", {
853
+ ok: result.exitCode === 0,
854
+ dry_run: subArgs.includes("--dry-run"),
855
+ explicit_since: sinceIdx >= 0,
856
+ agents_named: agents ? agents.length : 0,
857
+ });
791
858
  lastSubcommand = null;
792
859
  await exitAfterFlush(result.exitCode);
793
860
  return;
@@ -808,84 +875,71 @@ async function runCli() {
808
875
  // could reach it to notice. The pack lane is entered from
809
876
  // `policies add|remove|show` below, which is the only door it has.
810
877
 
878
+ if (args[0] === "otel") {
879
+ const subArgs = args.slice(1);
880
+ if (!subArgs.length || subArgs.includes("--help") || subArgs.includes("-h")) {
881
+ const verb = ["status", "enable", "disable", "env"].includes(subArgs[0]) ? subArgs[0] : null;
882
+ const usages = {
883
+ status: "failproofai otel status",
884
+ enable: "failproofai otel enable <agent|all> [--local] [--no-content] [--yes]",
885
+ disable: "failproofai otel disable <agent|all>",
886
+ env: "failproofai otel env [--service <name>] [--local]",
887
+ };
888
+ const options = {
889
+ enable: [
890
+ ["--local", "Use the loopback OTLP relay. Automatic for Gemini CLI and Copilot."],
891
+ ["--no-content", "Do not capture prompt text or tool details."],
892
+ ["--yes", "Accept switching this agent from transcript upload to OTEL."],
893
+ ],
894
+ env: [
895
+ ["--service <name>", "Service name for env (default: my-agent)."],
896
+ ["--local", "Use the loopback OTLP relay."],
897
+ ],
898
+ status: [],
899
+ disable: [],
900
+ };
901
+ const acceptedOptions = verb ? options[verb] : [...options.enable, options.env[0]];
902
+ await printHelp({
903
+ name: verb ? `otel ${verb}` : "otel",
904
+ usage: (verb ? [usages[verb]] : Object.values(usages)).map(usage => [
905
+ usage,
906
+ verb === "enable" || verb === "disable" || !verb ? "Agents: claude, codex, gemini, copilot, all" : "",
907
+ ]),
908
+ options: acceptedOptions,
909
+ examples: verb === "status" ? ["failproofai otel status"] :
910
+ verb === "disable" ? ["failproofai otel disable all"] :
911
+ verb === "env" ? ["failproofai otel env --service my-agent --local"] :
912
+ ["failproofai otel enable claude", "failproofai otel enable codex --local --no-content", "failproofai otel disable all"],
913
+ });
914
+ process.exit(0);
915
+ }
916
+ lastSubcommand = "otel";
917
+ const { runOtelCommand } = await import("../src/hooks/otel-cli");
918
+ const { otelReportLines } = await import("../src/hooks/otel-output");
919
+ const result = await runOtelCommand(subArgs, { onEvent: track });
920
+ // env is shell input and must not be wrapped in a styled report.
921
+ if (subArgs[0] === "env" && result.exitCode === 0) console.log(result.lines.join("\n"));
922
+ else await printReport(`otel ${subArgs[0]}`, otelReportLines(subArgs[0], result), { ok: result.exitCode === 0 });
923
+ lastSubcommand = null;
924
+ await exitAfterFlush(result.exitCode);
925
+ return;
926
+ }
927
+
811
928
  if (args[0] === "harness") {
812
929
  const subArgs = args.slice(1);
813
930
  if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
814
931
  await printHelp({
815
- command: "harness",
816
- tagline: "capture sessions from more than one location per agent CLI",
817
- sections: [
818
- {
819
- label: "usage",
820
- entries: [
821
- ["failproofai harness list [<harness>]"],
822
- ["failproofai harness add-path <harness> [<label>=]<path>"],
823
- ["failproofai harness remove-path <harness> <path|label>"],
824
- ],
825
- },
826
- {
827
- label: "why",
828
- lines: [
829
- "Each agent CLI is watched wherever its own installer put it —",
830
- "~/.claude/projects, ~/.hermes/state.db, and so on. That misses every",
831
- "other arrangement: a second profile, a mounted team share, a container's",
832
- "home beside the host's, an agent an operator moved. Those hold real",
833
- "sessions and nothing collects them.",
834
- ],
835
- },
836
- {
837
- label: "the label",
838
- lines: [
839
- "An entry is `<path>` or `<label>=<path>`. The label namespaces agent ids",
840
- "as <label>-<agentId>, and it matters: two locations holding the same",
841
- "project derive the SAME id (it comes from the cwd inside the transcript,",
842
- "identical in both copies), so without a label they merge into one agent",
843
- "whose sessions interleave from two machines' worth of history. Omit it",
844
- "and one is derived from the folder name.",
845
- ],
846
- },
847
- {
848
- label: "harnesses",
849
- lines: [
850
- "claude, codex, copilot, openclaw, pi, factory, antigravity, cursor,",
851
- "goose, opencode, devin, hermes",
852
- "",
853
- "`claude` covers subagent transcripts too — they live under the same root.",
854
- ],
855
- },
856
- {
857
- label: "notes",
858
- lines: [
859
- "• Session collection must be on for any of this to be read; extra paths",
860
- " live under \"collector\" in ~/.failproofai/config.json like the defaults.",
861
- "• A path overlapping one already captured is REFUSED by the daemon at",
862
- " startup rather than collected twice under two ids. It says so in the",
863
- " journal.",
864
- "• Two entries sharing a LABEL are refused too: they would share one",
865
- " cursor directory, whose whole map is written at once, so each would",
866
- " clobber the other's watermark and re-read from zero after a restart.",
867
- "• Takes effect within seconds — the daemon re-reads config.json on an",
868
- " interval and cycles its collector. No restart, no sudo.",
869
- ],
870
- },
871
- {
872
- label: "examples",
873
- lines: [
874
- "failproofai harness add-path claude work=/srv/team/.claude/projects",
875
- "failproofai harness add-path hermes prod=/srv/hermes-prod/state.db",
876
- "failproofai harness add-path codex /mnt/other-home/.codex/sessions",
877
- "failproofai harness list",
878
- "failproofai harness remove-path claude work",
879
- "",
880
- "One home per person — label them, or both derive the same folder-name",
881
- "label and the second is refused:",
882
- "failproofai harness add-path openclaw user1=/srv/.openclaw-user1",
883
- "failproofai harness add-path openclaw user2=/srv/.openclaw-user2",
884
- "",
885
- "Containers: FAILPROOFAI_<HARNESS>_EXTRA_PATHS replaces the file's entries.",
886
- "FAILPROOFAI_OPENCLAW_EXTRA_PATHS=\"user1=/srv/.a,user2=/srv/.b\"",
887
- ],
888
- },
932
+ name: "harness",
933
+ usage: [
934
+ ["failproofai harness list [agent]", "Show extra capture paths"],
935
+ ["failproofai harness add-path <agent> [label=]<path>", "Also capture from here"],
936
+ ["failproofai harness remove-path <agent> <path | label>", "Stop capturing from here"],
937
+ ],
938
+ // For hermes, goose, opencode and devin the extra path is a database file.
939
+ examples: [
940
+ "failproofai harness add-path claude work=/srv/team/.claude/projects",
941
+ "failproofai harness add-path hermes prod=/srv/hermes-prod/state.db",
942
+ "failproofai harness remove-path claude work",
889
943
  ],
890
944
  });
891
945
  process.exit(0);
@@ -919,102 +973,35 @@ async function runCli() {
919
973
  const subArgs = args.slice(1);
920
974
  if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
921
975
  await printHelp({
922
- command: "jev",
923
- tagline: "judge tool calls with Jev — FailproofAI Cloud or your own endpoint — above a hard regex floor",
924
- sections: [
925
- {
926
- label: "usage",
927
- entries: [
928
- ["failproofai jev --url <url> --key-stdin [options]"],
929
- ["failproofai jev setup --provider <kind> --key-stdin [options]"],
930
- ["failproofai jev setup --provider failproofai [--mode <m>]"],
931
- ["failproofai jev status [--json]"],
932
- ["failproofai jev test [--json]"],
933
- ["failproofai jev models [--provider <kind>] [--url <base>] [--json]"],
934
- ["failproofai jev remove"],
935
- ],
936
- },
937
- {
938
- label: "why",
939
- lines: [
940
- "Regex policies cannot tell `rm -rf build/` asked for from `rm -rf ~`",
941
- "slipped in. Jev, TypeSafe's classifier, reads the call against what",
942
- "you asked for. With a config it judges each call in parallel with",
943
- "the regex policies: a hard policy's deny always stands, a reviewable",
944
- "one's may be cleared, and any Jev failure falls back to regex. With",
945
- "no config nothing changes — the regex policies run exactly as before.",
946
- ],
947
- },
948
- {
949
- label: "providers",
950
- entries: [
951
- ["typesafe", "api.typesafe.ai, model jev-1.13.0"],
952
- ["openrouter", "openrouter.ai, model typesafe/jev-1.13, zero-retention routing"],
953
- ["vercel", "Vercel AI Gateway, model typesafe-ai/jev"],
954
- ["cloudflare", "Workers AI, model typesafe/jev; needs --account-id"],
955
- ["custom", "any TypeSafe-compatible endpoint; needs --base-url"],
956
- ["failproofai", "FailproofAI Cloud, on your org's plan; no key or URL of your own"],
957
- ],
958
- after: [
959
- "--url reads the provider off the host, so it needs no --provider;",
960
- "any other host is custom, with that URL as its base. It is a BASE:",
961
- "/systemone is appended to it, and a URL that already names an",
962
- "endpoint (/models, /chat/completions, /systemone, …) is refused",
963
- "with the base it implies. `failproofai jev models` says which",
964
- "model ids a base serves, and setup refuses one it does not.",
965
- "",
966
- "FailproofAI Cloud needs no setup here: `failproofai config --token",
967
- "<key>` with a key that carries jev:evaluate (the \"machine\" preset)",
968
- "turns it on in observe mode when there is no jev.json yet — except",
969
- "with --no-transcripts, which only stores the key; then",
970
- "`failproofai jev setup --provider failproofai` switches it on. Its",
971
- "key stays in credentials.json; no --url ever selects it.",
972
- ],
973
- },
974
- {
975
- label: "setup options",
976
- entries: [
977
- ["--url <url>", "The endpoint. Picks the provider from its host."],
978
- ["--token <token>", "The key, on the command line — history and `ps` see it."],
979
- ["--provider <kind>", "Required the first time, or to switch providers."],
980
- ["--key-stdin", "Read the key from stdin; on a terminal, a masked prompt."],
981
- ["--key-from-env", "Store no key; read FAILPROOFAI_JEV_API_KEY per session."],
982
- ["--account-id <id>", "Cloudflare account id (32 hex characters)."],
983
- ["--base-url <url>", "Override the API base; `default` clears it."],
984
- ["--model <id>", "Override the model id; `default` clears it."],
985
- ["--mode <m>", "enforce (default), observe (log Jev, enforce regex), or off."],
986
- ["--timeout-ms <n>", "Per-call budget before falling back. Default 3000."],
987
- ],
988
- },
989
- {
990
- label: "notes",
991
- lines: [
992
- "• Global only: ~/.failproofai/jev.json, written 0600. A copy anyone",
993
- " else can read — or one in a directory anyone else can WRITE, who",
994
- " could replace it — is refused; a repository can never set it.",
995
- "• Re-running setup for the same provider keeps the key, so",
996
- " `failproofai jev setup --mode observe` just switches the mode.",
997
- "• The daemon does not see your shell's environment: keep the key in",
998
- " the file on a machine set up with `failproofai config`.",
999
- "• --token is the fast path, not the safe one: your shell history keeps",
1000
- " it and the process list shows it. Prefer --key-stdin.",
1001
- ],
1002
- },
1003
- {
1004
- label: "examples",
1005
- lines: [
1006
- "failproofai jev --url https://api.typesafe.ai/v1 --key-stdin < ~/typesafe.key",
1007
- "failproofai jev --url https://openrouter.ai/api/v1 --token <token>",
1008
- "failproofai jev setup --provider typesafe --key-stdin < ~/typesafe.key",
1009
- "failproofai jev setup --provider cloudflare --account-id <id> --key-stdin",
1010
- "failproofai config --token <key> (FailproofAI Cloud, observe mode)",
1011
- "failproofai jev setup --mode enforce (switch the configured route's mode)",
1012
- "failproofai jev setup --mode off (keep the config, stop asking Jev)",
1013
- "failproofai jev test",
1014
- "failproofai jev status",
1015
- "failproofai jev models",
1016
- ],
1017
- },
976
+ name: "jev",
977
+ // One row per subcommand: the design's `status | test | models | remove`
978
+ // runs as a shell pipeline if pasted, and the four take different flags.
979
+ usage: [
980
+ ["failproofai jev setup --provider <kind> --key-stdin", "Turn Jev on"],
981
+ ["failproofai jev --url <url> --key-stdin", "Turn Jev on from a URL"],
982
+ ["failproofai jev setup --provider failproofai", "Use FailproofAI Cloud"],
983
+ ["failproofai jev status [--json]", "Show config and activity"],
984
+ ["failproofai jev test [--json]", "Send one live request"],
985
+ ["failproofai jev models [--provider <kind>] [--url <base>] [--json]"],
986
+ ["failproofai jev remove", "Delete the config"],
987
+ ],
988
+ options: [
989
+ ["--provider <kind>", "typesafe openrouter vercel cloudflare custom failproofai"],
990
+ ["--key-stdin", "Read the API key from stdin, or a prompt on a terminal"],
991
+ ["--key-from-env", "Store no key; read it from FAILPROOFAI_JEV_API_KEY"],
992
+ ["--token <token>", "The API key on the command line; prefer --key-stdin"],
993
+ ["--url <url>", "The API base URL; picks the provider from its host"],
994
+ ["--base-url <url>", "Override the API base; default clears it"],
995
+ ["--mode <m>", "enforce, observe (log Jev, enforce regex) or off"],
996
+ ["--model <id>", "Override the model id; default clears it"],
997
+ ["--account-id <id>", "Cloudflare account id, 32 hex characters"],
998
+ ["--timeout-ms <n>", "Time per call before falling back to regex (default 3000)"],
999
+ ["--json", "Print JSON (status, test and models)"],
1000
+ ],
1001
+ examples: [
1002
+ "failproofai jev setup --provider typesafe --key-stdin < key.txt",
1003
+ "failproofai jev setup --mode observe",
1004
+ "failproofai jev test",
1018
1005
  ],
1019
1006
  });
1020
1007
  process.exit(0);
@@ -1050,36 +1037,10 @@ async function runCli() {
1050
1037
  const subArgs = args.slice(1);
1051
1038
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
1052
1039
  await printHelp({
1053
- command: "migrate",
1054
- tagline: "bring ~/.failproofai up to the layout this version speaks",
1055
- sections: [
1056
- { label: "usage", entries: [["failproofai migrate [--dry-run]"]] },
1057
- {
1058
- label: "why",
1059
- lines: [
1060
- "npm cannot update an installed package on its own, so a machine can sit",
1061
- "on an old version for months and then jump several layouts at once. This",
1062
- "runs the steps for that jump in order, keyed on the LAYOUT recorded in",
1063
- "~/.failproofai/VERSION rather than on the npm version — so skipping",
1064
- "thirty releases with no layout change runs nothing at all.",
1065
- "",
1066
- "It normally happens by itself, on the first command after an upgrade.",
1067
- "This is for running it deliberately, and for seeing what it would do",
1068
- "first.",
1069
- "",
1070
- "Your settings, cloud enrolment, policy selection, your own policy files,",
1071
- "decision history and undelivered events are carried across, not removed.",
1072
- "The irreplaceable files are copied to migrations/backup-layout<n>/ before",
1073
- "anything runs, and every step is recorded in migrations/applied.json.",
1074
- ],
1075
- },
1076
- {
1077
- label: "options",
1078
- entries: [
1079
- ["--dry-run", "Print the steps and the files it would save. Change nothing."],
1080
- ],
1081
- },
1082
- ],
1040
+ name: "migrate",
1041
+ usage: [["failproofai migrate", "Bring ~/.failproofai up to this version's layout"]],
1042
+ options: [["--dry-run", "Show the steps, change nothing"]],
1043
+ optionsCol: 20,
1083
1044
  });
1084
1045
  process.exit(0);
1085
1046
  }
@@ -1088,7 +1049,7 @@ async function runCli() {
1088
1049
  const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
1089
1050
  if (unknown) {
1090
1051
  throw new CliError(
1091
- `Unexpected argument: ${unknown}\nRun \`failproofai migrate --help\` for usage.`,
1052
+ `Unexpected argument: ${unknown}.\nRun \`failproofai migrate --help\` for usage.`,
1092
1053
  );
1093
1054
  }
1094
1055
 
@@ -1136,11 +1097,13 @@ async function runCli() {
1136
1097
  (step) => ` ${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`,
1137
1098
  );
1138
1099
  if (run.backedUp.length > 0) report.push("", `Saved first: ${run.backedUp.join(", ")}`);
1100
+ // The failing step is marked where it is: the backup line comes first, and
1101
+ // left to printReport's guess it took the ✕ with the error indented under it.
1139
1102
  if (run.failed) {
1140
1103
  report.push(
1141
1104
  "",
1142
- `Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
1143
- `The home is still marked layout ${from} and will be retried.`,
1105
+ `✕ Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
1106
+ ` The home is still marked layout ${from} and will be retried.`,
1144
1107
  );
1145
1108
  }
1146
1109
  await printReport("migrate", report, {
@@ -1162,36 +1125,10 @@ async function runCli() {
1162
1125
  const subArgs = args.slice(1);
1163
1126
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
1164
1127
  await printHelp({
1165
- command: "update",
1166
- tagline: "finish an upgrade: migrate the home, match the daemon",
1167
- sections: [
1168
- {
1169
- label: "usage",
1170
- entries: [["npm install -g failproofai@latest && failproofai update [--no-daemon]"]],
1171
- },
1172
- {
1173
- label: "why",
1174
- lines: [
1175
- "npm replaces the CLI and nothing else. The daemon binary lives at",
1176
- "~/.failproofai/bin/failproofaid-<version> and stays exactly where it was,",
1177
- "so after an npm upgrade the two halves are different versions — and",
1178
- "failproofaid refuses to start against a layout it does not speak, which",
1179
- "is the loud version of that problem rather than the silent one.",
1180
- "",
1181
- "This does the rest of the upgrade: runs any pending layout migrations,",
1182
- "puts the matching daemon binary in place, and restarts the service.",
1183
- "Hermes profiles already using failproofai are moved to the linked",
1184
- "native plugin (legacy shell hooks never checked Hermes cron jobs).",
1185
- "Exits non-zero when any half could not be brought current.",
1186
- ],
1187
- },
1188
- {
1189
- label: "options",
1190
- entries: [
1191
- ["--no-daemon", "Migrate the home only. Leaves a version-skewed daemon in place, so prefer letting it run."],
1192
- ],
1193
- },
1194
- ],
1128
+ name: "update",
1129
+ usage: [["npm i -g failproofai@latest && failproofai update"]],
1130
+ options: [["--no-daemon", "Migrate everything but leave the daemon as is"]],
1131
+ optionsCol: 20,
1195
1132
  });
1196
1133
  process.exit(0);
1197
1134
  }
@@ -1200,7 +1137,7 @@ async function runCli() {
1200
1137
  const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
1201
1138
  if (unknown) {
1202
1139
  throw new CliError(
1203
- `Unexpected argument: ${unknown}\nRun \`failproofai update --help\` for usage.`,
1140
+ `Unexpected argument: ${unknown}.\nRun \`failproofai update --help\` for usage.`,
1204
1141
  );
1205
1142
  }
1206
1143
 
@@ -1242,11 +1179,14 @@ async function runCli() {
1242
1179
  report.push(` ${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
1243
1180
  }
1244
1181
  if (run.backedUp.length > 0) report.push("", `Saved first: ${run.backedUp.join(", ")}`);
1182
+ // Each failing half marks its own line (the daemon's lines arrive with
1183
+ // their ✕ already): the report opens on whatever ran first, often a
1184
+ // success, and printReport's guess would put the ✕ there.
1245
1185
  if (run.failed) {
1246
1186
  report.push(
1247
1187
  "",
1248
- `Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
1249
- `The home is still marked layout ${state.found} and will be retried.`,
1188
+ `✕ Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
1189
+ ` The home is still marked layout ${state.found} and will be retried.`,
1250
1190
  );
1251
1191
  }
1252
1192
  } else {
@@ -1285,19 +1225,22 @@ async function runCli() {
1285
1225
  const hermesResult = await runHermesUpdateMigration({
1286
1226
  daemonSupportsPolicyEvaluation: () => svc.probeDaemonPolicyEvaluation(),
1287
1227
  });
1288
- if (hermesResult.lines.length > 0) report.push("", ...hermesResult.lines);
1228
+ if (hermesResult.lines.length > 0) {
1229
+ // Its lines open on a "Hermes:" heading over one row per profile; on a
1230
+ // failure the heading carries the ✕ and the rows say which profile.
1231
+ const [heading, ...rest] = hermesResult.lines;
1232
+ report.push("", hermesResult.ok ? heading : `✕ ${heading}`, ...rest);
1233
+ }
1289
1234
  hermesOk = hermesResult.ok;
1290
1235
  hermesMigrated = hermesResult.profiles.filter((p) => p.status === "migrated").length;
1291
1236
  } catch (err) {
1292
- report.push("", `Hermes migration failed: ${err instanceof Error ? err.message : String(err)}`);
1237
+ report.push("", `✕ Hermes migration failed: ${err instanceof Error ? err.message : String(err)}`);
1293
1238
  hermesOk = false;
1294
1239
  }
1295
1240
 
1296
1241
  const updateOk = daemonOk && !migrationFailed && hermesOk;
1297
- await printReport("update", report, {
1298
- ok: updateOk,
1299
- meta: `v${version}`,
1300
- });
1242
+ // No `v${version}` meta: the header already names the version.
1243
+ await printReport("update", report, { ok: updateOk });
1301
1244
  await track("cli_update", {
1302
1245
  ok: updateOk,
1303
1246
  migrations: migrationsRan,
@@ -1314,39 +1257,15 @@ async function runCli() {
1314
1257
  const subArgs = args.slice(1);
1315
1258
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
1316
1259
  await printHelp({
1317
- command: "uninstall",
1318
- tagline: "remove failproofai from this machine",
1319
- sections: [
1320
- { label: "usage", entries: [["failproofai uninstall [--purge] [--dry-run] [--yes]"]] },
1321
- {
1322
- label: "what it removes",
1323
- lines: [
1324
- "• failproofai hook entries from every agent CLI that has them",
1325
- "• the failproofaid daemon service — ASKED on a plain uninstall (kept",
1326
- " unless you say yes); always removed with --purge, which prompts for",
1327
- " your password rather than printing commands to paste",
1328
- "• the \"require the daemon\" flag — cleared FIRST, so a partial uninstall",
1329
- " can never leave this machine denying every tool call",
1330
- ],
1331
- },
1332
- {
1333
- label: "options",
1334
- entries: [
1335
- ["--purge", "Also delete ~/.failproofai — settings, credentials, audit history and the downloaded daemon binary. Off by default so a reinstall keeps your history."],
1336
- ["--dry-run", "Print what would be removed and change nothing."],
1337
- ["--yes, -y", "Skip the confirmation prompt. Required when there is no TTY."],
1338
- ],
1339
- },
1340
- {
1341
- label: "why this exists",
1342
- lines: [
1343
- "npm runs no uninstall script, so `npm rm -g failproofai` removes the",
1344
- "package and leaves the hook entries and the service behind. Run this",
1345
- "first, then:",
1346
- " npm rm -g failproofai",
1347
- ],
1348
- },
1260
+ name: "uninstall",
1261
+ usage: [["failproofai uninstall [options]"]],
1262
+ options: [
1263
+ ["--purge", "Also delete ~/.failproofai: settings, keys and history"],
1264
+ ["--dry-run", "Show what would be removed, change nothing"],
1265
+ ["--yes, -y", "Skip confirmation, remove the daemon; needed without a TTY"],
1349
1266
  ],
1267
+ examples: ["failproofai uninstall && npm rm -g failproofai"],
1268
+ optionsCol: 20,
1350
1269
  });
1351
1270
  process.exit(0);
1352
1271
  }
@@ -1355,79 +1274,47 @@ async function runCli() {
1355
1274
  const unknown = subArgs.find((a) => !KNOWN.has(a));
1356
1275
  if (unknown) {
1357
1276
  throw new CliError(
1358
- `Unexpected argument: ${unknown}\nRun \`failproofai uninstall --help\` for usage.`,
1277
+ `Unexpected argument: ${unknown}.\nRun \`failproofai uninstall --help\` for usage.`,
1359
1278
  );
1360
1279
  }
1361
1280
 
1362
1281
  lastSubcommand = "uninstall_command";
1363
1282
  const { runUninstallCommand } = await import("../src/hooks/uninstall-cli");
1283
+ const { screenKit, optsFor, printBlock, stack } = await import("../src/hooks/tui");
1364
1284
  const purge = subArgs.includes("--purge");
1365
1285
  const dryRun = subArgs.includes("--dry-run");
1366
1286
  const yes = subArgs.includes("--yes") || subArgs.includes("-y");
1287
+ const render = optsFor(process.stdout);
1288
+ const kit = screenKit(render);
1367
1289
 
1368
- // Set only when the prompt actually rendered the plan, which is the one
1369
- // case where printing it again would duplicate it.
1290
+ // Set only when the question was actually asked, which is the one case
1291
+ // where the plan is already on screen and printing it again would
1292
+ // duplicate it.
1370
1293
  let planWasShown = false;
1371
1294
  const result = await runUninstallCommand({
1372
1295
  purge,
1373
1296
  dryRun,
1374
1297
  yes,
1375
1298
  cwd: process.cwd(),
1376
- // Only offered when a person can actually answer. Without a TTY the
1377
- // command requires --yes rather than assuming consent — see the module.
1299
+ render,
1300
+ // ONE question (D17): the plan says exactly what goes, and `y` removes all
1301
+ // of it, the daemon service included — the same thing `--yes` does. Only
1302
+ // offered when a person can actually answer: without a TTY the command
1303
+ // requires --yes rather than assuming consent — see the module.
1378
1304
  confirm: process.stdin.isTTY
1379
1305
  ? async (planLines) => {
1380
1306
  planWasShown = true;
1381
- const { selectOne } = await import("../src/hooks/tui");
1382
- // "No" first, so the default landing position on Enter is the
1383
- // non-destructive one.
1384
- const answer = await selectOne({
1385
- message: purge
1386
- ? "Remove failproofai and DELETE ~/.failproofai?"
1387
- : "Remove failproofai from this machine?",
1388
- body: planLines,
1389
- choices: [
1390
- { label: "No, cancel", value: false },
1391
- { label: purge ? "Yes, remove and purge" : "Yes, remove it", value: true },
1392
- ],
1393
- });
1394
- return answer === true;
1307
+ process.stdout.write(["", kit.header("Uninstall"), "", ...planLines].join("\n") + "\n");
1308
+ return kit.confirm({ stdin: process.stdin, stdout: process.stdout });
1395
1309
  }
1396
1310
  : undefined,
1397
- // Only on a plain uninstall: `--purge` removes the service unconditionally
1398
- // because it deletes the binary the service points at. Absent without a TTY,
1399
- // which the module reads as "keep it".
1400
- confirmDaemon:
1401
- process.stdin.isTTY && !purge
1402
- ? async () => {
1403
- const { selectOne } = await import("../src/hooks/tui");
1404
- const answer = await selectOne({
1405
- message: "Remove the failproofaid background service too?",
1406
- body: [
1407
- "It runs as a system service and needs sudo to remove, so you",
1408
- "will be asked for your password.",
1409
- "",
1410
- "Keeping it is fine if you plan to reinstall — the hooks are",
1411
- "already gone either way, so nothing is being enforced.",
1412
- ],
1413
- choices: [
1414
- { label: "Yes, remove the service", value: true },
1415
- { label: "No, leave it installed", value: false },
1416
- ],
1417
- });
1418
- return answer === true;
1419
- }
1420
- : undefined,
1421
1311
  });
1422
1312
 
1423
- // The prompt already rendered the plan as its body; re-printing it would
1424
- // show the same block twice. `planLines` is reported by the command rather
1425
- // than guessed from the text — see UninstallResult.
1426
- const skip = planWasShown ? result.planLines : 0;
1427
- await printReport("uninstall", result.lines.slice(skip), {
1428
- ok: result.exitCode === 0,
1429
- meta: subArgs.includes("--purge") ? "purge" : subArgs.includes("--dry-run") ? "dry run" : undefined,
1430
- });
1313
+ // The question already printed the header and the plan; the outcome goes
1314
+ // under it. `planLines` is reported by the command rather than guessed from
1315
+ // the text — see UninstallResult.
1316
+ const outcome = planWasShown ? result.lines.slice(result.planLines) : [kit.header("Uninstall"), "", ...result.lines];
1317
+ printBlock(result.exitCode === 0 ? process.stdout : process.stderr, stack(outcome));
1431
1318
  // NOT after a purge. `track` resolves the instance id, and `getInstanceId()`
1432
1319
  // lazily WRITES ~/.failproofai/state/telemetry-id — which re-created the
1433
1320
  // whole directory seconds after the purge deleted it, leaving a machine the
@@ -1460,171 +1347,30 @@ async function runCli() {
1460
1347
  // made the one documented command the only one that did nothing.
1461
1348
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
1462
1349
  await printHelp({
1463
- command: "publish",
1464
- tagline: "ship your policies as a pack anyone can install",
1465
- sections: [
1466
- {
1467
- label: "two commands, from nothing",
1468
- entries: [
1469
- ["failproofai publish --init", "write a policy to start from"],
1470
- ["failproofai publish", "ship it"],
1471
- ],
1472
- after: [
1473
- "Write your policies in a git repo, run publish, answer one question,",
1474
- "done. It asks WHERE only when nothing tells it — no remote to read —",
1475
- "and works the rest out: which files hold policies, what version is",
1476
- "next, who you are. Every flag below overrides something it would",
1477
- "otherwise decide. None is required, and on a pipe or in CI the flags",
1478
- "are all there is: nothing prompts where nobody can answer.",
1479
- ],
1480
- },
1481
- {
1482
- label: "what --init does",
1483
- lines: [
1484
- "Asks what the pack is called, writes <name>.mjs, and stops. No network,",
1485
- "no git, nothing published. The file is not a template with blanks — it",
1486
- "is one policy that already blocks git push --force, so the first thing",
1487
- "you do is edit something that works. It refuses rather than overwriting",
1488
- "a file that exists.",
1489
- "",
1490
- "Then try it on THIS machine, before anyone else can see it:",
1491
- "",
1492
- " failproofai policies -i -c ./<name>.mjs",
1493
- "",
1494
- "That enforces the file right now — any path, any filename. Ask your",
1495
- "agent to do the thing you blocked and watch it get refused. Nothing is",
1496
- "published and nobody else is affected.",
1497
- ],
1498
- },
1499
- {
1500
- label: "what publish does",
1501
- lines: [
1502
- "In order, stopping before it touches GitHub if anything is wrong:",
1503
- "",
1504
- "1 Finds the policy file here by CONTENT — one that imports failproofai",
1505
- " and calls customPolicies.add — not by filename, so it finds",
1506
- " guards.mjs and ignores an unrelated policies.mjs. Not recursive:",
1507
- " publishing a fixture is worse than being asked. Two candidates and",
1508
- " it lists them.",
1509
- "2 Reads the repo from git remote get-url origin, in the FILE's",
1510
- " directory rather than yours, and decides the version (see below).",
1511
- "3 Finds your credential: GITHUB_TOKEN, GH_TOKEN, or gh auth login.",
1512
- " Needs release-write and nothing else. Never printed.",
1513
- "4 Creates the repository if it does not exist — public, see below.",
1514
- "5 Builds the three assets, validating with the LOADER's own rules: the",
1515
- " code that decides what may install on a stranger's machine. A pack",
1516
- " that could never install fails here, where you can fix it.",
1517
- "6 Creates or reuses the release and uploads, replacing assets of the",
1518
- " same name — the install URL is built from fixed names, so a stale",
1519
- " copy is what somebody would fetch.",
1520
- ],
1521
- },
1522
- {
1523
- label: "how the version is decided",
1524
- lines: [
1525
- "The commit you are publishing from — its short sha, twelve",
1526
- "characters: a1b2c3d4e5f6. Nothing to pick, nothing to count, and it",
1527
- "names exactly where the bytes came from. Publish the same source",
1528
- "twice and you get the same version, because there is nothing to",
1529
- "increment.",
1530
- "",
1531
- "Twelve rather than git's seven: seven collides in a repository with",
1532
- "enough objects, and a version that stops being unique means two",
1533
- "artifacts claiming one name. The full sha is recorded beside it.",
1534
- "",
1535
- "Read from the tree in front of you, never from the repository's",
1536
- "releases, so a fresh clone and an air-gapped machine compute the",
1537
- "same answer and neither has to ask GitHub what happened before.",
1538
- "",
1539
- "It REFUSES rather than guessing, in two cases, because the version",
1540
- "claims to name a commit and must not be minted where that is false:",
1541
- "",
1542
- " no git checkout there is no commit to name",
1543
- " uncommitted changes those bytes are not in that commit",
1544
- "",
1545
- "--version overrides both, and a tag on HEAD wins over the sha —",
1546
- "someone who tagged v1.2.0 has SAID what this release is.",
1547
- "",
1548
- "A sha does not order. `policies show <owner>/<repo> --releases` is",
1549
- "where you see which came first, newest at the top.",
1550
- ],
1551
- },
1552
- {
1553
- label: "what a release records",
1554
- lines: [
1555
- "The commit you published from, when you are in a git checkout, in the",
1556
- "manifest and in the release notes. Provenance, not verification —",
1557
- "the artifact digest is still the only thing that decides whether the",
1558
- "bytes are the ones that were published. It answers the question a",
1559
- "digest cannot: which source produced them.",
1560
- "",
1561
- "Which is also how anyone installs that exact release:",
1562
- "",
1563
- " failproofai policies add <owner>/<repo>@a1b2c3d",
1564
- "",
1565
- "and read the whole history with:",
1566
- "",
1567
- " failproofai policies show <owner>/<repo> --releases",
1568
- ],
1569
- },
1570
- {
1571
- // This screen said "an existing private one still publishes, and
1572
- // warns" for as long as that was true. It is REFUSED now — exit 1,
1573
- // nothing created or uploaded — and `--allow-private` was named
1574
- // nowhere in this help, so the one documented behaviour pointed a
1575
- // publisher at a command that exits 1 and gave them no way through.
1576
- label: "the repo must be public",
1577
- lines: [
1578
- "Installs are anonymous HTTPS with no credential to offer, so a private",
1579
- "repository publishes to nobody. A repo created here is public for that",
1580
- "reason; an existing private one is REFUSED, before anything is built,",
1581
- "created or uploaded. --allow-private publishes to one anyway, for",
1582
- "somebody who will hand the three assets over another way — it still",
1583
- "says plainly that no `policies add` can reach them.",
1584
- "",
1585
- "Only the release matters. Installs read releases/download/<tag>/<asset>",
1586
- "and never touch your git tree — pushing the source is for humans.",
1587
- ],
1588
- },
1589
- {
1590
- label: "your policy files",
1591
- lines: [
1592
- "Write as many as you like — one per category reads well. Every file",
1593
- "here that registers policies is bundled into the single artifact a pack",
1594
- "has to be: only the entry is digest-pinned, and a pack importing",
1595
- "siblings could not honestly claim to be verified. Bundling needs bun;",
1596
- "without it, name one self-contained file.",
1597
- "",
1598
- "Each policy may carry category and defaultEnabled alongside the usual",
1599
- "fields. category is what --category selects on; defaultEnabled is what",
1600
- "a bare `policies add` switches on, and it defaults to false.",
1601
- ],
1602
- },
1603
- {
1604
- label: "options",
1605
- entries: [
1606
- ["--init [file]", "Write a starter policy and stop."],
1607
- ["--repo <owner>/<repo>", "Where to release it, created if missing."],
1608
- ["--version <version>", "Name the version, instead of the commit it was built from."],
1609
- ["--id <publisher/name>", "The pack's id. Defaults to --repo."],
1610
- ["--tag <tag>", "Release tag. Defaults to the version; v prefix ok."],
1611
- ["--notes <text>", "Release notes."],
1612
- ["--out <dir>", "Where to write the assets. Default: dist-pack."],
1613
- ["--effect <effect>", "enforce or observe. observe records and blocks nothing. Default: enforce."],
1614
- ["--dry-run", "Build the assets, publish nothing. No credential."],
1615
- ["--allow-private", "Publish to an already-private repo anyway. Nobody can install it."],
1616
- ],
1617
- },
1618
- {
1619
- label: "examples",
1620
- lines: [
1621
- "failproofai publish --init",
1622
- "failproofai publish",
1623
- "failproofai publish --dry-run",
1624
- "failproofai publish ./guards.mjs --repo me/guards --version 2.0.0",
1625
- ],
1626
- },
1350
+ name: "publish",
1351
+ usage: [
1352
+ ["failproofai publish --init [file]", "Write a starter policy file"],
1353
+ ["failproofai publish [file]", "Build and release it on GitHub"],
1627
1354
  ],
1355
+ options: [
1356
+ ["--repo <owner/repo>", "Where to release, created if missing (default: origin)"],
1357
+ ["--version <v>", "Name the version (default: HEAD's tag, else short sha)"],
1358
+ ["--id <publisher/name>", "The pack id (default: the repo)"],
1359
+ ["--tag <tag>", "Release tag (default: the version, v prefix ok)"],
1360
+ ["--notes <text>", "Release notes (default: policy counts and the commit)"],
1361
+ ["--out <dir>", "Where to write the assets (default: dist-pack)"],
1362
+ ["--effect <e>", "enforce (default), or observe: record without blocking"],
1363
+ ["--min-cli-version <v>", "Oldest failproofai version that may install it"],
1364
+ ["--dry-run", "Build the assets and publish nothing"],
1365
+ ["--allow-private", "Publish to a private repo anyway; installs will fail"],
1366
+ ["--entry <file>", "The policy file, same as [file]"],
1367
+ ],
1368
+ examples: [
1369
+ "failproofai publish --init guards",
1370
+ "failproofai policies -i -c ./guards.mjs",
1371
+ "failproofai publish --repo me/guards",
1372
+ ],
1373
+ optionsCol: 24,
1628
1374
  });
1629
1375
  process.exit(0);
1630
1376
  }
@@ -1659,81 +1405,7 @@ async function runCli() {
1659
1405
  const subArgs = args.slice(1);
1660
1406
 
1661
1407
  if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
1662
- await printHelp({
1663
- command: "policies add|remove|show",
1664
- tagline: "choose what your agents may do",
1665
- sections: [
1666
- {
1667
- label: "usage",
1668
- // Without the `failproofai policies` prefix, which the heading two
1669
- // lines up already carries: repeating it costs 21 of the 80 columns
1670
- // on every row and pushes each description into a second line. The
1671
- // examples below are the copy-pasteable spelling.
1672
- entries: [
1673
- ["add", "Pick from what is installed here"],
1674
- ["add <name>", "Turn one policy on"],
1675
- ["add <owner>/<repo>", "Install someone's pack"],
1676
- ["remove <name>", "Turn one policy off"],
1677
- ["remove <pack-id>", "Uninstall a pack"],
1678
- ["show <owner>/<repo>", "What a pack contains, before you take it"],
1679
- ["show <owner>/<repo> --releases", "Every version it has published, and which one is here"],
1680
- ],
1681
- },
1682
- {
1683
- label: "a name or a source",
1684
- lines: [
1685
- "Anything with a slash is a pack source; anything without is a policy",
1686
- "name. Policy names cannot contain a slash, so there is nothing to guess.",
1687
- "",
1688
- " block-sudo a policy",
1689
- " FailproofAI/policies our pack, like any other",
1690
- " acme/deploy-guard newest release, pinned",
1691
- " acme/deploy-guard@a1b2c3d4e5f6 that release",
1692
- " acme/deploy-guard@a1b2c3d the release built from",
1693
- " that commit",
1694
- " github:acme/deploy-guard@a1b2c3d4e5f6 same, explicit",
1695
- " https://github.com/acme/x/releases/tag/v2 the URL you copied",
1696
- ],
1697
- },
1698
- {
1699
- label: "choosing part of a pack",
1700
- entries: [
1701
- ["--policy a,b", "exactly these (comma-separated or repeated)"],
1702
- ["--category x,y", "whole categories (failproofai policies show <source>)"],
1703
- ["--all", "everything it contains"],
1704
- ],
1705
- },
1706
- {
1707
- label: "options (policy names only)",
1708
- entries: [
1709
- ["--cli <agent>...", "Agent CLI(s) to apply to; space-separated or repeated. Omit to detect installed CLIs and prompt."],
1710
- ["--scope user|project|local", "Config scope. Default: user."],
1711
- ["--beta", "Allow beta policies"],
1712
- ],
1713
- },
1714
- {
1715
- label: "examples",
1716
- lines: [
1717
- "failproofai policies add",
1718
- "failproofai policies add block-sudo",
1719
- "failproofai policies add sanitize-api-keys --scope project",
1720
- "failproofai policies add FailproofAI/policies --category sanitize,git",
1721
- "failproofai policies add acme/deploy-guard --policy block-prod-deploy",
1722
- "failproofai policies show acme/deploy-guard",
1723
- "failproofai policies remove block-sudo",
1724
- ],
1725
- },
1726
- ],
1727
- footer: [
1728
- "With no flags you get the pack's own defaults and are shown the rest.",
1729
- "Re-adding at a newer version keeps what you chose.",
1730
- "Agents: claude, codex, copilot, cursor, opencode, pi, hermes, openclaw,",
1731
- "factory, devin, antigravity, goose.",
1732
- "Publishing your own: failproofai publish --help",
1733
- "Offline: FAILPROOFAI_NO_DOWNLOAD=1 refuses to fetch; packs already",
1734
- "installed keep enforcing. FAILPROOFAI_PACK_BASE_URL points it at a mirror.",
1735
- ],
1736
- });
1408
+ await printHelp(POLICIES_HELP);
1737
1409
  process.exit(0);
1738
1410
  }
1739
1411
 
@@ -1785,8 +1457,8 @@ async function runCli() {
1785
1457
  if (action === "show" || looksLikeSource) {
1786
1458
  if (action === "show" && !firstPositional) {
1787
1459
  throw new CliError(
1788
- "Usage: failproofai policies show <owner>/<repo>\n" +
1789
- "Run `failproofai policies` to see what is already installed here.",
1460
+ "Name the pack to show, as <owner>/<repo>.\n" +
1461
+ "To see what is installed here, run `failproofai policies`",
1790
1462
  );
1791
1463
  }
1792
1464
  // One lane, one implementation. `show` is the pack lane's remote preview,
@@ -1809,44 +1481,22 @@ async function runCli() {
1809
1481
  return;
1810
1482
  }
1811
1483
 
1812
- if (action !== "add" && action !== "remove") {
1813
- throw new CliError(
1814
- `Unknown policies subcommand: ${action}\n` +
1815
- `Run \`failproofai policies --help\` for usage.`,
1816
- );
1817
- }
1818
-
1484
+ // `show` left above, so only add and remove reach here. A branch for any
1485
+ // other word stood here, unreachable: this lane is entered for those three.
1486
+ const validScopes = action === "remove"
1487
+ ? ["user", "project", "local", "all"]
1488
+ : ["user", "project", "local"];
1819
1489
  const scopeIdx = rest.indexOf("--scope");
1820
1490
  const scope = scopeIdx >= 0 ? rest[scopeIdx + 1] : "user";
1821
1491
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
1822
- throw new CliError("Missing value for --scope. Valid values: user, project, local");
1492
+ throw new CliError(`Missing value for --scope.\nUse one of: ${validScopes.join(", ")}`);
1823
1493
  }
1824
- const validScopes = action === "remove"
1825
- ? ["user", "project", "local", "all"]
1826
- : ["user", "project", "local"];
1827
1494
  if (scopeIdx >= 0 && !validScopes.includes(scope)) {
1828
- throw new CliError(`Invalid scope: ${scope}. Valid values: ${validScopes.join(", ")}`);
1495
+ throw new CliError(`Invalid scope: ${scope}.\nUse one of: ${validScopes.join(", ")}`);
1829
1496
  }
1830
1497
 
1831
1498
  // --cli accepts one or more space-separated values, optionally repeated.
1832
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]);
1833
- const cliFlagValues = [];
1834
- const cliConsumedIdxs = new Set();
1835
- const cliFlagIdxs = rest.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
1836
- for (const idx of cliFlagIdxs) {
1837
- let consumed = 0;
1838
- for (let j = idx + 1; j < rest.length; j++) {
1839
- const v = rest[j];
1840
- if (v.startsWith("-")) break;
1841
- if (!VALID_CLIS.has(v)) break;
1842
- cliFlagValues.push(v);
1843
- cliConsumedIdxs.add(j);
1844
- consumed++;
1845
- }
1846
- if (consumed === 0) {
1847
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
1848
- }
1849
- }
1499
+ const { cliFlagValues, cliConsumedIdxs } = await readCliFlag(rest);
1850
1500
 
1851
1501
  const includeBeta = rest.includes("--beta");
1852
1502
 
@@ -1854,7 +1504,17 @@ async function runCli() {
1854
1504
  const knownFlags = new Set(["--scope", "--cli", "--beta"]);
1855
1505
  const unknownFlag = rest.find((a) => a.startsWith("-") && !knownFlags.has(a));
1856
1506
  if (unknownFlag) {
1857
- throw new CliError(`Unknown flag: ${unknownFlag}\nRun \`failproofai policy --help\` for usage.`);
1507
+ // -c is on the shared help page, for --install and --uninstall; pointing
1508
+ // back at that page answered the question with itself.
1509
+ const custom = unknownFlag === "-c" || unknownFlag === "--custom";
1510
+ throw new CliError(
1511
+ `Unknown flag: ${unknownFlag}.\n` +
1512
+ (custom
1513
+ ? action === "add"
1514
+ ? "Add a custom policy file with `failproofai policies --install -c <file>`"
1515
+ : "Remove custom policy files with `failproofai policies --uninstall -c`"
1516
+ : `Run \`failproofai policies ${action} --help\` for usage.`),
1517
+ );
1858
1518
  }
1859
1519
 
1860
1520
  // Positional policy names = anything not consumed by --scope / --cli.
@@ -1879,8 +1539,8 @@ async function runCli() {
1879
1539
  }
1880
1540
  if (positional.length > 1) {
1881
1541
  throw new CliError(
1882
- `\`policy ${action}\` takes exactly one policy name (got ${positional.length}).\n` +
1883
- `For multiple policies use \`failproofai policies --${action === "add" ? "install" : "uninstall"} ${positional.join(" ")}\`.`,
1542
+ `policies ${action} takes one policy name, not ${positional.length}.\n` +
1543
+ `For several, run \`failproofai policies --${action === "add" ? "install" : "uninstall"} ${positional.join(" ")}\``,
1884
1544
  );
1885
1545
  }
1886
1546
  const policyName = positional[0];
@@ -1892,6 +1552,9 @@ async function runCli() {
1892
1552
  );
1893
1553
 
1894
1554
  lastPolicyAction = action;
1555
+ // `syncAgentSelection` on both: an agent hooked here is traced, and one
1556
+ // unhooked at user scope (`policies remove all`) is not, so the next
1557
+ // `failproofai config` neither undoes nor resurrects what this ran.
1895
1558
  if (action === "add") {
1896
1559
  const { installHooks } = await import("../src/hooks/manager");
1897
1560
  await installHooks(
@@ -1903,6 +1566,7 @@ async function runCli() {
1903
1566
  undefined,
1904
1567
  false,
1905
1568
  cli,
1569
+ { syncAgentSelection: true },
1906
1570
  );
1907
1571
  await track("cli_policy_add_success", {
1908
1572
  scope,
@@ -1923,7 +1587,7 @@ async function runCli() {
1923
1587
  [policyName],
1924
1588
  scope,
1925
1589
  undefined,
1926
- { betaOnly: false, removeCustomHooks: false, cli },
1590
+ { betaOnly: false, removeCustomHooks: false, cli, syncAgentSelection: true },
1927
1591
  );
1928
1592
  await track("cli_policy_remove_success", {
1929
1593
  scope,
@@ -1945,63 +1609,7 @@ async function runCli() {
1945
1609
  const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
1946
1610
 
1947
1611
  if (isHelp) {
1948
- await printHelp({
1949
- command: "policies",
1950
- tagline: "manage the policies your agents run under",
1951
- sections: [
1952
- {
1953
- label: "usage",
1954
- entries: [
1955
- ["(bare)", "List every policy here and whether it is on"],
1956
- ["add [what]", "Turn one on, take a pack, or pick from a list"],
1957
- ["remove <what>", "Turn one off, or uninstall a pack"],
1958
- ["show <owner>/<repo>", "What a pack holds, before you take it"],
1959
- ["--install, -i", "Wire policies into your agent CLIs"],
1960
- ["--uninstall, -u", "Unwire them, or strip the hooks"],
1961
- ],
1962
- },
1963
- {
1964
- label: "options",
1965
- entries: [
1966
- ["[names...]", "Policy names. Omit for the interactive picker."],
1967
- ["--cli <agent>...", "Agent CLI(s); space-separated or repeated. Omit to detect what is installed and prompt."],
1968
- ["--scope <scope>", "user, project or local. Default: user. --uninstall also takes all."],
1969
- ["--beta", "Include beta policies. On --uninstall, only those."],
1970
- ["--custom, -c <path>", "Custom policy file; repeat for several. Bare on --uninstall, clears every explicit path."],
1971
- ],
1972
- },
1973
- {
1974
- // Enumerated ONCE. It used to be spelled out in full four times on
1975
- // this screen — twice in the --cli lines, twice more as examples
1976
- // differing only in which agent they named.
1977
- label: "agents",
1978
- lines: [
1979
- "claude, codex, copilot, cursor, opencode, pi, hermes, openclaw,",
1980
- "factory, devin, antigravity, goose",
1981
- "",
1982
- "Codex, Copilot, Cursor, OpenCode and Pi take user or project scope only.",
1983
- ],
1984
- },
1985
- {
1986
- label: "examples",
1987
- lines: [
1988
- "failproofai policies",
1989
- "failproofai policies --install",
1990
- "failproofai policies --install block-sudo sanitize-api-keys",
1991
- "failproofai policies --install --cli codex --scope project",
1992
- "failproofai policies --install --cli claude codex copilot cursor",
1993
- "failproofai policies -i -c ./my-policies.js",
1994
- "failproofai policies --uninstall block-sudo",
1995
- "failproofai policies -u",
1996
- "failproofai policies add FailproofAI/policies --category git,database",
1997
- ],
1998
- },
1999
- ],
2000
- footer: [
2001
- "policy, pack and p are all spellings of policies.",
2002
- "add, remove and show in full: failproofai policies add --help",
2003
- ],
2004
- });
1612
+ await printHelp(POLICIES_HELP);
2005
1613
  process.exit(0);
2006
1614
  }
2007
1615
 
@@ -2013,10 +1621,10 @@ async function runCli() {
2013
1621
  const scopeIdx = subArgs.indexOf("--scope");
2014
1622
  const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
2015
1623
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
2016
- throw new CliError("Missing value for --scope. Valid values: user, project, local");
1624
+ throw new CliError("Missing value for --scope.\nUse one of: user, project, local");
2017
1625
  }
2018
1626
  if (scopeIdx >= 0 && !["user", "project", "local"].includes(scope)) {
2019
- throw new CliError(`Invalid scope: ${scope}. Valid values: user, project, local`);
1627
+ throw new CliError(`Invalid scope: ${scope}.\nUse one of: user, project, local`);
2020
1628
  }
2021
1629
 
2022
1630
  const customIdxs = subArgs
@@ -2024,33 +1632,16 @@ async function runCli() {
2024
1632
  .filter((index) => index >= 0);
2025
1633
  const customPoliciesPaths = customIdxs.map((index) => subArgs[index + 1]);
2026
1634
  if (customPoliciesPaths.some((path) => !path || path.startsWith("-"))) {
2027
- throw new CliError("Missing path after --custom/-c\nUsage: --custom <path> (e.g. --custom ./my-policies.js)");
1635
+ throw new CliError("Missing path after --custom/-c.\nFor example: `failproofai policies -i -c ./my-policies.js`");
2028
1636
  }
2029
1637
 
2030
1638
  // --cli accepts one or more space-separated values, optionally repeated:
2031
1639
  // --cli claude codex copilot
2032
1640
  // --cli claude --cli codex
2033
- // Values are consumed greedily until the next flag or end of argv.
2034
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]);
2035
- const cliFlagValues = [];
2036
- const cliConsumedIdxs = new Set();
2037
- const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
2038
- for (const idx of cliFlagIdxs) {
2039
- let consumed = 0;
2040
- for (let j = idx + 1; j < subArgs.length; j++) {
2041
- const v = subArgs[j];
2042
- if (v.startsWith("-")) break;
2043
- // Stop at the first non-CLI token so a policy name following --cli
2044
- // (e.g. `--cli claude block-sudo`) is not mis-consumed as a CLI.
2045
- if (!VALID_CLIS.has(v)) break;
2046
- cliFlagValues.push(v);
2047
- cliConsumedIdxs.add(j);
2048
- consumed++;
2049
- }
2050
- if (consumed === 0) {
2051
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
2052
- }
2053
- }
1641
+ // Values are consumed greedily until the next flag, or the first word that
1642
+ // is not an agent so a policy name following --cli (e.g. `--cli claude
1643
+ // block-sudo`) is not mis-consumed as one.
1644
+ const { cliFlagValues, cliConsumedIdxs } = await readCliFlag(subArgs);
2054
1645
 
2055
1646
  const includeBeta = subArgs.includes("--beta");
2056
1647
 
@@ -2064,7 +1655,7 @@ async function runCli() {
2064
1655
  const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c", "--cli"]);
2065
1656
  const unknownInstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
2066
1657
  if (unknownInstallFlag) {
2067
- throw new CliError(`Unknown flag: ${unknownInstallFlag}\nRun \`failproofai policies --help\` for usage.`);
1658
+ throw new CliError(`Unknown flag: ${unknownInstallFlag}.\nRun \`failproofai policies --help\` for usage.`);
2068
1659
  }
2069
1660
 
2070
1661
  const explicitPolicyNames = subArgs.filter(
@@ -2084,6 +1675,8 @@ async function runCli() {
2084
1675
  "install",
2085
1676
  );
2086
1677
 
1678
+ // An agent hooked here is traced from now on, so the next
1679
+ // `failproofai config` keeps these hooks instead of taking them out.
2087
1680
  await installHooks(
2088
1681
  policyNames,
2089
1682
  scope,
@@ -2093,6 +1686,7 @@ async function runCli() {
2093
1686
  customPoliciesPaths.length > 0 ? customPoliciesPaths : undefined,
2094
1687
  false,
2095
1688
  cli,
1689
+ { syncAgentSelection: true },
2096
1690
  );
2097
1691
  await track("cli_install_success", {
2098
1692
  scope,
@@ -2113,33 +1707,14 @@ async function runCli() {
2113
1707
  const scopeIdx = subArgs.indexOf("--scope");
2114
1708
  const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
2115
1709
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
2116
- throw new CliError("Missing value for --scope. Valid values: user, project, local, all");
1710
+ throw new CliError("Missing value for --scope.\nUse one of: user, project, local, all");
2117
1711
  }
2118
1712
  if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
2119
- throw new CliError(`Invalid scope: ${scope}. Valid values: user, project, local, all`);
1713
+ throw new CliError(`Invalid scope: ${scope}.\nUse one of: user, project, local, all`);
2120
1714
  }
2121
1715
 
2122
1716
  // --cli accepts one or more space-separated values; same parser as install.
2123
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]);
2124
- const cliFlagValues = [];
2125
- const cliConsumedIdxs = new Set();
2126
- const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
2127
- for (const idx of cliFlagIdxs) {
2128
- let consumed = 0;
2129
- for (let j = idx + 1; j < subArgs.length; j++) {
2130
- const v = subArgs[j];
2131
- if (v.startsWith("-")) break;
2132
- // Stop at the first non-CLI token so a policy name following --cli
2133
- // (e.g. `--cli claude block-sudo`) is not mis-consumed as a CLI.
2134
- if (!VALID_CLIS.has(v)) break;
2135
- cliFlagValues.push(v);
2136
- cliConsumedIdxs.add(j);
2137
- consumed++;
2138
- }
2139
- if (consumed === 0) {
2140
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
2141
- }
2142
- }
1717
+ const { cliFlagValues, cliConsumedIdxs } = await readCliFlag(subArgs);
2143
1718
 
2144
1719
  const betaOnly = subArgs.includes("--beta");
2145
1720
  const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
@@ -2150,7 +1725,7 @@ async function runCli() {
2150
1725
  const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c", "--cli"]);
2151
1726
  const unknownUninstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
2152
1727
  if (unknownUninstallFlag) {
2153
- throw new CliError(`Unknown flag: ${unknownUninstallFlag}\nRun \`failproofai policies --help\` for usage.`);
1728
+ throw new CliError(`Unknown flag: ${unknownUninstallFlag}.\nRun \`failproofai policies --help\` for usage.`);
2154
1729
  }
2155
1730
 
2156
1731
  const policyNames = subArgs.filter(
@@ -2162,11 +1737,13 @@ async function runCli() {
2162
1737
  "uninstall",
2163
1738
  );
2164
1739
 
1740
+ // Hooks taken out at user scope or every scope stop the agent being
1741
+ // traced; a project- or local-only removal leaves the selection alone.
2165
1742
  await removeHooks(
2166
1743
  policyNames.length > 0 ? policyNames : undefined,
2167
1744
  scope,
2168
1745
  undefined,
2169
- { betaOnly, removeCustomHooks, cli },
1746
+ { betaOnly, removeCustomHooks, cli, syncAgentSelection: true },
2170
1747
  );
2171
1748
  await track("cli_uninstall_success", {
2172
1749
  scope,
@@ -2181,26 +1758,27 @@ async function runCli() {
2181
1758
 
2182
1759
  // Default: list policies
2183
1760
  // Accept --list as a no-op alias (common intuition), reject all other unknown flags
2184
- // and unexpected positional args (e.g. "hi").
2185
- const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list"]);
1761
+ // and unexpected positional args (e.g. "hi"). --all lists every policy instead of
1762
+ // folding the off ones into one line per category.
1763
+ const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list", "--all"]);
2186
1764
  const unknownListArg = subArgs.find((a) => a.startsWith("-") && !knownListFlags.has(a));
2187
1765
  if (unknownListArg) {
2188
1766
  throw new CliError(
2189
- `Unknown flag: ${unknownListArg}\n` +
2190
- `Run \`failproofai policies --help\` for usage.`
1767
+ `Unknown flag: ${unknownListArg}.\n` +
1768
+ "Run `failproofai policies --help` for usage."
2191
1769
  );
2192
1770
  }
2193
1771
  const positionalArgs = subArgs.filter((a) => !a.startsWith("-"));
2194
1772
  if (positionalArgs.length > 0) {
2195
1773
  throw new CliError(
2196
- `Unexpected argument: ${positionalArgs[0]}\n` +
2197
- `Run \`failproofai policies --help\` for usage.`
1774
+ `Unexpected argument: ${positionalArgs[0]}.\n` +
1775
+ "Run `failproofai policies --help` for usage."
2198
1776
  );
2199
1777
  }
2200
1778
 
2201
1779
  lastSubcommand = "list";
2202
1780
  const { listHooks } = await import("../src/hooks/manager");
2203
- await listHooks();
1781
+ await listHooks(undefined, { all: subArgs.includes("--all") });
2204
1782
  await track("cli_list_invoked", {});
2205
1783
  process.exit(0);
2206
1784
  }
@@ -2224,103 +1802,72 @@ async function runCli() {
2224
1802
  if (args[0] === "config") {
2225
1803
  if (args.includes("--help") || args.includes("-h")) {
2226
1804
  await printHelp({
2227
- command: "config",
2228
- tagline: "set this machine up",
2229
- sections: [
2230
- {
2231
- label: "usage",
2232
- entries: [
2233
- ["(bare)", "Guided setup: agents, daemon, cloud"],
2234
- ["--token <key>", "Set up and connect to Cloud, asking nothing"],
2235
- ["--status", "Connection, daemon version and pause state"],
2236
- ["--pause [<time>]", "Pause enforcement for one session"],
2237
- ["--resume [--all]", "End a pause early"],
2238
- ["--disconnect", "Stop pulling policy and sending activity"],
2239
- ["configure, setup", "Aliases for config"],
2240
- ],
2241
- },
2242
- {
2243
- label: "what it does",
2244
- lines: [
2245
- "Installs the failproofaid service (needs root once), wires hooks into",
2246
- "every agent CLI it supports, and optionally connects this machine to",
2247
- "Cloud. It chooses NO policies — take some with:",
2248
- "",
2249
- " failproofai policies add <owner>/<repo>",
2250
- ],
2251
- },
2252
- {
2253
- label: "with no terminal (CI, containers, an agent driving it)",
2254
- lines: [
2255
- "It just runs. There is nothing to confirm when nobody is watching, so it",
2256
- "applies rather than asking — no flag needed.",
2257
- "",
2258
- "Exit 1 if anything it was asked to do did not happen — including a key",
2259
- "the server refused, and a machine that could not reach root. sudo is",
2260
- "never prompted for here; it either works without a password, or you are",
2261
- "told the exact commands to run.",
2262
- ],
2263
- },
2264
- {
2265
- label: "failproof cloud",
2266
- entries: [
2267
- ["--token <key>", "The API key. Passing one IS the request to connect."],
2268
- ["--url <url>", "Somewhere other than app.befailproof.ai"],
2269
- ["--machine-id <id>", "Defaults to a stable per-machine key"],
2270
- ["--machine-label <n>", "Dashboard name. Alone, renames a connected machine."],
2271
- ["--no-transcripts", "Decisions only, no session transcripts"],
2272
- ["--connect <url>", "Enrol only, on a machine already set up"],
2273
- ["--disconnect", "Stop pulling policy and sending activity"],
2274
- ],
2275
- },
2276
- {
2277
- label: "what connecting means",
2278
- lines: [
2279
- "The key can come from FAILPROOFAI_CLOUD_TOKEN instead of the command",
2280
- "line, and the url from FAILPROOFAI_CLOUD_URL — the same variable the",
2281
- "daemon reads. Prefer the environment: an argument is readable from `ps`",
2282
- "by every user on the box, and lands in shell history and CI logs.",
2283
- "",
2284
- "One connection, two capabilities: this machine PULLS centrally-managed",
2285
- "policies and SENDS what its hooks decided. Both are checked against the",
2286
- "server before anything is written, and reported separately — a key",
2287
- "carrying policies:pull but not events:add connects for policy and says",
2288
- "exactly why the dashboard is empty.",
2289
- "",
2290
- "Connecting sends BOTH policy decisions and full session transcripts. A",
2291
- "transcript carries prompts, file contents and whatever was pasted into a",
2292
- "terminal — that is the point of connecting, and it is stated here rather",
2293
- "than buried behind a flag nobody finds. Use --no-transcripts for",
2294
- "decisions only.",
2295
- "",
2296
- "Tokens are stored owner-only in ~/.failproofai/, never in the service",
2297
- "unit — that file is world-readable. Connecting needs no sudo.",
2298
- ],
2299
- },
2300
- {
2301
- label: "pausing enforcement (one session, always time-boxed)",
2302
- entries: [
2303
- ["--pause", "This directory's newest agent session, for 30m"],
2304
- ["--pause 10m", "A given time (max 8h; s/m/h, bare = minutes)"],
2305
- ["--resume", "End the pause early"],
2306
- ["--resume --all", "End every active pause"],
2307
- ["--session <id>", "Target a specific session"],
2308
- ["--status", "What is paused, and when it lifts"],
2309
- ],
2310
- after: [
2311
- "A pause suspends builtin, custom and convention policies for that",
2312
- "session only, and always expires on its own. Cloud-managed policies",
2313
- "keep enforcing.",
2314
- ],
2315
- },
1805
+ name: "config",
1806
+ usage: [["failproofai config [options]"]],
1807
+ options: [
1808
+ ["--token <key>", "Connect with this key; prefer FAILPROOFAI_CLOUD_TOKEN"],
1809
+ ["--oss", "Use open source; leaves any connection as it is"],
1810
+ ["--agents <a,b>", "Trace these agents, or all, without asking"],
1811
+ ["--status", "Show the cloud connection, daemon version and pauses"],
1812
+ ["--pause [time]", "Pause enforcement for a session (default 30m, max 8h)"],
1813
+ ["--resume [--all]", "End a pause early, or every pause with --all"],
1814
+ ["--session <id>", "Session to pause or resume (default: newest one here)"],
1815
+ ["--connect <url>", "Connect to cloud only (for a machine already set up)"],
1816
+ ["--disconnect", "Stop pulling policies and sending activity"],
1817
+ ["--no-transcripts", "Send policy decisions only, not session transcripts"],
1818
+ ["--url <url>", "Use a self-hosted cloud (or FAILPROOFAI_CLOUD_URL)"],
1819
+ ["--machine-label <name>", "Rename this machine; with --token, set up under it"],
1820
+ ["--machine-id <id>", "Machine id for --connect (default: a stable id)"],
1821
+ ],
1822
+ // The key never goes on the command line: config itself warns that argv
1823
+ // lands in shell history and the process list.
1824
+ examples: [
1825
+ "read -rs FAILPROOFAI_CLOUD_TOKEN && export FAILPROOFAI_CLOUD_TOKEN",
1826
+ "failproofai config",
1827
+ "failproofai config --pause 1h",
2316
1828
  ],
2317
- footer: ["Prefer flags? failproofai policies --help"],
1829
+ optionsCol: 22,
2318
1830
  });
2319
1831
  process.exit(0);
2320
1832
  process.exit(0);
2321
1833
  }
2322
1834
  lastSubcommand = "config";
2323
1835
 
1836
+ // Every option config takes. A typo used to fall through to the full setup
1837
+ // run with the option ignored, so `--agent claude` asked every question.
1838
+ //
1839
+ // `--send-transcripts` is retired, not unknown: it was the transcript
1840
+ // opt-in until transcripts became the default (#653), and provisioning
1841
+ // scripts written then still pass it. Accepted, and does nothing.
1842
+ const CONFIG_FLAGS = new Set([
1843
+ "--token", "--oss", "--agents", "--status", "--pause", "--resume", "--all", "--session",
1844
+ "--connect", "--disconnect", "--no-transcripts", "--url", "--machine-label", "--machine-id",
1845
+ "--send-transcripts",
1846
+ ]);
1847
+ const CONFIG_VALUE_FLAGS = new Set([
1848
+ "--token", "--agents", "--pause", "--session", "--connect", "--url", "--machine-label", "--machine-id",
1849
+ ]);
1850
+ const configRest = args.slice(1);
1851
+ const strayConfig = configRest.find(
1852
+ (a, i) => !CONFIG_FLAGS.has(a) && !(CONFIG_VALUE_FLAGS.has(configRest[i - 1]) && !a.startsWith("-")),
1853
+ );
1854
+ if (strayConfig) {
1855
+ const asFlag = `--${strayConfig.replace(/^-+/, "")}`;
1856
+ throw new CliError(
1857
+ `Unexpected argument: ${strayConfig}.\n` +
1858
+ (CONFIG_FLAGS.has(asFlag) && asFlag !== strayConfig
1859
+ ? `Did you mean \`failproofai config ${asFlag}\`?`
1860
+ : "See its options: `failproofai config --help`"),
1861
+ );
1862
+ }
1863
+
1864
+ if (args.includes("--send-transcripts") && args.includes("--no-transcripts")) {
1865
+ throw new CliError(
1866
+ "--send-transcripts and --no-transcripts cannot be combined.\n" +
1867
+ "Transcripts are sent by default; --no-transcripts alone turns them off.",
1868
+ );
1869
+ }
1870
+
2324
1871
  // --pause / --resume / --status are non-interactive session actions that
2325
1872
  // share `config`'s surface but not the wizard. They write session state,
2326
1873
  // never the config file — a pause that reached policies-config.json would
@@ -2336,14 +1883,33 @@ async function runCli() {
2336
1883
  // keeps its old meaning (the name to enrol under); on its own it changes the
2337
1884
  // name of a machine that is already connected, which previously required
2338
1885
  // re-running enrolment with the url and token again just to fix a display name.
1886
+ //
1887
+ // Alongside a setup option (--token, --oss, --agents) it is the name to set up
1888
+ // under: `config --token K --machine-label L` used to run as a rename, so
1889
+ // setup never ran and the key was ignored. Setup applies it when it connects;
1890
+ // otherwise it is refused up front or applied as a rename afterwards (below).
2339
1891
  const wantsRename =
2340
- connectIdx < 0 && !wantsDisconnect && args.includes("--machine-label");
1892
+ connectIdx < 0 &&
1893
+ !wantsDisconnect &&
1894
+ args.includes("--machine-label") &&
1895
+ !["--token", "--oss", "--agents"].some((flag) => args.includes(flag));
2341
1896
  // A key given as --token is in shell history whatever became of the run.
2342
1897
  const warnTokenOnArgv = async () => {
2343
1898
  if (!args.includes("--token")) return;
2344
1899
  const { CONFIG_TOKEN_HISTORY_WARNING } = await import("../src/hooks/cloud-enrollment-cli");
2345
- process.stderr.write(`\n${CONFIG_TOKEN_HISTORY_WARNING.join("\n")}\n`);
1900
+ const { screenKit, colorsEnabled } = await import("../src/hooks/tui");
1901
+ const kit = screenKit({ color: colorsEnabled(process.stderr) });
1902
+ process.stderr.write(`\n${kit.notice("caution", CONFIG_TOKEN_HISTORY_WARNING.join("\n")).join("\n")}\n`);
2346
1903
  };
1904
+ // The fix for a flag given no value. Never "put the key after --token": the
1905
+ // whole point of the variable is that a key on argv lands in shell history.
1906
+ const missingValue = (flag) =>
1907
+ new CliError(
1908
+ `Missing value after ${flag}.\n` +
1909
+ (flag === "--token"
1910
+ ? "Or leave --token out and set FAILPROOFAI_CLOUD_TOKEN."
1911
+ : "Run `failproofai config --help` for what it takes."),
1912
+ );
2347
1913
  if (connectIdx >= 0 || wantsDisconnect || wantsRename) {
2348
1914
  if (connectIdx >= 0 && wantsDisconnect) {
2349
1915
  throw new CliError("--connect and --disconnect cannot be combined.");
@@ -2352,7 +1918,7 @@ async function runCli() {
2352
1918
  const i = args.indexOf(flag);
2353
1919
  if (i < 0) return undefined;
2354
1920
  const v = args[i + 1];
2355
- if (!v || v.startsWith("-")) throw new CliError(`Missing value after ${flag}.`);
1921
+ if (!v || v.startsWith("-")) throw missingValue(flag);
2356
1922
  return v;
2357
1923
  };
2358
1924
  let result;
@@ -2382,7 +1948,11 @@ async function runCli() {
2382
1948
  sessions: !args.includes("--no-transcripts"),
2383
1949
  });
2384
1950
  }
2385
- await printLines(result.lines, result.exitCode === 0);
1951
+ // Through the shared presenter: a header naming what ran, and a failure
1952
+ // opening with ✕ and its fix under it, like every other action result.
1953
+ await printReport(wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect", result.lines, {
1954
+ ok: result.exitCode === 0,
1955
+ });
2386
1956
  await warnTokenOnArgv();
2387
1957
  await track("cli_cloud_enrollment", {
2388
1958
  action: wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect",
@@ -2398,11 +1968,22 @@ async function runCli() {
2398
1968
  if (pauseIdx >= 0 || wantsResume || wantsStatus) {
2399
1969
  const chosen = [pauseIdx >= 0 && "--pause", wantsResume && "--resume", wantsStatus && "--status"].filter(Boolean);
2400
1970
  if (chosen.length > 1) {
2401
- throw new CliError(`${chosen.join(" and ")} cannot be combined.`);
1971
+ throw new CliError(`${chosen.join(" and ")} cannot be combined.\nRun them one at a time.`);
1972
+ }
1973
+ // `--status` answers "what is this machine's state?": the daemon, the
1974
+ // dashboard and any pauses, the cloud connection and whether data
1975
+ // arrives, and what enforcement is doing — one screen, one column.
1976
+ if (wantsStatus) {
1977
+ const { runStatusCommand } = await import("../src/hooks/config-status");
1978
+ const status = await runStatusCommand({ cwd: process.cwd() });
1979
+ await printLines(status.lines);
1980
+ await track("cli_pause_invoked", { action: "status", ok: true, affected: status.paused });
1981
+ await exitAfterFlush(0);
1982
+ return;
2402
1983
  }
2403
1984
  const sessionIdx = args.indexOf("--session");
2404
1985
  if (sessionIdx >= 0 && !args[sessionIdx + 1]) {
2405
- throw new CliError("Missing session id after --session.");
1986
+ throw new CliError("Missing session id after --session.\nOr leave --session out to use the newest session here.");
2406
1987
  }
2407
1988
  // A bare `--pause` takes the default duration, so only treat the next
2408
1989
  // token as a duration when it isn't another flag.
@@ -2411,55 +1992,15 @@ async function runCli() {
2411
1992
 
2412
1993
  const { runPauseCommand } = await import("../src/hooks/session-pause-cli");
2413
1994
  const result = runPauseCommand({
2414
- action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
1995
+ action: pauseIdx >= 0 ? "pause" : "resume",
2415
1996
  duration,
2416
1997
  sessionId: sessionIdx >= 0 ? args[sessionIdx + 1] : undefined,
2417
1998
  all: args.includes("--all"),
2418
1999
  cwd: process.cwd(),
2419
2000
  });
2420
- // `--status` answers "what is this machine's state?", which is both
2421
- // halves: whether enforcement is paused AND whether cloud is connected.
2422
- if (wantsStatus) {
2423
- const { connectionStatusReport, versionStatusLines } = await import(
2424
- "../src/hooks/cloud-enrollment-cli"
2425
- );
2426
- const { optsFor, printBlock, rows, stack, title, warning } = await import(
2427
- "../src/hooks/tui"
2428
- );
2429
- const opts = optsFor(process.stdout);
2430
- const report = connectionStatusReport();
2431
- const { hermesProfileStatusRows } = await import("../src/hooks/integrations");
2432
- const hermesRows = hermesProfileStatusRows();
2433
- // The version line was written to be "the only place a user can find out
2434
- // which daemon they are running" and then never called from anywhere. It
2435
- // is the right thing for the heading to carry, and it retires a heading
2436
- // that would otherwise have said the word "status" back to someone who
2437
- // just typed it.
2438
- printBlock(
2439
- process.stdout,
2440
- stack(
2441
- title("failproofai config", versionStatusLines()[0], opts),
2442
- // ONE rows() call over both blocks, so the connection facts and the
2443
- // enforcement state share a label column. Rendered separately they
2444
- // computed one column each and the window read as two commands'
2445
- // output stacked up.
2446
- //
2447
- // Always printed, including where reports can never work: "why am I
2448
- // not getting them?" is the question --status exists to answer, and
2449
- // an omitted line answers it with silence.
2450
- rows([...report.rows, ...hermesRows, ...(result.rows ?? [])], opts),
2451
- report.warnings.length > 0 ? warning(report.warnings, opts) : null,
2452
- // The trailer is not rows — it is the note and the resume command.
2453
- // Rendering only `rows` dropped the one line that tells a paused
2454
- // user how to get unpaused.
2455
- result.rows ? (result.trailer ?? null) : result.lines,
2456
- ),
2457
- );
2458
- } else {
2459
- await printLines(result.lines, result.exitCode === 0);
2460
- }
2001
+ await printLines(result.lines, result.exitCode === 0);
2461
2002
  await track("cli_pause_invoked", {
2462
- action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
2003
+ action: pauseIdx >= 0 ? "pause" : "resume",
2463
2004
  ok: result.exitCode === 0,
2464
2005
  affected: result.affected,
2465
2006
  });
@@ -2480,20 +2021,66 @@ async function runCli() {
2480
2021
  const i = args.indexOf(flag);
2481
2022
  if (i < 0) return undefined;
2482
2023
  const v = args[i + 1];
2483
- if (!v || v.startsWith("-")) throw new CliError(`Missing value after ${flag}.`);
2024
+ if (!v || v.startsWith("-")) throw missingValue(flag);
2484
2025
  return v;
2485
2026
  };
2027
+ // --oss skips the connect step and leaves any connection as it is (Tab's
2028
+ // meaning, decision D4); --disconnect is what removes one. It overrides a
2029
+ // key in the environment, and only an explicit --token contradicts it.
2030
+ const oss = args.includes("--oss");
2031
+ if (oss && args.includes("--token")) {
2032
+ throw new CliError("--oss and --token cannot be combined: one uses open source, the other connects.");
2033
+ }
2034
+ // --agents: any of the twelve, or `all`, validated here so a typo is an
2035
+ // error rather than an agent silently left untraced.
2036
+ let agents;
2037
+ if (args.includes("--agents")) {
2038
+ const raw = valueFor("--agents");
2039
+ const { INTEGRATION_TYPES } = await import("../src/hooks/types");
2040
+ const ids = raw === "all" ? [...INTEGRATION_TYPES] : raw.split(",").map((a) => a.trim()).filter(Boolean);
2041
+ const unknown = ids.filter((id) => !INTEGRATION_TYPES.includes(id));
2042
+ if (ids.length === 0 || unknown.length > 0) {
2043
+ throw new CliError(`Not an agent: ${unknown.join(", ") || raw}.\nAgents: ${INTEGRATION_TYPES.join(", ")}, or all`);
2044
+ }
2045
+ agents = ids;
2046
+ }
2047
+ // `--machine-label` here is the name to connect under. When this run cannot
2048
+ // connect at all it is refused now, before anything is written; on a
2049
+ // machine that stays connected as it was, it is applied after setup.
2050
+ const machineLabel = valueFor("--machine-label");
2051
+ if (machineLabel !== undefined) {
2052
+ const { machineLabelRefusal } = await import("../src/hooks/cloud-enrollment-cli");
2053
+ const refusal = machineLabelRefusal({
2054
+ oss,
2055
+ keyGiven: !oss && Boolean(valueFor("--token") ?? process.env.FAILPROOFAI_CLOUD_TOKEN),
2056
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
2057
+ });
2058
+ if (refusal) throw new CliError(refusal);
2059
+ }
2486
2060
  const { runConfigureWizard } = await import("../src/hooks/configure-wizard");
2487
2061
  const result = await runConfigureWizard(
2488
2062
  {},
2489
2063
  {
2490
- token: valueFor("--token") ?? process.env.FAILPROOFAI_CLOUD_TOKEN,
2064
+ token: oss ? undefined : (valueFor("--token") ?? process.env.FAILPROOFAI_CLOUD_TOKEN),
2491
2065
  url: valueFor("--url") ?? process.env.FAILPROOFAI_CLOUD_URL,
2492
2066
  machineId: valueFor("--machine-id"),
2493
- machineLabel: valueFor("--machine-label"),
2067
+ machineLabel,
2494
2068
  noTranscripts: args.includes("--no-transcripts"),
2069
+ oss,
2070
+ agents,
2495
2071
  },
2496
2072
  );
2073
+ // Setup names the machine only when it connects; when it left a
2074
+ // connection as it was, the label is the rename the flag asked for.
2075
+ let labelExit = 0;
2076
+ if (result.applied && machineLabel !== undefined) {
2077
+ const { applyMachineLabelAfterSetup } = await import("../src/hooks/cloud-enrollment-cli");
2078
+ const renamed = await applyMachineLabelAfterSetup(machineLabel);
2079
+ if (renamed) {
2080
+ await printReport("rename", renamed.lines, { ok: renamed.exitCode === 0 });
2081
+ labelExit = renamed.exitCode;
2082
+ }
2083
+ }
2497
2084
  await warnTokenOnArgv();
2498
2085
  await track("cli_configure_invoked", {
2499
2086
  applied: result.applied,
@@ -2505,74 +2092,50 @@ async function runCli() {
2505
2092
  scopes: result.scopes ?? [],
2506
2093
  cli_count: result.clis?.length ?? 0,
2507
2094
  abort: result.abort ?? null,
2095
+ oss,
2096
+ agents_named: agents ? agents.length : 0,
2508
2097
  });
2509
2098
  // `abort` is the field `WizardAbort` exists to expose, and exiting 0
2510
2099
  // regardless discarded it: a fleet script could not tell "the user pressed
2511
2100
  // Esc" from "this machine could not install the required daemon and is
2512
2101
  // unconfigured". Cancelling is not a failure; the other two are.
2513
- await exitAfterFlush(!result.applied && result.abort && result.abort !== "cancelled" ? 1 : 0);
2102
+ await exitAfterFlush(!result.applied && result.abort && result.abort !== "cancelled" ? 1 : labelExit);
2514
2103
  return;
2515
2104
  }
2516
2105
 
2517
- // Shared by both "unknown thing" guards below, so a mistyped SUBCOMMAND gets
2518
- // the same nearest-match treatment a mistyped flag already got.
2519
- function levenshtein(a, b) {
2520
- const m = a.length, n = b.length;
2521
- const dp = Array.from({ length: m + 1 }, (_, i) =>
2522
- Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
2523
- );
2524
- for (let i = 1; i <= m; i++)
2525
- for (let j = 1; j <= n; j++)
2526
- dp[i][j] = a[i - 1] === b[j - 1]
2527
- ? dp[i - 1][j - 1]
2528
- : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
2529
- return dp[m][n];
2530
- }
2531
-
2532
- // Unknown flag guard — must appear after all known-flag branches
2106
+ // Unknown flag guard — must appear after all known-flag branches.
2107
+ //
2108
+ // Both guards below suggest a whole command, runnable as printed, or nothing
2109
+ // at all. The nearest-spelling guess they replaced always answered, so
2110
+ // `status` got `flush`, `install` got `uninstall` and `-i` got `audit`; see
2111
+ // did-you-mean.ts for the table of words people type and the cut-off.
2533
2112
  const knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
2534
2113
  const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
2535
2114
 
2536
2115
  if (unknownFlag) {
2537
- const primary = ["--version", "--help", "--hook", "policies", "policy", "audit"];
2538
- const closest = primary.reduce((best, flag) => {
2539
- const dist = levenshtein(unknownFlag, flag);
2540
- return dist < best.dist ? { flag, dist } : best;
2541
- }, { flag: primary[0], dist: Infinity });
2542
-
2116
+ const { suggestFlag } = await import("../src/hooks/did-you-mean");
2117
+ const suggestion = suggestFlag(unknownFlag);
2543
2118
  throw new CliError(
2544
- `Unknown flag: ${unknownFlag}\n` +
2545
- `Did you mean: ${closest.flag}?\n` +
2546
- `Run \`failproofai --help\` for usage details.`
2119
+ `Unknown flag: ${unknownFlag}.\n` +
2120
+ (suggestion ? `Did you mean \`${suggestion}\`?` : "Run `failproofai help` for usage.")
2547
2121
  );
2548
2122
  }
2549
2123
 
2550
2124
  // Unknown subcommand guard (non-flag args that aren't a known subcommand)
2551
2125
  const unknownSubcommand = args.find(a => !a.startsWith("-") && !SUBCOMMANDS.includes(a));
2552
2126
  if (unknownSubcommand) {
2553
- // Nearest match rather than a hardcoded "policies", which was wrong for
2554
- // every input that was not a typo of it. `auth` made that concrete: it was
2555
- // a real subcommand until this release, so an old script or plain muscle
2556
- // memory lands here, and answering "did you mean policies?" sends someone
2557
- // to the one command that has nothing to do with what they typed.
2558
- const nearest = SUBCOMMANDS.reduce(
2559
- (best, name) => {
2560
- const dist = levenshtein(unknownSubcommand, name);
2561
- return dist < best.dist ? { name, dist } : best;
2562
- },
2563
- { name: SUBCOMMANDS[0], dist: Infinity },
2564
- );
2127
+ const { suggestCommand } = await import("../src/hooks/did-you-mean");
2128
+ const suggestion = suggestCommand(unknownSubcommand, SUBCOMMANDS);
2565
2129
  throw new CliError(
2566
- `Unknown command: ${unknownSubcommand}\n` +
2567
- `Did you mean: failproofai ${nearest.name}?\n` +
2568
- `Run \`failproofai --help\` for usage details.`
2130
+ `There is no command called '${unknownSubcommand}'.\n` +
2131
+ (suggestion ? `Did you mean \`${suggestion}\`?` : "Run `failproofai help` to see every command.")
2569
2132
  );
2570
2133
  }
2571
2134
 
2572
2135
  // Dashboard launch — always production mode. Runs on every bare `failproofai`
2573
2136
  // (first-run onboarding, if any, already ran above).
2574
2137
  const { launch } = await import("../scripts/launch");
2575
- launch("start");
2138
+ launch("start", { logo: !setupWizardRan });
2576
2139
  }
2577
2140
 
2578
2141
  // ── Import CliError for use in the guard above ────────────────────────────────
@@ -2600,8 +2163,19 @@ try {
2600
2163
  exit_code: err.exitCode,
2601
2164
  });
2602
2165
  }
2603
- console.error(`Error: ${err.message}`);
2604
- process.exit(err.exitCode);
2166
+ // `✕ <what went wrong>`, then each fix on its own line, indented two, with
2167
+ // the command in pink (see `notice` in tui.ts). The exit code is the error's.
2168
+ // Written rather than `console.error`, which Bun paints red on a terminal —
2169
+ // a colour from outside the palette — and drained before exiting.
2170
+ let failureText = `✕ ${err.message}`;
2171
+ try {
2172
+ const { screenKit, colorsEnabled } = await import("../src/hooks/tui");
2173
+ failureText = screenKit({ color: colorsEnabled(process.stderr) }).notice("fail", err.message).join("\n");
2174
+ } catch {
2175
+ // The plain glyph is still the right shape; the kit is decoration.
2176
+ }
2177
+ process.stderr.write(`${failureText}\n`);
2178
+ await exitAfterFlush(err.exitCode);
2605
2179
  }
2606
2180
  // Unexpected internal error — show message only, no stack trace
2607
2181
  const msg = err instanceof Error ? err.message : String(err);
@@ -2619,6 +2193,8 @@ try {
2619
2193
  error_type: err instanceof Error ? err.name : "unknown",
2620
2194
  });
2621
2195
  }
2622
- console.error(`Unexpected error: ${msg}`);
2623
- process.exit(2);
2196
+ // Plain, never through the kit: this is the path for an install so broken
2197
+ // that anything could be the next thing to fail.
2198
+ process.stderr.write(`✕ Unexpected error: ${msg}\n`);
2199
+ await exitAfterFlush(2);
2624
2200
  }