failproofai 0.0.14 → 0.0.15-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +2 -2
  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 +2 -2
  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.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/invite/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/status/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  47. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  49. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/index.html +1 -1
  52. package/.next/standalone/.next/server/app/index.rsc +15 -15
  53. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  54. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  55. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  56. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  57. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  59. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/page.js +2 -2
  61. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  64. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  65. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  66. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  69. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  70. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -4
  71. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  74. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  75. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  76. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  77. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  80. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  81. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  82. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0hv08mx._.js → [root-of-the-server]__0-dh5jy._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0obaznk._.js → [root-of-the-server]__0h3lfmp._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xvsnvt._.js → [root-of-the-server]__0l4vuqm._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +4 -4
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +6 -4
  94. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1hkdkmz._.js → [root-of-the-server]__10oa-v9._.js} +1 -1
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1i9rknc._.js → [root-of-the-server]__18d-9oi._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1is3glr._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +6 -6
  102. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0hdsupo._.js → [root-of-the-server]__0114ewg._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01od0m6._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +4 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1s2b6h0._.js → [root-of-the-server]__04o-kw7._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +4 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lpvwqm._.js +21 -0
  112. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ynf7tx._.js → [root-of-the-server]__0qucxyj._.js} +3 -3
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +4 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +8 -8
  116. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00cz9as._.js → [root-of-the-server]__1apb7k_._.js} +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +4 -0
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1i21qhv._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1py795m._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +4 -0
  123. package/.next/standalone/.next/server/chunks/ssr/_077y5np._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/app_audit_loading_tsx_1j1kc6j._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0gkzjaa.js → node_modules_html-to-image_es_index_0w82veb.js} +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  137. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  138. package/.next/standalone/.next/server/pages/404.html +1 -1
  139. package/.next/standalone/.next/server/pages/500.html +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  142. package/.next/standalone/.next/static/chunks/0hn30_j22wpp_.js +6 -0
  143. package/.next/standalone/.next/static/chunks/{3s8aaig91937v.js → 1_20f96f1253b.js} +1 -1
  144. package/.next/standalone/.next/static/chunks/1hdhteui932bq.js +1 -0
  145. package/.next/standalone/.next/static/chunks/{33u59vf_8xpd-.js → 1j2xm011_aoae.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/1qzsbft-jtnf8.js +1 -0
  147. package/.next/standalone/.next/static/chunks/{3-ogr79xqbw0h.js → 2mazwb1qmur1p.js} +1 -1
  148. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +2 -0
  149. package/.next/standalone/.next/static/chunks/2xbkqlma_-61k.js +1 -0
  150. package/.next/standalone/.next/static/chunks/{29o4j32z643_a.js → 2y7acsw0--8xf.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/3060a-0fayz96.js +1 -0
  152. package/.next/standalone/.next/static/chunks/312ihvhxeuoi_.js +1 -0
  153. package/.next/standalone/.next/static/chunks/31c084gsop1bb.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{1ua5armwfph8o.js → 31fmsp1soxi07.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{02jcotghcxrbu.js → 3kmwfieo15mr2.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{2uh_4k4m296z5.js → 3q9y67-fobgpc.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{turbopack-10hj8t5pf33tu.js → turbopack-1lsv92x7klwrn.js} +1 -1
  158. package/.next/standalone/app/actions/get-hooks-config.ts +85 -0
  159. package/.next/standalone/app/components/project-list.tsx +229 -42
  160. package/.next/standalone/app/policies/hooks-client.tsx +112 -14
  161. package/.next/standalone/app/project/[name]/page.tsx +12 -2
  162. package/.next/standalone/integration-suite/README.md +43 -1
  163. package/.next/standalone/integration-suite/ci-entrypoint.sh +21 -6
  164. package/.next/standalone/integration-suite/install-clis.sh +58 -13
  165. package/.next/standalone/integration-suite/probe-cli.sh +51 -11
  166. package/.next/standalone/integration-suite/report.js +52 -6
  167. package/.next/standalone/integration-suite/run.sh +55 -3
  168. package/.next/standalone/lib/hermes-profiles.ts +156 -0
  169. package/.next/standalone/lib/hermes-projects.ts +91 -25
  170. package/.next/standalone/lib/hermes-sessions.ts +65 -10
  171. package/.next/standalone/lib/openclaw-projects.ts +111 -18
  172. package/.next/standalone/lib/openclaw-sessions.ts +20 -0
  173. package/.next/standalone/lib/paths.ts +36 -0
  174. package/.next/standalone/lib/project-tree.ts +193 -0
  175. package/.next/standalone/lib/projects.ts +22 -0
  176. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  177. package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +3 -114
  178. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  179. package/.next/standalone/node_modules/next/dist/cli/next-test.js +2 -1
  180. package/.next/standalone/node_modules/next/dist/compiled/http-proxy/index.js +5 -5
  181. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +11 -10
  182. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -10
  183. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
  184. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +9 -9
  185. package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +1 -1
  186. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  187. package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
  188. package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +1 -1
  189. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
  190. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
  191. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +97 -56
  192. package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +129 -34
  193. package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +52 -4
  194. package/.next/standalone/node_modules/next/dist/server/config-schema.js +1 -1
  195. package/.next/standalone/node_modules/next/dist/server/config-shared.js +1 -2
  196. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  197. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  198. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  199. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +8 -27
  200. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  201. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +90 -53
  202. package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +15 -2
  203. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +4 -0
  204. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  205. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +1 -1
  206. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  207. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +2 -1
  208. package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +12 -0
  209. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  210. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  211. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  212. package/.next/standalone/node_modules/next/package.json +10 -10
  213. package/.next/standalone/package.json +7 -6
  214. package/.next/standalone/pi-extension/index.ts +38 -5
  215. package/.next/standalone/server.js +1 -1
  216. package/dist/cli.mjs +623 -308
  217. package/lib/hermes-profiles.ts +156 -0
  218. package/lib/hermes-projects.ts +91 -25
  219. package/lib/hermes-sessions.ts +65 -10
  220. package/lib/openclaw-projects.ts +111 -18
  221. package/lib/openclaw-sessions.ts +20 -0
  222. package/lib/paths.ts +36 -0
  223. package/lib/project-tree.ts +193 -0
  224. package/lib/projects.ts +22 -0
  225. package/package.json +7 -6
  226. package/pi-extension/index.ts +38 -5
  227. package/scripts/launch.ts +7 -0
  228. package/src/audit/cli-adapters/hermes.ts +6 -3
  229. package/src/audit/cli-adapters/openclaw.ts +17 -13
  230. package/src/hooks/configure-wizard.ts +24 -2
  231. package/src/hooks/custom-hooks-loader.ts +58 -4
  232. package/src/hooks/enforcement-capability.ts +266 -0
  233. package/src/hooks/handler.ts +12 -1
  234. package/src/hooks/hook-activity-store.ts +14 -0
  235. package/src/hooks/hooks-config.ts +59 -1
  236. package/src/hooks/integrations.ts +123 -19
  237. package/src/hooks/manager.ts +93 -27
  238. package/src/hooks/policy-evaluator.ts +59 -7
  239. package/src/hooks/policy-types.ts +26 -0
  240. package/src/hooks/types.ts +50 -2
  241. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +0 -3
  243. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +0 -4
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +0 -4
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +0 -4
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +0 -4
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zf8xlq._.js +0 -3
  253. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +0 -4
  254. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +0 -3
  255. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fax1sl._.js +0 -19
  256. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +0 -4
  257. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +0 -3
  260. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +0 -3
  261. package/.next/standalone/.next/static/chunks/06z_ex1ts3n4s.js +0 -1
  262. package/.next/standalone/.next/static/chunks/0o0ut0ugd0_-s.js +0 -1
  263. package/.next/standalone/.next/static/chunks/0xl97x-874k-u.js +0 -1
  264. package/.next/standalone/.next/static/chunks/19d7hqx8udopz.js +0 -1
  265. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +0 -2
  266. package/.next/standalone/.next/static/chunks/370sj2lgkw5ey.js +0 -1
  267. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +0 -1
  268. package/.next/standalone/.next/static/chunks/42oeva1-w0z2d.js +0 -6
  269. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_buildManifest.js +0 -0
  270. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_clientMiddlewareManifest.js +0 -0
  271. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_ssgManifest.js +0 -0
@@ -2,10 +2,22 @@
2
2
  # ─────────────────────────────────────────────────────────────────────────────
3
3
  # Tier-0 install probe — runs INSIDE the canary sandbox container.
4
4
  #
5
- # Installs/upgrades all 12 agent CLIs to @latest into the persistent HOME volume,
6
- # then records which resolve to a runnable binary + their version. This is both
7
- # the daily "fresh upgrade" step AND the Tier-0 canary signal (packaging/binary
8
- # breaks show up here as a FAIL).
5
+ # Installs/upgrades the agent CLIs into the persistent HOME volume, then records
6
+ # which resolve to a runnable binary + their version. This is both the daily
7
+ # "fresh upgrade" step AND the Tier-0 canary signal (packaging/binary breaks show
8
+ # up here as a FAIL).
9
+ #
10
+ # CANARY_CHANNEL selects WHICH ref of each CLI to install:
11
+ # stable (default) — what users get. All 12 CLIs.
12
+ # beta — the vendor's public pre-release ref, for early warning.
13
+ # Only the CLIs that actually publish one; the rest are
14
+ # SKIPPED, never silently re-installed at stable (that would
15
+ # be duplicate work reported as coverage).
16
+ #
17
+ # Every beta ref below was verified live against the vendor's registry/CDN on
18
+ # 2026-07-23; see ~/Desktop/failproofai-integration-suite-beta-channels-design.md
19
+ # for the evidence, lead times, and the CLIs that have no pre-release ref at all
20
+ # (factory, antigravity, pi, opencode, devin, hermes).
9
21
  #
10
22
  # Install methods verified 2026-07-16 (see failproofai-cli-canary-design.md).
11
23
  # Vendor installers scatter binaries across several dirs, so we search an
@@ -18,6 +30,7 @@ set -u
18
30
 
19
31
  RESULTS="${CANARY_RESULTS:-$HOME/canary-tier0.json}"
20
32
  LOGDIR="${CANARY_LOGDIR:-$HOME/canary-logs}"
33
+ CHANNEL="${CANARY_CHANNEL:-stable}"
21
34
  mkdir -p "$LOGDIR"
22
35
 
23
36
  # Cover every dir the vendor installers are known to drop binaries into.
@@ -25,8 +38,19 @@ export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$HOME/.factory/bin:$HOME/.he
25
38
 
26
39
  ROWS=()
27
40
 
41
+ # probe <id> <binary> <--version flag> <stable install cmd> [beta install cmd]
42
+ # Omitting the 5th argument declares "this CLI has no public pre-release ref",
43
+ # which skips it entirely on the beta channel.
28
44
  probe() {
29
- local id="$1" bin="$2" vflag="$3" install="$4"
45
+ local id="$1" bin="$2" vflag="$3" install="$4" beta="${5:-}"
46
+ if [ "$CHANNEL" != stable ]; then
47
+ if [ -z "$beta" ]; then
48
+ echo "════════════════════ $id ════════════════════"
49
+ echo " skipped — no public pre-release ref for this CLI"
50
+ return 0
51
+ fi
52
+ install="$beta"
53
+ fi
30
54
  echo "════════════════════ $id ════════════════════"
31
55
  local t0 t1 rc=1; t0=$SECONDS
32
56
  # Retry transient install failures. Flaky vendor CDNs / HTTP-2 stream resets are
@@ -64,19 +88,40 @@ probe() {
64
88
  "$id" "$status" "${resolved:-}" "${ver:-}" "$rc" "$((t1 - t0))")")
65
89
  }
66
90
 
67
- # id binary --ver flag install command (@latest)
68
- probe claude claude "--version" 'curl -fsSL https://claude.ai/install.sh | bash'
69
- probe codex codex "--version" 'npm install -g @openai/codex@latest'
70
- probe copilot copilot "--version" 'npm install -g @github/copilot@latest'
71
- probe cursor cursor-agent "--version" 'curl https://cursor.com/install -fsS | bash'
91
+ # id binary --ver flag stable ref beta ref (omitted = none exists)
92
+ #
93
+ # claude is the one CLI whose channels run BACKWARDS: nothing ships ahead of
94
+ # `latest`, which is the bleeding edge (~1 release/day). What exists is `stable`,
95
+ # ~13 days behind. So the stable leg pins `stable` — both to test what
96
+ # conservative users actually run and to stop a same-day Anthropic release
97
+ # red-lighting an unrelated PR — and `latest` becomes the early-warning ref.
98
+ probe claude claude "--version" 'curl -fsSL https://claude.ai/install.sh | bash -s stable' \
99
+ 'curl -fsSL https://claude.ai/install.sh | bash'
100
+ probe codex codex "--version" 'npm install -g @openai/codex@latest' \
101
+ 'npm install -g @openai/codex@alpha'
102
+ probe copilot copilot "--version" 'npm install -g @github/copilot@latest' \
103
+ 'npm install -g @github/copilot@prerelease'
104
+ # Cursor validates the channel server-side: only prod/lab/static/prod-stable-internal
105
+ # return 200, anything else is a 400. `lab` is Anysphere's own dogfood channel.
106
+ probe cursor cursor-agent "--version" 'curl https://cursor.com/install -fsS | bash' \
107
+ 'curl "https://cursor.com/install?channel=lab" -fsS | bash'
108
+ # opencode's beta/dev tags are ~31 branch snapshots a day (0.0.0-<branch>-<ts>),
109
+ # not release candidates — deliberately no beta ref.
72
110
  probe opencode opencode "--version" 'npm install -g opencode-ai@latest'
73
111
  probe pi pi "--version" 'npm install -g @mariozechner/pi-coding-agent@latest'
112
+ # hermes install.sh git-clones main, which is ALSO what every hermes user gets —
113
+ # its tags are markers nobody installs. So there is nothing ahead of us to probe,
114
+ # and pinning a tag would test something no user runs.
74
115
  probe hermes hermes "--version" 'curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash'
75
- probe openclaw openclaw "--version" 'npm install -g openclaw@latest'
116
+ probe openclaw openclaw "--version" 'npm install -g openclaw@latest' \
117
+ 'npm install -g openclaw@beta'
76
118
  probe factory droid "--version" 'curl -fsSL https://app.factory.ai/cli | sh'
77
119
  probe devin devin "--version" 'curl -fsSL https://cli.devin.ai/install.sh | bash'
78
120
  probe antigravity agy "--version" 'curl -fsSL https://antigravity.google/cli/install.sh | bash'
79
- probe goose goose "--version" 'curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash'
121
+ # goose's canary is a ROLLING TAG (prerelease:false), selected by env var — not a
122
+ # GitHub prerelease, so channel detection that scans for prerelease:true misses it.
123
+ probe goose goose "--version" 'curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash' \
124
+ 'curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CANARY=true CONFIGURE=false bash'
80
125
 
81
126
  # Emit JSON array.
82
127
  { printf '['; IFS=,; printf '%s' "${ROWS[*]}"; printf ']\n'; } > "$RESULTS"
@@ -87,5 +132,5 @@ ok=0; fail=0
87
132
  for r in "${ROWS[@]}"; do
88
133
  case "$r" in *'"status":"OK"'*) ok=$((ok+1));; *) fail=$((fail+1));; esac
89
134
  done
90
- printf 'installed OK: %s / 12 failed: %s\n' "$ok" "$fail"
135
+ printf 'channel: %s installed OK: %s / %s failed: %s\n' "$CHANNEL" "$ok" "$((ok + fail))" "$fail"
91
136
  echo "results JSON: $RESULTS"
@@ -14,14 +14,33 @@
14
14
  set -u
15
15
  CLI="${1:?usage: probe-cli.sh <cli>}"
16
16
  : "${CANARY_LLM_API_KEY:?gateway key missing}"
17
- # Gateway default model. deepseek-v4-pro is cheapest AND works on both the OpenAI
18
- # chat-completions and Responses paths. EXCEPTION: the claude CLI speaks Anthropic
19
- # tool-use, which deepseek-via-/v1/messages doesn't emit correctly (all probes go
20
- # INCONCLUSIVE), so claude pins to the cheapest Anthropic model via CANARY_CLAUDE_MODEL.
17
+ # Gateway default model, used by every CLI without a pin of its own. deepseek-v4-pro is
18
+ # cheapest AND works on both the OpenAI chat-completions and Responses paths. Three CLIs
19
+ # are pinned away from it below (claude, pi, codex) — each for a payload deepseek refuses.
20
+ # EXCEPTION 1: the claude CLI speaks Anthropic tool-use, which deepseek-via-/v1/messages
21
+ # doesn't emit correctly (all probes go INCONCLUSIVE), so claude pins to the cheapest
22
+ # Anthropic model via CANARY_CLAUDE_MODEL.
21
23
  : "${CANARY_LLM_MODEL:=deepseek-v4-pro}"
22
24
  : "${CANARY_CLAUDE_MODEL:=claude-haiku-4-5}"
23
- # pi sends an `include` (encrypted reasoning) param deepseek rejects (400) → pin to Anthropic.
25
+ # EXCEPTION 2: pi sends an `include` (encrypted reasoning) param deepseek rejects (400).
24
26
  : "${CANARY_PI_MODEL:=claude-haiku-4-5}"
27
+ # EXCEPTION 3: codex ≥0.145.0 hits the SAME rejection. For a model it has no metadata for
28
+ # (deepseek logs "Model metadata not found. Defaulting to fallback metadata") it now sends
29
+ # `reasoning:{summary:"auto"}` + `include:["reasoning.encrypted_content"]`, where 0.144.6
30
+ # sent `reasoning:null` + `include:[]`. The gateway answers 400 "Encrypted content is not
31
+ # supported with this model" (param: include), codex exits before its first tool call, and
32
+ # BOTH probes report INCONCLUSIVE — enforcement is fine, there is just nothing to observe.
33
+ # No config override strips it (`model_reasoning_summary=none`,
34
+ # `model_supports_reasoning_summaries=false`, `model_reasoning_effort=none` all still emit
35
+ # `include`), and the old escape hatch is gone — `wire_api="chat"` is rejected outright in
36
+ # 0.145.0 (openai/codex#7782). So codex needs a model that accepts encrypted reasoning
37
+ # content. gpt-5.1-codex-mini is the cheapest that does AND supports codex's full toolset:
38
+ # gpt-5.4-nano accepts the reasoning params but 400s on `tool_search`.
39
+ # Do NOT "fix" this by pinning codex to a Claude model the way pi and claude are: the
40
+ # gateway routes Anthropic weighted 1:1 through Bedrock, which 400s on codex's request
41
+ # metadata (#576). That fails on roughly half of requests — a coin-flip red is worse in a
42
+ # daily canary than a consistent one, and a single green probe does not disprove it.
43
+ : "${CANARY_CODEX_MODEL:=gpt-5.1-codex-mini}"
25
44
  # Gateway base URL — overridable via env (CI supplies it as a secret). Strip any
26
45
  # trailing slash so the `$GW/v1` joins below never produce `//v1`.
27
46
  GW="${CANARY_LLM_BASE_URL:-https://models.aikin.club}"; GW="${GW%/}"
@@ -129,7 +148,7 @@ drive() { # $1 = prompt ; run ONE prompt headless, executing tools without appro
129
148
  codex) ( cd "$BASE" && codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust \
130
149
  -c model_providers.gw.name="gw" -c model_providers.gw.base_url="$GW/v1" -c model_providers.gw.wire_api="responses" \
131
150
  -c model_providers.gw.env_key="CANARY_LLM_API_KEY" -c model_provider="gw" \
132
- -c model="${CANARY_CODEX_MODEL:-deepseek-v4-pro}" "$1" 2>&1 ) ;; # Claude models 400 on Bedrock metadata; deepseek is non-Bedrock
151
+ -c model="$CANARY_CODEX_MODEL" "$1" 2>&1 ) ;;
133
152
  cursor) ( cd "$BASE" && cursor-agent -p --force "$1" 2>&1 ) ;;
134
153
  copilot) ( cd "$BASE" && copilot -p "$1" --allow-all-tools 2>&1 ) ;;
135
154
  devin) ( cd "$BASE" && devin -p "$1" --permission-mode dangerous --respect-workspace-trust false 2>&1 ) ;;
@@ -163,7 +182,21 @@ read_denied() { grep -qE "result=deny policy=(failproofai/|custom/)?(canary-read
163
182
  # Vendor quota / auth errors (Copilot-Free credits, antigravity Google quota,
164
183
  # expired logins) → the CLI errors before any tool call. Report these DISTINCTLY
165
184
  # (not as plain INCONCLUSIVE) so "can't test right now" ≠ "model just didn't try".
166
- is_error() { printf '%s' "$1" | grep -qiE "quota|rate.?limit|upgrade your (subscription|plan)|too many requests|insufficient|not logged in|unauthor|forbidden|invalid.*(key|token|credential)|payment required|\\b(401|402|429)\\b"; }
185
+ # Payload rejections belong in the same bucket: when a CLI update starts sending a param
186
+ # the pinned model refuses, the run is untestable, NOT a model that declined to act.
187
+ # Leaving those as INCONCLUSIVE is how codex 0.145.0's `include:
188
+ # ["reasoning.encrypted_content"]` 400 read as a quiet 🟡 for a full day instead of the
189
+ # ⚠️ it was.
190
+ #
191
+ # These patterns MUST stay machine-shaped. `$1` is the agent's whole transcript, so a bare
192
+ # `400` or `not supported` also matches ordinary prose ("400 tests passed", "that flag is
193
+ # not supported") and would report a chatty refusal as a vendor outage — the exact
194
+ # inversion this function exists to prevent. Hence: the structured `"code": 400` form
195
+ # rather than a loose `400`, and `not supported with` (the gateway's own phrasing:
196
+ # "not supported with this model") rather than `not supported`. Both live failures we have
197
+ # seen carry `invalid_request_error` AND `BadRequestError` anyway, so the tight forms lose
198
+ # nothing. __tests__/integration-suite/is-error.test.ts holds the fixtures both ways.
199
+ is_error() { printf '%s' "$1" | grep -qiE "quota|rate.?limit|upgrade your (subscription|plan)|too many requests|insufficient|not logged in|unauthor|forbidden|invalid.*(key|token|credential)|payment required|\\b(401|402|429)\\b|\"code\"[[:space:]]*:[[:space:]]*\"?40[0-9]|bad.?request(error)?\\b|invalid_request_error|not supported with|unsupported (parameter|model|value)|deploymentnotfound"; }
167
200
 
168
201
  ATTEMPTS=3 # retry up to N times to absorb LLM nondeterminism (flaky tool-callers)
169
202
 
@@ -180,8 +213,13 @@ for _ in $(seq 1 $ATTEMPTS); do
180
213
  denied canary-bash "$LOGA/hooks.log" && break
181
214
  [ -f "$BASE/CANARY_PROBE_ran" ] && break
182
215
  done
183
- if denied canary-bash "$LOGA/hooks.log"; then VA=PASS
184
- elif [ -f "$BASE/CANARY_PROBE_ran" ]; then VA=FAIL
216
+ # Ground truth FIRST. A CLI that logs our deny and runs the command anyway is
217
+ # the silent-allow this suite exists to catch (copilot 1.0.70 did exactly that)
218
+ # — and checking our own hooks.log first scored it PASS, because our log says
219
+ # "denied" whether or not the CLI honoured it. The marker file is the only
220
+ # evidence of what the CLI actually did, so it decides.
221
+ if [ -f "$BASE/CANARY_PROBE_ran" ]; then VA=FAIL
222
+ elif denied canary-bash "$LOGA/hooks.log"; then VA=PASS
185
223
  elif is_error "$OUTA"; then VA=ERROR
186
224
  else VA=INCONCLUSIVE; fi
187
225
 
@@ -195,8 +233,10 @@ for _ in $(seq 1 $ATTEMPTS); do
195
233
  read_denied "$LOGB/hooks.log" && break
196
234
  printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT" && break
197
235
  done
198
- if read_denied "$LOGB/hooks.log"; then VB=PASS
199
- elif printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT"; then VB=FAIL
236
+ # Same ordering rule as probe A: the sentinel leaking into the transcript proves
237
+ # the read happened, which outranks our own log claiming we denied it.
238
+ if printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT"; then VB=FAIL
239
+ elif read_denied "$LOGB/hooks.log"; then VB=PASS
200
240
  elif is_error "$OUTB"; then VB=ERROR
201
241
  else VB=INCONCLUSIVE; fi
202
242
 
@@ -1,15 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  // Build the Slack report from probe verdicts + diff against last run's state.
3
- // Args: <resultsJson> <statePath> <model>
3
+ // Args: <resultsJson> <statePath> <model> [channel] [peerStatePath]
4
4
  // Each result: {cli, probes:{...}, version?, gated?}. Prints report to stdout
5
5
  // AND writes new state: clis[cli] = {probes, version}. Flags broke/recovered.
6
+ //
7
+ // On the BETA channel the report answers a different question — not "is
8
+ // enforcement broken?" but "is it about to break?" — so it compares each CLI
9
+ // against the sibling stable leg's last known state (peerStatePath).
6
10
  const fs = require("fs");
7
- const [, , resultsJson, statePath, model] = process.argv;
11
+ const [, , resultsJson, statePath, model, channel = "stable", peerStatePath = "", eligible = ""] = process.argv;
8
12
  const results = JSON.parse(resultsJson);
13
+ const isBeta = channel !== "stable";
9
14
 
10
15
  let prev = { clis: {}, lastRun: null };
11
16
  try { prev = JSON.parse(fs.readFileSync(statePath, "utf8")); } catch {}
12
17
 
18
+ let peer = { clis: {} };
19
+ if (peerStatePath) { try { peer = JSON.parse(fs.readFileSync(peerStatePath, "utf8")); } catch {} }
20
+
13
21
  const probesOf = (entry) => (entry ? (entry.probes || entry) : null); // compat old flat schema
14
22
  const statusOf = (probes) => {
15
23
  const v = Object.values(probes || {});
@@ -23,16 +31,41 @@ const emo = { green: "🟢", yellow: "🟡", red: "🔴", grey: "⚪", error: "
23
31
  const rank = { green: 0, grey: 1, yellow: 1, error: 1, red: 2 };
24
32
 
25
33
  const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
26
- const lines = [`🧪 *failproofai CLI integration tests* · ${now} · model=${model}`];
34
+ const title = isBeta
35
+ ? `🔮 *failproofai CLI pre-release watch* · ${now} · channel=${channel} · model=${model}`
36
+ : `🧪 *failproofai CLI integration tests* · ${now} · model=${model}`;
37
+ const lines = [title];
27
38
  let worst = "green";
28
- const breaks = [], recoveries = [];
39
+ const breaks = [], recoveries = [], incoming = [];
40
+
41
+ // A beta CLI counts as "incoming breakage" only when the SAME CLI is green on the
42
+ // stable leg — otherwise we would report an already-broken CLI twice — and only
43
+ // after two consecutive non-green probes, so a broken alpha that gets reverted
44
+ // before release doesn't burn anyone's attention. Strike counts live in state.
45
+ const strikesOf = (cli) => (prev.clis && prev.clis[cli] && prev.clis[cli].strikes) || 0;
46
+ const newStrikes = {};
29
47
 
30
48
  for (const r of results) {
31
49
  const st = statusOf(r.probes);
32
50
  const prevEntry = prev.clis && prev.clis[r.cli];
33
51
  const prevSt = prevEntry ? statusOf(probesOf(prevEntry)) : null;
34
52
  let mark = "";
35
- if (r.gated) {
53
+ if (isBeta) {
54
+ const peerEntry = peer.clis && peer.clis[r.cli];
55
+ const peerSt = peerEntry ? statusOf(probesOf(peerEntry)) : null;
56
+ const strikes = st === "green" ? 0 : strikesOf(r.cli) + 1;
57
+ newStrikes[r.cli] = strikes;
58
+ if (r.gated) {
59
+ mark = ` 🔵 _gated: pre-release unchanged, last green_`;
60
+ } else if (st !== "green" && peerSt === "green") {
61
+ mark = strikes >= 2
62
+ ? ` 🚨 *INCOMING* — green on stable, ${st === "red" ? "FAILING" : "not verifiable"} on ${channel} (${strikes} runs)`
63
+ : ` 👀 _first ${channel} miss; confirming next run_`;
64
+ if (strikes >= 2) incoming.push(r.cli);
65
+ } else if (st !== "green" && peerSt && peerSt !== "green") {
66
+ mark = ` ⏸️ _also ${peerSt} on stable — the stable leg owns this alarm_`;
67
+ }
68
+ } else if (r.gated) {
36
69
  mark = ` 🔵 _gated: CLI v${r.version || "?"} + failproofai ${r.fpSha || "?"} unchanged, last green_`;
37
70
  } else if (prevSt && prevSt !== "red" && st === "red") {
38
71
  mark = " ⚠️ *BROKE*"; breaks.push(r.cli);
@@ -48,7 +81,19 @@ for (const r of results) {
48
81
 
49
82
  const errored = results.filter((r) => statusOf(r.probes) === "error").map((r) => r.cli);
50
83
  let hdr;
51
- if (breaks.length) hdr = `🔴 *ENFORCEMENT BROKEN*: ${breaks.join(", ")} — investigate now`;
84
+ if (isBeta) {
85
+ // Coverage is stated explicitly and always: only some vendors publish a
86
+ // pre-release ref, and "beta: all green" must never read as assurance for the
87
+ // CLIs that were never probed at all. The denominator is how many are ELIGIBLE
88
+ // (passed in by run.sh), not 12 — a targeted run must not imply the CLIs it
89
+ // skipped have no pre-release ref, and hardcoding the eligible list here would
90
+ // be a third copy to drift against.
91
+ const total = Number(eligible) || results.length;
92
+ const cov = `_watching ${results.length}/${total} CLIs that publish a pre-release ref (of 12 total)_`;
93
+ hdr = incoming.length
94
+ ? `🚨 *INCOMING BREAKAGE*: ${incoming.join(", ")} — broken on ${channel}, still green on stable. Fix before it ships.\n${cov}`
95
+ : `🔮 no incoming breakage detected on ${channel}\n${cov}`;
96
+ } else if (breaks.length) hdr = `🔴 *ENFORCEMENT BROKEN*: ${breaks.join(", ")} — investigate now`;
52
97
  // Any CLI still red dominates the header — a recovery elsewhere must not mask it.
53
98
  else if (worst === "red") hdr = "🔴 still broken (unchanged)";
54
99
  else if (recoveries.length) hdr = `✅ recovered: ${recoveries.join(", ")}`;
@@ -66,6 +111,7 @@ for (const r of results) {
66
111
  probes: r.probes,
67
112
  version: r.version || (prevEntry && prevEntry.version) || null,
68
113
  fpSha: r.fpSha || (prevEntry && prevEntry.fpSha) || null,
114
+ ...(isBeta ? { strikes: newStrikes[r.cli] || 0 } : {}),
69
115
  };
70
116
  }
71
117
  try { fs.writeFileSync(statePath, JSON.stringify(newState, null, 2)); }
@@ -30,7 +30,29 @@ ENVFILE="${CANARY_ENVFILE:?CANARY_ENVFILE (docker --env-file with gateway creds)
30
30
  MODEL="${CANARY_LLM_MODEL:-deepseek-v4-pro}"
31
31
  GATED="${CANARY_VERSION_GATED-all}"
32
32
 
33
- CLIS=("$@"); [ ${#CLIS[@]} -eq 0 ] && CLIS=(claude codex copilot cursor factory devin antigravity goose opencode pi hermes openclaw)
33
+ CHANNEL="${CANARY_CHANNEL:-stable}"
34
+ # Sibling leg's state file, for the cross-leg comparison (see report.js). The beta
35
+ # leg needs to know whether a CLI is green on stable: "stable green + beta not
36
+ # green" is the incoming-breakage signal, and it is the ONLY way to distinguish
37
+ # "the vendor is about to break us" from "this CLI is broken for everyone already".
38
+ PEER_STATE="${CANARY_PEER_STATE:-}"
39
+
40
+ # The CLIs with a public pre-release ref — kept in sync with the beta refs in
41
+ # install-clis.sh (__tests__/integration-suite/channel-refs.test.ts asserts the two
42
+ # lists agree, since a silent drift here would look like coverage while probing
43
+ # nothing). Its LENGTH is also the honest denominator for the beta report: a
44
+ # targeted run (`run.sh cursor`) must not imply the CLIs it skipped have no
45
+ # pre-release ref.
46
+ BETA_CLIS=(claude codex copilot cursor goose openclaw)
47
+
48
+ CLIS=("$@")
49
+ if [ ${#CLIS[@]} -eq 0 ]; then
50
+ if [ "$CHANNEL" = stable ]; then
51
+ CLIS=(claude codex copilot cursor factory devin antigravity goose opencode pi hermes openclaw)
52
+ else
53
+ CLIS=("${BETA_CLIS[@]}")
54
+ fi
55
+ fi
34
56
 
35
57
  # failproofai main HEAD — the second gate dimension. Prefer the value the workflow
36
58
  # computed; fall back to git in the checkout.
@@ -72,14 +94,33 @@ for cli in "${CLIS[@]}"; do
72
94
  vj="$(node -e 'const st=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const p=st.clis[process.argv[2]];process.stdout.write(JSON.stringify({cli:process.argv[2],probes:p.probes||p,version:p.version,fpSha:p.fpSha,gated:true}))' "$STATE" "$cli")"
73
95
  else
74
96
  echo ">> probing $cli ..." >&2
75
- vj="$(run_probe "$cli" | sed -n 's/^VERDICT_JSON //p' | tail -1)"
97
+ # Spool to a file rather than a shell variable: a probe's output is a full agent
98
+ # transcript from up to 6 CLI invocations, and a stuck or noisy client could make it
99
+ # arbitrarily large. Only the verdict line and a 20-line tail are ever needed.
100
+ out_file="$(mktemp)"
101
+ run_probe "$cli" > "$out_file"
102
+ vj="$(sed -n 's/^VERDICT_JSON //p' "$out_file" | tail -1)"
103
+ # Echo the probe tail whenever the verdict is not a clean pass. Without this the
104
+ # ONLY thing that survived a probe was its VERDICT_JSON line, so a yellow/red run
105
+ # said WHAT broke and never WHY — and re-running told you no more, because the
106
+ # cause (a vendor error message) was discarded both times. Diagnosing codex's
107
+ # 0.145.0 regression needed a full local reproduction purely for want of these
108
+ # lines. Safe in a public log: every credential here is a registered Actions
109
+ # secret, so GitHub masks it on the way out.
110
+ case "$vj" in
111
+ *FAIL*|*INCONCLUSIVE*|*ERROR*|"")
112
+ echo "── $cli probe output (tail) ──" >&2
113
+ tail -20 "$out_file" >&2
114
+ echo "── end $cli ──" >&2 ;;
115
+ esac
116
+ rm -f "$out_file"
76
117
  [ -z "$vj" ] && vj="{\"cli\":\"$cli\",\"probes\":{}}"
77
118
  vj="$(node -e 'const v=JSON.parse(process.argv[1]);v.version=process.argv[2]||null;v.fpSha=process.argv[3]||null;process.stdout.write(JSON.stringify(v))' "$vj" "$cur_ver" "$FP_SHA")"
78
119
  fi
79
120
  results="$(node -e 'const a=JSON.parse(process.argv[1]);a.push(JSON.parse(process.argv[2]));process.stdout.write(JSON.stringify(a))' "$results" "$vj")"
80
121
  done
81
122
 
82
- report="$(node "$HERE/report.js" "$results" "$STATE" "$MODEL")"
123
+ report="$(node "$HERE/report.js" "$results" "$STATE" "$MODEL" "$CHANNEL" "$PEER_STATE" "${#BETA_CLIS[@]}")"
83
124
  echo "════ report ════" >&2; printf '%s\n' "$report" >&2; echo "════════════════" >&2
84
125
  printf '%s\n' "$report" # also to stdout for the workflow log / artifact
85
126
 
@@ -94,6 +135,17 @@ else
94
135
  echo "(no CANARY_SLACK_WEBHOOK set — report not posted)" >&2
95
136
  fi
96
137
 
138
+ # The beta leg is ADVISORY and must never fail the job. It probes vendor
139
+ # pre-release builds, which are broken-by-nature often enough that gating CI on
140
+ # them would train everyone to ignore a red run — and the thing it reports is
141
+ # "this WILL break on release", not "this IS broken", which is not a reason to
142
+ # stop the world. The report is emitted and posted either way, so the signal is
143
+ # never lost; escalation is the report's job, not the exit code's.
144
+ if [ "$CHANNEL" != stable ]; then
145
+ echo "(channel=$CHANNEL — advisory only, not failing the job)" >&2
146
+ exit 0
147
+ fi
148
+
97
149
  # Fail the job when any probe reported a hard FAIL (broken enforcement) so this
98
150
  # scheduled check actually blocks regressions. ERROR (vendor quota/auth) and
99
151
  # INCONCLUSIVE (model didn't attempt the tool) are NOT failures — they mean
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Hermes profile discovery — shared by BOTH pillars.
3
+ *
4
+ * A Hermes "profile" is not a column or a flag: it is a whole separate Hermes
5
+ * home directory, each with its own `config.yaml`, `.env`, `SOUL.md`, and
6
+ * `state.db`. The default profile lives at `~/.hermes`; every other profile at
7
+ * `~/.hermes/profiles/<name>/`. Selection is `hermes -p <name>`, a generated
8
+ * `~/.local/bin/<name>` alias that exports `HERMES_HOME`, or a sticky default
9
+ * recorded in `<root>/active_profile`.
10
+ *
11
+ * Upstream's own contributor guide warns that hardcoding `~/.hermes` breaks
12
+ * profiles — which is exactly what both pillars used to do:
13
+ * • audit → read one state.db, so non-default profiles were INVISIBLE;
14
+ * • enforcement→ wrote one config.yaml, so non-default profiles ran UNHOOKED.
15
+ * Both now enumerate through here.
16
+ *
17
+ * Kept deliberately dependency-light (node:fs / node:os / node:path only) so
18
+ * `src/hooks/integrations.ts` can import it without pulling the sql.js reader
19
+ * into the hook hot path.
20
+ *
21
+ * Home override: set `HERMES_HOME` (Hermes's own env var — respected here so a
22
+ * profile-scoped shell and failproofai agree on what "all profiles" means).
23
+ */
24
+ import { readdirSync } from "node:fs";
25
+ import { homedir } from "node:os";
26
+ import { basename, dirname, join, resolve } from "node:path";
27
+
28
+ /** Directory under the Hermes root that holds non-default profiles. */
29
+ const PROFILES_DIR = "profiles";
30
+
31
+ /** Name we give the root home (`~/.hermes`), which Hermes itself leaves unnamed. */
32
+ export const HERMES_DEFAULT_PROFILE = "default";
33
+
34
+ export interface HermesProfile {
35
+ /** Profile name — `"default"` for the root home, else the directory name. */
36
+ name: string;
37
+ /** Absolute HERMES_HOME for this profile (holds config.yaml + state.db). */
38
+ home: string;
39
+ }
40
+
41
+ /**
42
+ * The Hermes ROOT home — the directory that owns `profiles/`.
43
+ *
44
+ * `HERMES_HOME` may point AT a profile (`<root>/profiles/<name>`), because
45
+ * that's what the per-profile alias wrapper exports. We climb back to `<root>`
46
+ * in that case so discovery still sees every sibling profile — mirroring what
47
+ * upstream does so `profile list` can see them all.
48
+ */
49
+ export function hermesRoot(): string {
50
+ const env = (process.env.HERMES_HOME || "").trim();
51
+ if (env) {
52
+ const home = resolve(env);
53
+ const parent = dirname(home);
54
+ if (basename(parent) === PROFILES_DIR) return dirname(parent);
55
+ return home;
56
+ }
57
+ return join(homedir(), ".hermes");
58
+ }
59
+
60
+ /**
61
+ * Every Hermes profile on disk: the root home first (as `"default"`), then each
62
+ * `<root>/profiles/<name>/` in name order.
63
+ *
64
+ * Fail-open — a missing or unreadable `profiles/` dir just means "default only",
65
+ * which is the single-profile install everyone starts with.
66
+ *
67
+ * A profile directory literally named `default` would collide with the root's
68
+ * reserved name; the root wins and the directory is skipped (dedup by name keeps
69
+ * grouping keys unique and the outcome deterministic).
70
+ */
71
+ export function listHermesProfiles(): HermesProfile[] {
72
+ const root = hermesRoot();
73
+ const out: HermesProfile[] = [{ name: HERMES_DEFAULT_PROFILE, home: root }];
74
+ const seen = new Set<string>([HERMES_DEFAULT_PROFILE]);
75
+
76
+ let names: string[];
77
+ try {
78
+ names = readdirSync(join(root, PROFILES_DIR), { withFileTypes: true })
79
+ // Symlinked profile dirs are legitimate, and `isDirectory()` is false for them.
80
+ .filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."))
81
+ .map((e) => e.name)
82
+ .sort();
83
+ } catch {
84
+ return out;
85
+ }
86
+
87
+ for (const name of names) {
88
+ if (seen.has(name)) continue;
89
+ seen.add(name);
90
+ out.push({ name, home: join(root, PROFILES_DIR, name) });
91
+ }
92
+ return out;
93
+ }
94
+
95
+ // ── (profile, source) naming ────────────────────────────────────────────────
96
+ //
97
+ // Gateway sessions have no cwd to group by, so the dashboard and the audit
98
+ // synthesize a project per (profile, source). Lives here — next to the profile
99
+ // list the parser needs — so lib/hermes-sessions.ts and lib/hermes-projects.ts
100
+ // can both use it without importing each other.
101
+
102
+ /**
103
+ * Encoded project name for a (profile, source) pair.
104
+ *
105
+ * `decodeFolderName()` renders every `-` as `/`, so `hermes-work-slack` shows up
106
+ * as `hermes/work/slack` in the projects panel with no UI change.
107
+ */
108
+ export function hermesProjectName(profile: string, source: string): string {
109
+ return `hermes-${profile}-${source}`;
110
+ }
111
+
112
+ /** Machine-readable grouping key, shown under the project name. */
113
+ export function hermesProjectPath(profile: string, source: string): string {
114
+ return `hermes:${profile}:${source}`;
115
+ }
116
+
117
+ export interface HermesNameSplit {
118
+ profile: string;
119
+ source: string;
120
+ }
121
+
122
+ /**
123
+ * Every way `hermes-<profile>-<source>` could split, best guess first.
124
+ *
125
+ * Profile names may contain `-`, so the slug is never split blindly: each
126
+ * candidate is a profile that actually exists on disk, tried longest-first.
127
+ * Length alone is not decisive though — with profiles `work` and `work-slack`,
128
+ * `hermes-work-slack-dev` could be `work-slack`+`dev` or `work`+`slack-dev` —
129
+ * so callers walk the candidates and take the first that owns real sessions.
130
+ *
131
+ * The last candidate is always the legacy `hermes-<source>` form attributed to
132
+ * the default profile, keeping links made before profile support alive.
133
+ */
134
+ export function hermesProjectNameCandidates(name: string): HermesNameSplit[] {
135
+ if (!name.startsWith("hermes-")) return [];
136
+ const rest = name.slice("hermes-".length);
137
+ if (!rest) return [];
138
+
139
+ const out: HermesNameSplit[] = [];
140
+ const profiles = listHermesProfiles()
141
+ .map((p) => p.name)
142
+ .sort((a, b) => b.length - a.length);
143
+ for (const profile of profiles) {
144
+ const prefix = `${profile}-`;
145
+ if (rest.startsWith(prefix) && rest.length > prefix.length) {
146
+ out.push({ profile, source: rest.slice(prefix.length) });
147
+ }
148
+ }
149
+ out.push({ profile: HERMES_DEFAULT_PROFILE, source: rest });
150
+ return out;
151
+ }
152
+
153
+ /** The best-guess split for a project name — `null` if it isn't a Hermes one. */
154
+ export function parseHermesProjectName(name: string): HermesNameSplit | null {
155
+ return hermesProjectNameCandidates(name)[0] ?? null;
156
+ }