failproofai 1.0.6-beta.1 → 1.0.7-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 (258) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +7 -5
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +13 -11
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +41 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -9
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__1j-zsg5._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_r2rbg._.js → [root-of-the-server]__1bf34x4._.js} +5 -7
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +5 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0576iuy._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +5 -0
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1x3j9mj._.js → [root-of-the-server]__1p28m0u._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/{_0qlygw6._.js → _0cf1olg._.js} +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{_1_ddpm3._.js → _135-c3y._.js} +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +5 -0
  110. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  111. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _1w_5l7t._.js} +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +4 -4
  115. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  119. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_0az0sfq._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  122. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  123. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +5 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +12 -0
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +3 -0
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  132. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  133. package/.next/standalone/.next/server/pages/404.html +1 -1
  134. package/.next/standalone/.next/server/pages/500.html +1 -1
  135. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  136. package/.next/standalone/.next/server/server-reference-manifest.json +56 -23
  137. package/.next/standalone/.next/static/chunks/{06fjx8-0_qufs.js → 00i9thnpht4l1.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{3ht0xsco46pul.js → 0289oapk24zql.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/{1qf2uiipatr80.js → 0pyy9m5cm8zku.js} +1 -1
  140. package/.next/standalone/.next/static/chunks/15heikmy3yjzc.js +1 -0
  141. package/.next/standalone/.next/static/chunks/285spx855h_3r.css +2 -0
  142. package/.next/standalone/.next/static/chunks/{02vwr5js1x28j.js → 2plg9qae1yejv.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/{2x776s0x3n_tu.js → 2tqszh84mp_2l.js} +1 -1
  145. package/.next/standalone/.next/static/chunks/34jpdb2xjm-x7.js +1 -0
  146. package/.next/standalone/.next/static/chunks/3cdkxrukvlvul.js +1 -0
  147. package/.next/standalone/.next/static/chunks/3dnd-_29uiaj3.js +6 -0
  148. package/.next/standalone/.next/static/chunks/{0y227_ohdqwvn.js → 3vp7c3knkh8mr.js} +1 -1
  149. package/.next/standalone/.next/static/chunks/{27_bwbg8sv6vj.js → 3vwnzfx9q80yl.js} +1 -1
  150. package/.next/standalone/app/actions/get-jev-config.ts +409 -0
  151. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  152. package/.next/standalone/app/actions/update-jev-config.ts +420 -0
  153. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  154. package/.next/standalone/app/policies/hooks-client.tsx +9 -0
  155. package/.next/standalone/app/settings/jev-panel.tsx +469 -0
  156. package/.next/standalone/app/settings/page.tsx +20 -1
  157. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  158. package/.next/standalone/app/settings/settings.css +79 -0
  159. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +51 -3
  160. package/.next/standalone/fp-cloud-cli/README.md +1 -1
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
  162. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +44 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +226 -5
  164. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +11 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +68 -1
  166. package/.next/standalone/fp-cloud-cli/pyproject.toml +1 -1
  167. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +17 -2
  168. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +7 -4
  169. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +184 -0
  170. package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
  171. package/.next/standalone/package.json +11 -11
  172. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  173. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  174. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +2 -0
  175. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +24 -1
  176. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +41 -6
  177. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +88 -0
  178. package/.next/standalone/sdk/python/uv.lock +15 -15
  179. package/.next/standalone/server.js +1 -1
  180. package/bin/failproofai.mjs +115 -2
  181. package/dist/cli.mjs +10982 -3787
  182. package/dist/index.js +19 -1
  183. package/dist/worker.mjs +13761 -7834
  184. package/package.json +11 -11
  185. package/pi-extension/index.ts +11 -0
  186. package/scripts/build-policy-pack.mjs +39 -2
  187. package/src/hooks/builtin-policies.ts +21 -3
  188. package/src/hooks/cloud-enrollment-cli.ts +1 -1
  189. package/src/hooks/cloud-managed-policies.ts +22 -0
  190. package/src/hooks/custom-hooks-loader.ts +45 -7
  191. package/src/hooks/custom-hooks-registry.ts +45 -1
  192. package/src/hooks/daemon-client.ts +128 -50
  193. package/src/hooks/daemon-service.ts +30 -0
  194. package/src/hooks/effective-reviewers.ts +79 -0
  195. package/src/hooks/first-run-gate.ts +5 -0
  196. package/src/hooks/fp-home.ts +23 -0
  197. package/src/hooks/handler.ts +265 -6
  198. package/src/hooks/hook-activity-store.ts +105 -1
  199. package/src/hooks/hook-telemetry.ts +41 -0
  200. package/src/hooks/integrations.ts +3 -2
  201. package/src/hooks/jev-activity.ts +385 -0
  202. package/src/hooks/jev-cli.ts +1193 -0
  203. package/src/hooks/loader-utils.ts +6 -0
  204. package/src/hooks/manager.ts +4 -4
  205. package/src/hooks/pack-cli.ts +237 -11
  206. package/src/hooks/pack-manifest.ts +479 -7
  207. package/src/hooks/pack-store.ts +156 -10
  208. package/src/hooks/policy-authority.ts +333 -0
  209. package/src/hooks/policy-catalog.ts +65 -0
  210. package/src/hooks/policy-evaluator.ts +940 -796
  211. package/src/hooks/policy-registry.ts +25 -0
  212. package/src/hooks/policy-reviewability.ts +229 -0
  213. package/src/hooks/policy-types.ts +126 -0
  214. package/src/hooks/semantic/combine.ts +541 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +392 -0
  217. package/src/hooks/semantic/envelope.ts +1296 -0
  218. package/src/hooks/semantic/evaluator.ts +547 -0
  219. package/src/hooks/semantic/facts.ts +292 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +643 -0
  222. package/src/hooks/semantic/jev-config.ts +594 -0
  223. package/src/hooks/semantic/jev-review.ts +374 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +421 -0
  226. package/src/hooks/semantic/pack-policies.ts +251 -0
  227. package/src/hooks/semantic/policies.ts +596 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2910 -0
  231. package/src/hooks/semantic/types.ts +145 -0
  232. package/src/hooks/semver-precedence.ts +128 -0
  233. package/src/hooks/worker-server.ts +119 -26
  234. package/src/index.ts +6 -0
  235. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  236. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  237. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  238. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  239. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  240. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  247. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +0 -10
  250. package/.next/standalone/.next/static/chunks/0cbvytf8epn34.js +0 -1
  251. package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +0 -2
  252. package/.next/standalone/.next/static/chunks/1p4xauf-zu6zk.js +0 -1
  253. package/.next/standalone/.next/static/chunks/1uc_s_dftgxiq.js +0 -6
  254. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  255. package/.next/standalone/.next/static/chunks/2lvr2pt70qn14.js +0 -1
  256. /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → qbVI48kgBOf9CTOKcFIk2}/_buildManifest.js +0 -0
  257. /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → qbVI48kgBOf9CTOKcFIk2}/_clientMiddlewareManifest.js +0 -0
  258. /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → qbVI48kgBOf9CTOKcFIk2}/_ssgManifest.js +0 -0
@@ -1,12 +1,60 @@
1
1
  # Changelog — `fp` CLI
2
2
 
3
- ## 0.0.1b3 — 2026-09-12
3
+ ## 0.0.2b0 — 2026-09-22
4
4
 
5
- Open for the next release. `0.0.1b2` published on 2026-09-12 and the `bump` job
6
- moved the version here automatically; nothing has landed against `0.0.1b3` yet.
5
+ Open for the next release. `0.0.1` published on 2026-09-22 and the `bump` job
6
+ moved the version here automatically; nothing has landed against `0.0.2b0` yet.
7
7
  Add entries as changes merge — this section becomes the GitHub Release body when
8
8
  it ships.
9
9
 
10
+ ## 0.0.1 — 2026-09-22
11
+
12
+ **First stable release.** `0.0.1b1` and `0.0.1b2` are the two betas behind it; the
13
+ `0.0.1b3` line the `bump` job opened is cut stable here rather than published, because
14
+ the server endpoints these commands drive are deployed to FailproofAI Cloud and the
15
+ CLI half should not stay a pre-release behind them. Until now `pipx install
16
+ fp-cloud-cli` resolved a beta only because no stable existed — a default that would
17
+ have silently changed the day one did. `Development Status` moves to
18
+ `5 - Production/Stable` with the version, so the classifier and the one string pip
19
+ reads say the same thing.
20
+
21
+ Nothing about the command surface changes at the cut: the entries below are the
22
+ entire diff from `0.0.1b2`, and `0.0.2b0` opens the next beta line.
23
+
24
+ ### Added
25
+
26
+ - `fp issues close <id>` — end an issue as won't-fix. Distinct from `resolve`: a
27
+ recurring audit finding reopens a **resolved** issue and leaves a **closed** one
28
+ alone. On an audit issue it marks the finding `dismissed`, without writing the
29
+ org-wide fingerprint suppression that `fp audits dismiss` writes. Exit 9 if the
30
+ issue already ended. (#815)
31
+ - `fp issues archive <id>` / `unarchive <id>` — take an issue off the board and put
32
+ it back. Orthogonal to state, allowed in any state, no confirmation prompt. (#815)
33
+ - `fp issues clear (--audit <id> | --all-audits | --everything) [--dry-run]` — resolve
34
+ every open issue in a scope plus the audit findings behind them, in one server-side
35
+ transaction. Exactly one scope flag is required. Needs `issues:close` **and**
36
+ `audits:write`. Writes no suppression, so anything still broken reopens its issue
37
+ on the next run. The confirm names the count from a dry run that shares its
38
+ scope predicate with the write. (#815)
39
+ - `issues list --state closed` is accepted, and `Incident` carries `closed_at` and
40
+ `archived_at`. (#815)
41
+
42
+ ### Fixes
43
+
44
+ - The recurrence sentence said two different things in one release. `audits resolve`'s
45
+ confirm line (`a genuine recurrence re-opens as new`), the skill and the command
46
+ reference implied a recurrence opens a **different** issue; `issues close --help`, the
47
+ audits guide and the CLI reference said the resolved issue **reopens**. The second is
48
+ the one the rest of the model needs — a `closed` issue has to have something to stay
49
+ closed through — so `reopens` is now what every surface says. (#815)
50
+ - `issues clear --help` claimed the `--dry-run` count "cannot disagree" with the write.
51
+ The two requests share a scope predicate, not a row set, so an issue entering the scope
52
+ between them is cleared without being in the confirmed number — correct for a scoped
53
+ clear, but not the guarantee that was written. The help now states the real one, and
54
+ names the closing line as the count of what changed. (#815)
55
+ - The skill answered "clear all our issues" with `--all-audits`, which leaves alert-born
56
+ and hand-opened issues untouched. Settling the scope is now the first step. (#815)
57
+
10
58
  ## 0.0.1b2 — 2026-08-25
11
59
 
12
60
  Open for the next release. `0.0.1b1` published on 2026-08-24 and the `bump` job
@@ -90,7 +90,7 @@ fp keys list|show|create|update|disable|regenerate # API keys (secret shown
90
90
  fp users list|show|create|update|disable|enable
91
91
  fp settings list|schema|set
92
92
  fp alerts list|show|create|update|delete|test
93
- fp issues list|count|show|ack|assign|resolve|comment-add|comment-list|comment-delete|subscribe|subscribers|unsubscribe|open
93
+ fp issues list|count|show|ack|assign|resolve|close|archive|unarchive|clear|comment-add|comment-list|comment-delete|subscribe|subscribers|unsubscribe|open
94
94
  fp audits list|show|create|edit|delete|run|runs|findings|finding # scheduled audits
95
95
  fp audits ack|assign|resolve|dismiss|mute|reopen # triage a finding
96
96
  fp audits context-show|context-set|context-refresh # reference context
@@ -1 +1 @@
1
- __version__ = "0.0.1b3"
1
+ __version__ = "0.0.2b0"
@@ -1161,6 +1161,50 @@ def resolve_incident(ctx: ClientContext, incident_id: str) -> None:
1161
1161
  _post_json(ctx, f"/api/issues/{incident_id}/resolve")
1162
1162
 
1163
1163
 
1164
+ def close_incident(ctx: ClientContext, incident_id: str) -> None:
1165
+ """POST /api/issues/{id}/close — end the issue as won't-fix.
1166
+
1167
+ The sibling of ``resolve_incident``, and the difference is what happens next:
1168
+ a resolved issue REOPENS if its audit finding recurs, a closed one does not.
1169
+ 409 if the issue already ended.
1170
+ """
1171
+ _post_json(ctx, f"/api/issues/{incident_id}/close")
1172
+
1173
+
1174
+ def set_incident_archived(ctx: ClientContext, incident_id: str, archived: bool) -> None:
1175
+ """POST /api/issues/{id}/archive or /unarchive — hide from the board, or restore.
1176
+
1177
+ Does not change ``state``: an issue ends once, and archiving says nothing
1178
+ about how. Idempotent on both sides, so a caller never has to read the
1179
+ current value first.
1180
+ """
1181
+ verb = "archive" if archived else "unarchive"
1182
+ _post_json(ctx, f"/api/issues/{incident_id}/{verb}")
1183
+
1184
+
1185
+ def clear_issues(
1186
+ ctx: ClientContext,
1187
+ *,
1188
+ scope: str,
1189
+ audit_id: Optional[str] = None,
1190
+ dry_run: bool = False,
1191
+ ) -> Dict[str, Any]:
1192
+ """POST /api/issues/bulk-clear — resolve every live issue in a scope.
1193
+
1194
+ ``scope`` is ``audit`` (with ``audit_id``), ``all_audits`` or ``everything``.
1195
+ Returns ``{issues, findings, dry_run, scope}``.
1196
+
1197
+ Needs ``issues:close`` AND ``audits:write`` — clearing resolves the audit
1198
+ findings behind the issues, and the second grant is what stops a key that
1199
+ cannot touch one finding from resolving all of them at once.
1200
+ """
1201
+ body: Dict[str, Any] = {"scope": scope, "dry_run": dry_run}
1202
+ if audit_id:
1203
+ body["audit_id"] = audit_id
1204
+ data = _post_json(ctx, "/api/issues/bulk-clear", body)
1205
+ return data if isinstance(data, dict) else {}
1206
+
1207
+
1164
1208
  def list_incident_comments(ctx: ClientContext, incident_id: str) -> List[IncidentComment]:
1165
1209
  data = _get_json(ctx, f"/api/issues/{incident_id}/comments")
1166
1210
  return [IncidentComment.from_dict(c) for c in (data if isinstance(data, list) else [])]
@@ -1,4 +1,4 @@
1
- """Incident triage: incidents list/count/show/ack/assign/resolve/comment*/subscribe*/open.
1
+ """Incident triage: incidents list/count/show/ack/assign/resolve/close/archive/clear/comment*/subscribe*/open.
2
2
 
3
3
  Incidents live under /api/issues but the triage workflow is distinct, so it
4
4
  gets its own top-level group. The id IS the handle (incidents have no human name), so the
@@ -19,13 +19,16 @@ from ..errors import ApiError, ForbiddenError, NotFoundError
19
19
  from . import _write
20
20
 
21
21
  _SEVERITIES = ("info", "warning", "critical")
22
- _STATES = ("firing", "acknowledged", "resolved")
22
+ # Mirrors `issue_sync::ISSUE_STATES` on the server. `closed` is the second
23
+ # TERMINAL state (won't-fix); missing it here would reject `--state closed`
24
+ # client-side with exit 2 for a state the server accepts.
25
+ _STATES = ("firing", "acknowledged", "resolved", "closed")
23
26
  _UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
24
27
 
25
28
 
26
29
  def _validate_states(value: Optional[str]) -> None:
27
30
  """Reject an unknown ``--state`` value up front (exit 2) rather than letting the server
28
- silently drop it and return a confusing set. Accepts a CSV of firing/acknowledged/resolved."""
31
+ silently drop it and return a confusing set. Accepts a CSV of firing/acknowledged/resolved/closed."""
29
32
  if value is None:
30
33
  return
31
34
  for s in value.split(","):
@@ -67,7 +70,7 @@ def _fail(state: AppState, exc: Exception, *, incident_id: str = "") -> None:
67
70
 
68
71
  def incidents_list(
69
72
  ctx: typer.Context,
70
- state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s): firing, acknowledged, resolved (CSV)."),
73
+ state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s): firing, acknowledged, resolved, closed (CSV)."),
71
74
  alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only incidents for this alert."),
72
75
  limit: int = typer.Option(50, "--limit", "-n", help="Max incidents to return."),
73
76
  show_id: bool = typer.Option(False, "--show-id", help="Show the full incident id instead of the short form (always full in --json)."),
@@ -244,6 +247,220 @@ def incidents_resolve(
244
247
  output.incident_resolved(incident_id)
245
248
 
246
249
 
250
+ def incidents_close(
251
+ ctx: typer.Context,
252
+ incident_id: str = typer.Argument(..., help="Issue id."),
253
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
254
+ ) -> None:
255
+ """Close an issue — "we're done with it", not "we fixed it".
256
+
257
+ The difference from `resolve` is what happens next. A **resolved** issue REOPENS if its
258
+ audit finding recurs: someone claimed a fix and the fix did not hold, which is worth
259
+ knowing. A **closed** issue does not — closing records a decision (won't fix, not a
260
+ problem, stale), and a recurrence is not news about a decision.
261
+
262
+ For an issue that came from an audit, this also marks the finding `dismissed`. It does
263
+ NOT write the finding's org-wide suppression: closing one issue never hides that pattern
264
+ in your other audits. Use `fp audits finding-status <id> --action dismiss` for that.
265
+
266
+ Needs `issues:close`. Exits 9 (conflict) if the issue already ended — an issue ends once,
267
+ and closing must not overwrite the record that someone believed they had fixed it.
268
+
269
+ With `--json`: `{"closed": true, "id": "<id>"}` (or `{cancelled: true}` on a declined
270
+ prompt).
271
+
272
+ Example:
273
+
274
+ * `fp issues close <id> --yes`
275
+ """
276
+ state: AppState = ctx.obj
277
+ cctx = require_auth(state)
278
+ if _write.should_prompt(state, yes):
279
+ alert_name = None
280
+ try:
281
+ alert_name = api.get_incident(cctx, incident_id).alert_name
282
+ except NotFoundError as exc:
283
+ _fail(state, exc, incident_id=incident_id)
284
+ except (ApiError, ForbiddenError):
285
+ alert_name = None
286
+ if not output.confirm_incident_close(incident_id, alert_name):
287
+ if state.json:
288
+ output.emit_json({"cancelled": True})
289
+ else:
290
+ output.cancelled_plain("nothing changed")
291
+ return
292
+ try:
293
+ api.close_incident(cctx, incident_id)
294
+ except (ApiError, ForbiddenError, NotFoundError) as exc:
295
+ _fail(state, exc, incident_id=incident_id)
296
+ _write.record_action("incident_closed", resource="incident", success=True)
297
+ if state.json:
298
+ output.emit_json({"closed": True, "id": incident_id})
299
+ else:
300
+ output.incident_closed(incident_id)
301
+
302
+
303
+ def incidents_archive(
304
+ ctx: typer.Context,
305
+ incident_id: str = typer.Argument(..., help="Issue id."),
306
+ ) -> None:
307
+ """Take an issue off the issues board, keeping its history.
308
+
309
+ Archiving is separate from state: it does not resolve or close anything, and an issue
310
+ that has already ended keeps the record of HOW it ended. It works on a live issue too —
311
+ you should not have to triage something in order to stop looking at it — because the
312
+ safety net is on the other side: a fresh alert breach or a recurring audit finding puts
313
+ a live issue back on the board automatically. Archive can hide a problem; it cannot keep
314
+ hiding one that is still happening. A closed (won't-fix) issue is the exception and stays
315
+ hidden, since nobody asked for it back.
316
+
317
+ No confirmation: archiving destroys nothing and `fp issues unarchive` undoes it.
318
+
319
+ Needs `issues:close`. With `--json`: `{"archived": true, "id": "<id>"}`.
320
+
321
+ Example:
322
+
323
+ * `fp issues archive <id>`
324
+ """
325
+ _set_archived(ctx, incident_id, True)
326
+
327
+
328
+ def incidents_unarchive(
329
+ ctx: typer.Context,
330
+ incident_id: str = typer.Argument(..., help="Issue id."),
331
+ ) -> None:
332
+ """Put an archived issue back on the issues board. See `fp issues archive`.
333
+
334
+ Needs `issues:close`. With `--json`: `{"archived": false, "id": "<id>"}`.
335
+
336
+ Example:
337
+
338
+ * `fp issues unarchive <id>`
339
+ """
340
+ _set_archived(ctx, incident_id, False)
341
+
342
+
343
+ def _set_archived(ctx: typer.Context, incident_id: str, archived: bool) -> None:
344
+ """Shared body of archive/unarchive. Two commands rather than one with a flag, so each
345
+ reads as the verb it is and neither can be invoked meaning the opposite."""
346
+ state: AppState = ctx.obj
347
+ cctx = require_auth(state)
348
+ try:
349
+ api.set_incident_archived(cctx, incident_id, archived)
350
+ except (ApiError, ForbiddenError, NotFoundError) as exc:
351
+ _fail(state, exc, incident_id=incident_id)
352
+ _write.record_action(
353
+ "incident_archived" if archived else "incident_unarchived",
354
+ resource="incident", success=True,
355
+ )
356
+ if state.json:
357
+ output.emit_json({"archived": archived, "id": incident_id})
358
+ else:
359
+ output.incident_archived(incident_id, archived)
360
+
361
+
362
+ def incidents_clear(
363
+ ctx: typer.Context,
364
+ audit_id: Optional[str] = typer.Option(None, "--audit", help="Clear only the issues this audit raised."),
365
+ all_audits: bool = typer.Option(False, "--all-audits", help="Clear every issue any audit raised. Alert and hand-opened issues are left alone."),
366
+ everything: bool = typer.Option(False, "--everything", help="Clear every open issue in the workspace, whatever opened it."),
367
+ dry_run: bool = typer.Option(False, "--dry-run", help="Print what would be cleared and change nothing."),
368
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
369
+ ) -> None:
370
+ """Resolve every open issue in a scope — a fresh start after changing your agents.
371
+
372
+ Exactly one of `--audit <id>`, `--all-audits` or `--everything` is required: the three
373
+ have very different blast radii, so there is deliberately no default.
374
+
375
+ This is the same thing as resolving each issue by hand, including what it does NOT do.
376
+ **Nothing is deleted, and nothing is suppressed.** A pattern your agent changes genuinely
377
+ fixed stays gone; one they did not comes back on the next audit run and REOPENS the issue
378
+ it was raised under — a cleared board is not a quiet one. If you want a pattern silenced
379
+ for good, that is `fp audits finding-status <id> --action mute`, which is a different and
380
+ much bigger hammer.
381
+
382
+ Run it with `--dry-run` first: the count comes from the server, taken with the same scope
383
+ predicate the write uses, so it is the real size of the scope rather than a client-side
384
+ guess. It is a count, not a lease — the confirmed write re-runs that predicate, so an
385
+ issue that entered the scope since the preview is cleared along with the rest, which is
386
+ what clearing a SCOPE means. The line printed at the end is what actually changed.
387
+
388
+ Needs `issues:close` AND `audits:write` — clearing resolves the audit findings behind the
389
+ issues, so a key that cannot touch one finding cannot resolve all of them at once.
390
+
391
+ With `--json`: `{"issues": n, "findings": n, "dry_run": bool, "scope": "..."}` (or
392
+ `{cancelled: true}` on a declined prompt).
393
+
394
+ Examples:
395
+
396
+ * `fp issues clear --all-audits --dry-run`
397
+ * `fp issues clear --audit <id> --yes`
398
+ """
399
+ state: AppState = ctx.obj
400
+ picked = [bool(audit_id), all_audits, everything]
401
+ if sum(picked) != 1:
402
+ raise typer.BadParameter(
403
+ "choose exactly one of --audit <id>, --all-audits, or --everything.",
404
+ param_hint="--all-audits",
405
+ )
406
+ if audit_id:
407
+ scope, label = "audit", "this audit"
408
+ elif all_audits:
409
+ scope, label = "all_audits", "every audit"
410
+ else:
411
+ scope, label = "everything", "the whole workspace"
412
+
413
+ cctx = require_auth(state)
414
+
415
+ # The dry run is also the preview behind the prompt, so a `--dry-run` call and a
416
+ # confirmed clear count the same rows the same way.
417
+ try:
418
+ preview = api.clear_issues(cctx, scope=scope, audit_id=audit_id, dry_run=True)
419
+ except (ApiError, ForbiddenError, NotFoundError) as exc:
420
+ _fail(state, exc)
421
+ return
422
+ issues = int(preview.get("issues") or 0)
423
+ findings = int(preview.get("findings") or 0)
424
+
425
+ if dry_run:
426
+ if state.json:
427
+ output.emit_json(preview)
428
+ else:
429
+ output.issues_cleared(issues, findings, dry_run=True)
430
+ return
431
+
432
+ if issues == 0:
433
+ # Nothing to do is not a failure, and prompting to confirm zero rows is noise.
434
+ if state.json:
435
+ output.emit_json({"issues": 0, "findings": 0, "dry_run": False, "scope": scope})
436
+ else:
437
+ output.issues_cleared(0, 0)
438
+ return
439
+
440
+ if _write.should_prompt(state, yes) and not output.confirm_issues_clear(label, issues, findings):
441
+ if state.json:
442
+ output.emit_json({"cancelled": True})
443
+ else:
444
+ output.cancelled_plain("nothing changed")
445
+ return
446
+
447
+ try:
448
+ result = api.clear_issues(cctx, scope=scope, audit_id=audit_id)
449
+ except (ApiError, ForbiddenError, NotFoundError) as exc:
450
+ _fail(state, exc)
451
+ return
452
+ # `mode` and `count` are both on _SAFE_PROP_KEYS, and both are shape: a closed enum we
453
+ # authored and an integer. Never an audit id or an issue title.
454
+ _write.record_action(
455
+ "issues_cleared", resource="incident", success=True, destructive=True,
456
+ mode=scope, count=int(result.get("issues") or 0),
457
+ )
458
+ if state.json:
459
+ output.emit_json(result)
460
+ else:
461
+ output.issues_cleared(int(result.get("issues") or 0), int(result.get("findings") or 0))
462
+
463
+
247
464
  def incidents_comment_list(
248
465
  ctx: typer.Context,
249
466
  incident_id: str = typer.Argument(..., help="Incident id."),
@@ -454,7 +671,7 @@ def register(app: typer.Typer) -> None:
454
671
  no_args_is_help=True,
455
672
  rich_markup_mode="markdown",
456
673
  context_settings={"help_option_names": ["-h", "--help"]},
457
- help="Triage issues (list / count / show / ack / assign / resolve / comment-* / subscribe* / open).",
674
+ help="Triage issues (list / count / show / ack / assign / resolve / close / archive / unarchive / clear / comment-* / subscribe* / open).",
458
675
  )
459
676
  inc.command("list", epilog=GLOBALS_EPILOG)(incidents_list)
460
677
  inc.command("count", epilog=GLOBALS_EPILOG)(incidents_count)
@@ -462,6 +679,10 @@ def register(app: typer.Typer) -> None:
462
679
  inc.command("ack", epilog=GLOBALS_EPILOG)(incidents_ack)
463
680
  inc.command("assign", epilog=GLOBALS_EPILOG)(incidents_assign)
464
681
  inc.command("resolve", epilog=GLOBALS_EPILOG)(incidents_resolve)
682
+ inc.command("close", epilog=GLOBALS_EPILOG)(incidents_close)
683
+ inc.command("archive", epilog=GLOBALS_EPILOG)(incidents_archive)
684
+ inc.command("unarchive", epilog=GLOBALS_EPILOG)(incidents_unarchive)
685
+ inc.command("clear", epilog=GLOBALS_EPILOG)(incidents_clear)
465
686
  inc.command("comment-list", epilog=GLOBALS_EPILOG)(incidents_comment_list)
466
687
  inc.command("comment-add", epilog=GLOBALS_EPILOG)(incidents_comment_add)
467
688
  inc.command("comment-delete", epilog=GLOBALS_EPILOG)(incidents_comment_delete)
@@ -417,6 +417,12 @@ class Incident:
417
417
  acknowledged_by: Optional[str] = None
418
418
  assignees: List[str] = field(default_factory=list)
419
419
  resolved_at: Optional[str] = None
420
+ #: When the issue was CLOSED (won't-fix) rather than resolved (fixed). At most
421
+ #: one of the two is ever set. Both absent on a live issue.
422
+ closed_at: Optional[str] = None
423
+ #: When the issue was hidden from the board. Orthogonal to ``state`` — an
424
+ #: archived issue keeps whatever state it ended in.
425
+ archived_at: Optional[str] = None
420
426
  breach_value: Optional[float] = None
421
427
  breach_summary: Optional[str] = None
422
428
  evidence: Optional[Dict[str, Any]] = None
@@ -443,6 +449,11 @@ class Incident:
443
449
  acknowledged_by=d.get("acknowledged_by"),
444
450
  assignees=list(d.get("assignees") or []),
445
451
  resolved_at=d.get("resolved_at"),
452
+ # `from_dict` is an ALLOWLIST: a key missing here is dropped with no
453
+ # error, so a column added server-side renders as a blank column here
454
+ # and nowhere complains. That is why these two lines exist at all.
455
+ closed_at=d.get("closed_at"),
456
+ archived_at=d.get("archived_at"),
446
457
  breach_value=d.get("breach_value"),
447
458
  breach_summary=d.get("breach_summary"),
448
459
  evidence=d.get("evidence"),
@@ -4563,6 +4563,73 @@ def incident_resolved(incident_id: str) -> None:
4563
4563
  (_short_id(incident_id or "-"), theme.ACCENT))
4564
4564
 
4565
4565
 
4566
+ def confirm_incident_close(incident_id: str, alert_name: Optional[str] = None) -> bool:
4567
+ """Plain close confirm (stderr). Says what separates close from resolve — a closed issue
4568
+ does NOT come back when the pattern recurs — because that is the whole distinction and
4569
+ nobody can infer it from the word."""
4570
+ h = Text()
4571
+ h.append("⚠ ", style=f"bold {theme.AMBER}")
4572
+ h.append("close issue ", style=theme.TEXT)
4573
+ h.append(_short_id(incident_id or "-"), style=f"bold {theme.ACCENT}")
4574
+ if alert_name:
4575
+ h.append(f" ({alert_name})", style=theme.LABEL)
4576
+ h.append("?", style=theme.TEXT)
4577
+ return confirm_line(
4578
+ h, Text("won't fix — it stays closed even if the pattern comes back", style=theme.LABEL)
4579
+ )
4580
+
4581
+
4582
+ def incident_closed(incident_id: str) -> None:
4583
+ """Plain green line (stderr): ``✓ closed issue {short id}``."""
4584
+ if _quiet:
4585
+ return
4586
+ _stderr.print()
4587
+ _plain(("✓ ", theme.SUCCESS), ("closed issue ", theme.TEXT),
4588
+ (_short_id(incident_id or "-"), theme.ACCENT))
4589
+
4590
+
4591
+ def incident_archived(incident_id: str, archived: bool) -> None:
4592
+ """Plain green line (stderr): ``✓ archived issue {short id}`` / ``✓ unarchived …``."""
4593
+ if _quiet:
4594
+ return
4595
+ _stderr.print()
4596
+ _plain(("✓ ", theme.SUCCESS),
4597
+ ("archived issue " if archived else "unarchived issue ", theme.TEXT),
4598
+ (_short_id(incident_id or "-"), theme.ACCENT))
4599
+
4600
+
4601
+ def confirm_issues_clear(scope_label: str, issues: int, findings: int) -> bool:
4602
+ """The bulk-clear confirm (stderr). Names the COUNT from the server's dry run rather than
4603
+ the scope alone — a confirmation that cannot say how many rows it will change is a
4604
+ confirmation in name only — and states the part operators actually need: nothing is
4605
+ suppressed, so a pattern that survived their agent changes comes back and reopens its issue."""
4606
+ h = Text()
4607
+ h.append("⚠ ", style=f"bold {theme.AMBER}")
4608
+ h.append("clear ", style=theme.TEXT)
4609
+ h.append(f"{issues} issue{'' if issues == 1 else 's'}", style=f"bold {theme.ACCENT}")
4610
+ h.append(f" ({scope_label})", style=theme.LABEL)
4611
+ h.append("?", style=theme.TEXT)
4612
+ tail = f"resolves them"
4613
+ if findings:
4614
+ tail += f" and {findings} audit finding{'' if findings == 1 else 's'}"
4615
+ tail += " — nothing is deleted or suppressed"
4616
+ return confirm_line(h, Text(tail, style=theme.LABEL))
4617
+
4618
+
4619
+ def issues_cleared(issues: int, findings: int, dry_run: bool = False) -> None:
4620
+ """Plain green line (stderr) for a completed bulk clear, or the dry-run preview."""
4621
+ if _quiet:
4622
+ return
4623
+ _stderr.print()
4624
+ noun = f"{issues} issue{'' if issues == 1 else 's'}"
4625
+ if dry_run:
4626
+ _plain(("○ ", theme.FAINT), ("would clear ", theme.LABEL), (noun, theme.ACCENT),
4627
+ (f" · {findings} finding{'' if findings == 1 else 's'}", theme.FAINT))
4628
+ return
4629
+ _plain(("✓ ", theme.SUCCESS), ("cleared ", theme.TEXT), (noun, theme.ACCENT),
4630
+ (f" · {findings} finding{'' if findings == 1 else 's'} resolved", theme.FAINT))
4631
+
4632
+
4566
4633
  def incident_assigned(incident_id: str, assignees: Sequence[str]) -> None:
4567
4634
  """Plain green line (stderr): ``✓ assigned {short id} · a@x, b@x`` — or ``✓ cleared assignees
4568
4635
  on {short id}`` when the list is empty."""
@@ -5754,7 +5821,7 @@ def confirm_finding_action(action: str, finding_id: str, *, title: Optional[str]
5754
5821
  consequence = {
5755
5822
  "mute": "future runs stop surfacing this pattern",
5756
5823
  "dismiss": "it's suppressed as not worth acting on",
5757
- "resolve": "it closes; a genuine recurrence re-opens as new",
5824
+ "resolve": "it closes; a genuine recurrence re-opens it",
5758
5825
  }.get(action, "this changes the finding's status")
5759
5826
  return confirm_line(h, Text(consequence, style=theme.LABEL))
5760
5827
 
@@ -12,7 +12,7 @@ license = { file = "LICENSE" }
12
12
  authors = [{ name = "Failproof AI", email = "failproofai@exosphere.host" }]
13
13
  keywords = ["agents", "observability", "ai", "monitoring", "failproofai", "cli"]
14
14
  classifiers = [
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Environment :: Console",
17
17
  "Intended Audience :: Developers",
18
18
  "Programming Language :: Python :: 3",
@@ -192,7 +192,7 @@ you need a flag you don't already know.
192
192
  - `users list|show|create|update|disable|enable` — referenced by **email**.
193
193
  - `settings list|schema|set` — fixed registry; `schema` shows what each key accepts.
194
194
  - `alerts list|show|create|update|delete|test` — referenced by **name**.
195
- - `issues list|count|show|ack|assign|resolve|comment-add|comment-list|comment-delete|subscribe|subscribers|unsubscribe|open` — by id (short ids accepted). **One board for everything needing attention**: alert breaches, hand-raised issues, and audit findings, told apart by a `source` of `alert` / `manual` / `audit`. (This group was called `incidents` before; the old name is gone.)
195
+ - `issues list|count|show|ack|assign|resolve|close|archive|unarchive|clear|comment-add|comment-list|comment-delete|subscribe|subscribers|unsubscribe|open` — by id (short ids accepted). **One board for everything needing attention**: alert breaches, hand-raised issues, and audit findings, told apart by a `source` of `alert` / `manual` / `audit`. (This group was called `incidents` before; the old name is gone.)
196
196
  - `audits list|show|create|edit|delete|run|runs` — scheduled sweeps, referenced by **name**; `audits findings|finding` + the triage verbs `ack|mute|dismiss|resolve|reopen|assign` act on a finding **id**. `audits run <name>` only *queues* a run (poll `audits runs <name>` for completion). See §8.
197
197
 
198
198
  **Enforce (cloud-managed policy, session-only — see §2):**
@@ -225,6 +225,9 @@ command (`list <kind>`, `whoami`, a `list` subcommand) before committing.
225
225
  | "what has this org used this metering window?" | `usage` (or `--json usage` for the complete response) |
226
226
  | "is anything on fire?", "any alerts firing / open issues?" | `alerts list` + `issues list` (and `issues count`) |
227
227
  | "ack / look at / resolve that issue" | `issues list` → `issues show <id>` → **confirm** → `issues ack`/`resolve <id>` |
228
+ | "clear all our issues", "fresh start", "we changed the agents" | **settle the scope first** (`--all-audits` leaves alert and hand-opened issues alone; `--everything` does not; `--audit <id>` is one audit) → `issues clear <scope> --dry-run` → show the count → **confirm** → `issues clear <scope>` |
229
+ | "we're not going to fix that one" | `issues close <id>` (NOT `resolve` — closed survives a recurrence) |
230
+ | "get that off my board" | `issues archive <id>` |
228
231
  | "run an audit", "what did the audit find?", "any findings to triage?" | `audits list` → `audits run <name>` (queues) → `audits runs <name>` (wait for `succeeded`) → `audits findings --audit <name>`; triage with `audits resolve/mute/dismiss <id>` — **confirm first** |
229
232
  | "give CI / this service an API key" | `keys create <name> --add events:add` (scope to what they describe) — **state it, then create**; capture the one-time secret |
230
233
  | "who has access?", "add / remove a teammate", "make them read-only" | `users list` / `users show <email>` / `users create`/`update`/`disable` |
@@ -267,6 +270,18 @@ fp --json events --full --session-id run-001 --all | jq '.events[].payload' #
267
270
  reliable cross-check that you pulled everything.
268
271
  - **Triage flow:** `issues list` → `issues show <id>` (read the activity
269
272
  log) → confirm with the user → `issues ack <id>` or `resolve <id>`.
273
+ - **`resolve` vs `close`:** `resolve` claims a fix, so a recurring audit finding
274
+ REOPENS it — that is the signal that the fix did not hold. `close` records a
275
+ decision (won't fix / not a problem / stale) and survives the recurrence. Pick
276
+ the one that matches what the user actually said; they are not synonyms.
277
+ - **"clear all our issues" / "fresh start":** that is `issues clear`, not a loop
278
+ of `resolve`. **Settle the scope before you run anything** — "all our issues"
279
+ does not pick one: `--all-audits` leaves alert and hand-opened issues on the
280
+ board, `--everything` takes them too, and `--audit <id>` is one audit's work.
281
+ Ask which they mean, then `--dry-run` first, tell the user the number it
282
+ returns, and only then run it for real. It resolves the audit findings too and
283
+ writes **no** suppression, so anything still broken comes back and reopens its
284
+ issue — say that, because users often expect "clear" to mean "silence".
270
285
  - **Investigate a regression:** `evals --aggregate` to see which score dropped →
271
286
  `evals --score helpfulness:..0.5` to list the bad runs → `events --session-id <id>`
272
287
  to see what happened inside one.
@@ -293,7 +308,7 @@ Two things about the flow matter when driving it from the CLI:
293
308
  either surface works, they never disagree. Triage a finding with
294
309
  `audits ack|mute|dismiss|resolve|reopen <id>` (durable **mute/dismiss** suppress
295
310
  the pattern org-wide by fingerprint; **resolve** leaves no suppression, so a true
296
- recurrence reopens as new). Reads need `audits:read`, every mutation
311
+ recurrence reopens the issue). Reads need `audits:read`, every mutation
297
312
  `audits:write` (note: triaging a finding needs `audits:write`, not an `issues:*`
298
313
  permission — the audit is the system of record and the issue follows it).
299
314
 
@@ -169,7 +169,7 @@ Context is a **separate sub-resource on purpose**: `audits edit` read-merges the
169
169
  - `audits ack <id> [--reason ...]` — seen, stays visible, ranked lower. No confirm.
170
170
  - `audits mute <id> [--reason ...] [--yes]` — stop surfacing this pattern in future runs (durable). Confirms first.
171
171
  - `audits dismiss <id> [--reason ...] [--yes]` — judged not worth acting on; suppressed like mute. Confirms first.
172
- - `audits resolve <id> [--yes]` — you fixed it. Leaves **no** suppression, so a genuine recurrence is raised as new. Confirms first.
172
+ - `audits resolve <id> [--yes]` — you fixed it. Leaves **no** suppression, so a genuine recurrence reopens the issue it graduated to. Confirms first.
173
173
  - `audits reopen <id>` — back to `open` **and** clears any mute/dismiss suppression. The undo for the three above.
174
174
  - `audits assign <id> --to <email>` — set the owner; the status is untouched. `--to` is required (exit 2 without it).
175
175
 
@@ -177,14 +177,17 @@ The title column truncates to whatever width is left so the fixed columns always
177
177
 
178
178
  **`audits run` is async — it only queues.** Success is `{"queued": true}`, not a finished run; the analysis can take minutes. Poll `audits runs <name>` until the newest row is `succeeded`/`failed` before reading `audits findings`, rather than assuming results exist on the call that queued them.
179
179
 
180
- **Findings and issues are one bucket.** Every finding graduates to an issue (`source: audit`) that stores the finding's full content, so the same problem appears under both `audits findings` and `issues list`. Triage is consistent in **both** directions and needs `audits:write` either way: `audits resolve|mute|dismiss|ack|reopen <finding-id>` mirrors onto the linked issue, and `issues resolve <issue-id>` on an audit issue mirrors back onto the finding — the two never disagree. **resolve** leaves no suppression (a genuine recurrence reopens as new); **mute/dismiss** suppress the pattern org-wide by fingerprint.
180
+ **Findings and issues are one bucket.** Every finding graduates to an issue (`source: audit`) that stores the finding's full content, so the same problem appears under both `audits findings` and `issues list`. Triage is consistent in **both** directions and needs `audits:write` either way: `audits resolve|mute|dismiss|ack|reopen <finding-id>` mirrors onto the linked issue, and `issues resolve <issue-id>` on an audit issue mirrors back onto the finding — the two never disagree. **resolve** leaves no suppression (a genuine recurrence reopens the issue); **mute/dismiss** suppress the pattern org-wide by fingerprint. `issues close <issue-id>` sits between the two: it marks the finding `dismissed` and the issue stays closed through a recurrence, but it writes no org-wide suppression. `issues clear` is bulk `resolve`, with the same absence of suppression.
181
181
 
182
182
  ## issues
183
- The single board for everything needing human attention — alert breaches (`source: alert`), hand-raised issues (`manual`), and audit findings (`audit`). Referenced by id (short ids accepted, `--show-id` shows them). This group was **renamed from `incidents`**; the old name no longer exists. Reads and ack/comment need `issues:read`; opening, assigning, and subscribing others need `issues:create`; resolving needs `issues:close`.
184
- - `issues list [--state firing|acknowledged|resolved] [--alert-id <id>] [--limit N] [--show-id]` — there is **no** `--severity` filter on this command.
183
+ The single board for everything needing human attention — alert breaches (`source: alert`), hand-raised issues (`manual`), and audit findings (`audit`). Referenced by id (short ids accepted, `--show-id` shows them). This group was **renamed from `incidents`**; the old name no longer exists. Reads and ack/comment need `issues:read`; opening, assigning, and subscribing others need `issues:create`; resolving, closing, archiving and clearing need `issues:close` (and `clear` also needs `audits:write`, because it resolves the findings behind the issues).
184
+ - `issues list [--state firing|acknowledged|resolved|closed] [--alert-id <id>] [--limit N] [--show-id]` — there is **no** `--severity` filter on this command. Archived issues are hidden.
185
185
  - `issues count`
186
186
  - `issues show <id>` — identity + comments + subscribers + **activity log** (read this before acting). An audit-born issue (`source: audit`) carries the full finding it graduated from and back-links to the audit/run.
187
187
  - `issues ack <id>` · `issues assign <id> --assignee <member>` (repeatable; omit to clear all assignees; each must be an operator) · `issues resolve <id>` (calm confirm). **On an audit issue these stay in sync with the finding** — resolving the issue resolves the underlying audit finding, so it can't reappear on the next run (equivalently, triage it with `audits resolve <finding-id>`; both surfaces agree).
188
+ - `issues close <id>` (calm confirm) — the second terminal state: **"we're done with it", not "we fixed it"** (won't fix / not a problem / stale). The difference from `resolve` is what happens next: a **resolved** issue REOPENS when its audit finding recurs, a **closed** one does not. On an audit issue this marks the finding `dismissed`; it does **not** write the org-wide fingerprint suppression that `audits dismiss` writes, so closing one issue never hides that pattern in your other audits. Exit 9 if the issue already ended — an issue ends once, and neither terminal state may overwrite the other.
189
+ - `issues archive <id>` · `issues unarchive <id>` — take an issue off the board / put it back. **Orthogonal to state**: archiving resolves nothing and never overwrites how an issue ended, and it works on a live issue too. No confirm (it destroys nothing). A fresh alert breach or a recurring audit finding un-archives a live issue automatically, so archive can hide a problem but cannot keep hiding one that is still happening; a `closed` issue is the exception and stays hidden.
190
+ - `issues clear (--audit <id> | --all-audits | --everything) [--dry-run] [--yes]` — **the "we changed our agents, give us a fresh board" command.** Resolves every open issue in the scope in one server-side transaction, plus the audit findings behind them. Exactly one scope flag is required. Needs `issues:close` **and** `audits:write`. **Nothing is deleted and nothing is suppressed** — a pattern the agent changes genuinely fixed stays gone, one they did not comes back on the next run and reopens its issue; for permanent silence use `audits mute`/`dismiss` instead. Always `--dry-run` first: the count comes from the server using the same scope predicate the write uses, so it is the real size of the scope — but it is a count, not a lease: the write re-runs the predicate, so anything that entered the scope since the preview is cleared too, and the closing line reports what actually changed.
188
191
  - `issues open --summary <text> (--title <text> | --alert-id <id>) [--title ...] [--severity ...]` — `--title` is **required** for a standalone issue (nothing to borrow a name from); with `--alert-id` it is optional and defaults to the alert's name. Missing `--title` on the standalone path → exit 2.
189
192
  - `issues comment-add <id> (--body <text> | --file <path>)` — `--file -` reads stdin; exactly one of the two · `comment-list <id>` · `comment-delete <id> <comment-id>`
190
193
  - `issues subscribe <id> [--email <addr>]` · `unsubscribe <id> [--email <addr>]` · `subscribers <id>` — `--email` defaults to you; naming someone else needs `issues:create`