failproofai 1.0.7 → 1.0.8-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 +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/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 +9 -7
  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 +11 -10
  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 +6 -6
  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 +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  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]__20kzpkf._.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]__1ttrwnd._.js +22 -0
  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/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  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]__056wjo4._.js +4 -0
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.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]__0n0xg95._.js +4 -0
  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]__0rwtwpm._.js +4 -0
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bd3mje._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +4 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  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/{_166t73i._.js → _0o4xkpl._.js} +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{_1-i_gzc._.js → _1-7sqrb._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  120. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  131. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  132. package/.next/standalone/.next/server/pages/404.html +1 -1
  133. package/.next/standalone/.next/server/pages/500.html +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  135. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  136. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0ahfwmbkpgfw4.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  139. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 1v_tp3hm8wyhe.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  143. package/.next/standalone/.next/static/chunks/2vo7qbdbvnc0o.js +69 -0
  144. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 2z7i-yg59w4pn.js} +1 -1
  145. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 3bgot5v6f6s3c.js} +1 -1
  147. package/.next/standalone/.next/static/chunks/3o3f1ibfci0p7.js +6 -0
  148. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  149. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  150. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  151. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  152. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  153. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  154. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  155. package/.next/standalone/app/settings/page.tsx +20 -1
  156. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  157. package/.next/standalone/app/settings/settings.css +79 -0
  158. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  159. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  161. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  164. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  165. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  166. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  167. package/.next/standalone/package.json +10 -10
  168. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  169. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  170. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  171. package/.next/standalone/server.js +1 -1
  172. package/README.md +3 -2
  173. package/bin/failproofai.mjs +148 -4
  174. package/dist/cli.mjs +18537 -9492
  175. package/dist/index.js +19 -1
  176. package/dist/worker.mjs +10421 -3770
  177. package/package.json +10 -10
  178. package/pi-extension/index.ts +11 -0
  179. package/scripts/build-policy-pack.mjs +53 -2
  180. package/src/audit/features.ts +3 -2
  181. package/src/hooks/builtin-policies.ts +195 -8
  182. package/src/hooks/cloud-connection.ts +190 -1
  183. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  184. package/src/hooks/cloud-introspect.ts +6 -0
  185. package/src/hooks/cloud-managed-policies.ts +22 -0
  186. package/src/hooks/configure-wizard.ts +26 -6
  187. package/src/hooks/custom-hooks-loader.ts +98 -10
  188. package/src/hooks/custom-hooks-registry.ts +45 -1
  189. package/src/hooks/effective-reviewers.ts +243 -0
  190. package/src/hooks/first-run-gate.ts +5 -0
  191. package/src/hooks/flush-cli.ts +35 -8
  192. package/src/hooks/fp-config.ts +266 -1
  193. package/src/hooks/fp-home.ts +23 -0
  194. package/src/hooks/fp-reset.ts +22 -4
  195. package/src/hooks/handler.ts +303 -7
  196. package/src/hooks/hook-activity-store.ts +118 -6
  197. package/src/hooks/hook-telemetry.ts +41 -0
  198. package/src/hooks/jev-activity.ts +385 -0
  199. package/src/hooks/jev-cli.ts +2172 -0
  200. package/src/hooks/jev-cloud-connection.ts +392 -0
  201. package/src/hooks/loader-utils.ts +6 -0
  202. package/src/hooks/manager.ts +45 -7
  203. package/src/hooks/pack-cli.ts +568 -37
  204. package/src/hooks/pack-failclosed.ts +3 -0
  205. package/src/hooks/pack-manifest.ts +489 -7
  206. package/src/hooks/pack-store.ts +194 -11
  207. package/src/hooks/policy-authority.ts +386 -0
  208. package/src/hooks/policy-catalog.ts +206 -0
  209. package/src/hooks/policy-evaluator.ts +963 -796
  210. package/src/hooks/policy-registry.ts +27 -1
  211. package/src/hooks/policy-reviewability.ts +258 -0
  212. package/src/hooks/policy-types.ts +128 -0
  213. package/src/hooks/semantic/combine.ts +581 -0
  214. package/src/hooks/semantic/compile.ts +176 -0
  215. package/src/hooks/semantic/decide.ts +408 -0
  216. package/src/hooks/semantic/envelope.ts +1255 -0
  217. package/src/hooks/semantic/evaluator.ts +556 -0
  218. package/src/hooks/semantic/facts.ts +298 -0
  219. package/src/hooks/semantic/intent.ts +1190 -0
  220. package/src/hooks/semantic/jev-client.ts +1155 -0
  221. package/src/hooks/semantic/jev-config.ts +1142 -0
  222. package/src/hooks/semantic/jev-review.ts +389 -0
  223. package/src/hooks/semantic/jev-stats.ts +289 -0
  224. package/src/hooks/semantic/jev-throttle.ts +427 -0
  225. package/src/hooks/semantic/pack-policies.ts +294 -0
  226. package/src/hooks/semantic/policies.ts +597 -0
  227. package/src/hooks/semantic/precondition-names.ts +60 -0
  228. package/src/hooks/semantic/preconditions.ts +58 -0
  229. package/src/hooks/semantic/redact.ts +2937 -0
  230. package/src/hooks/semantic/session-root.ts +116 -0
  231. package/src/hooks/semantic/types.ts +156 -0
  232. package/src/hooks/semver-precedence.ts +128 -0
  233. package/src/hooks/tui.ts +4 -0
  234. package/src/hooks/types.ts +1 -1
  235. package/src/hooks/worker-server.ts +121 -27
  236. package/src/index.ts +6 -0
  237. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  239. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  240. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +0 -4
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +0 -4
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +0 -4
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +0 -4
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  253. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +0 -4
  255. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  257. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  260. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  261. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  262. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  263. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  264. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  265. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  266. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  267. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  268. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  269. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
  270. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
  271. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
@@ -13,7 +13,20 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
13
13
  import { INTEGRATION_TYPES } from "./types";
14
14
  import { PACK_COMMIT_RE, PACK_VERSION_RE } from "./pack-manifest";
15
15
  import { detectInstalledClis } from "./integrations";
16
- import { parsePackIdentity, parsePackPolicy, readInstalledPacks } from "./pack-manifest";
16
+ import {
17
+ packParamsProblem,
18
+ parsePackIdentity,
19
+ parsePackPolicy,
20
+ parsePackSemanticPolicy,
21
+ readInstalledPacks,
22
+ type SemanticManifestEntry,
23
+ } from "./pack-manifest";
24
+ import { compareVersions, parseSemver } from "./semver-precedence";
25
+ // The build step measures a pack's question set against the envelope's own
26
+ // budget, which means reaching the semantic side. This is a CLI module — loaded
27
+ // by `failproofai publish`, never by a hook — so the rule that keeps those
28
+ // modules off an unconfigured machine's hook path does not apply here.
29
+ import { BUILTIN_QUESTION_CHARS, MAX_PACK_QUESTION_CHARS, questionChars, semanticPoliciesFromPacks } from "./semantic/pack-policies";
17
30
  import {
18
31
  AmbiguousPackId,
19
32
  PACK_CHECKSUMS_ASSET,
@@ -27,10 +40,15 @@ import {
27
40
  removePack,
28
41
  setPackPolicyEnabled,
29
42
  slugifyCategory,
43
+ type PackPreview,
30
44
  } from "./pack-store";
31
45
  import type { PolicyEffect } from "./cloud-managed-policies";
32
46
  import { loadCustomHooks } from "./custom-hooks-loader";
33
- import type { MultiChoice, TTYIn, TTYOut } from "./tui";
47
+ import { getSemanticRegistrations } from "./custom-hooks-registry";
48
+ import { isFirstPartyPack, isReservedClaim, jevPacks } from "./effective-reviewers";
49
+ import { authorityFieldsOf, authorityProblem, resolvePolicyAuthority } from "./policy-authority";
50
+ import type { PolicyCatalogEntry, SemanticPolicyDeclaration } from "./policy-types";
51
+ import type { MultiChoice, RenderOpts, TTYIn, TTYOut } from "./tui";
34
52
  import {
35
53
  chip,
36
54
  emptyState,
@@ -64,8 +82,20 @@ export interface PackBuildMeta {
64
82
  policies: number;
65
83
  defaultOn: number;
66
84
  commit?: string;
85
+ /** Jev checks and no regex half: `publish` repeats {@link JEV_ONLY_ROLLBACK_REMINDER}. */
86
+ semanticOnly?: boolean;
67
87
  }
68
88
 
89
+ /**
90
+ * A build from before `semanticOnly` refuses such a pack as blanket-deny, so its
91
+ * author has to tell users. Shared by build's lines and publish's success
92
+ * message, which is the only one an author actually reads.
93
+ */
94
+ const JEV_ONLY_ROLLBACK_REMINDER = [
95
+ " Jev checks alone: tell users to remove it before rolling a machine back to an older failproofai,",
96
+ " which can deny every tool call over a pack it will not load (see the publish-a-pack docs).",
97
+ ];
98
+
69
99
  const ok = (lines: string[], meta?: PackBuildMeta): PackCliResult => ({
70
100
  lines,
71
101
  exitCode: 0,
@@ -73,12 +103,30 @@ const ok = (lines: string[], meta?: PackBuildMeta): PackCliResult => ({
73
103
  });
74
104
  const fail = (lines: string[]): PackCliResult => ({ lines, exitCode: 1 });
75
105
 
76
- function parseList(rest: string[], flag: string): string[] | undefined {
77
- const idx = rest.findIndex((a) => a === flag || a.startsWith(`${flag}=`));
78
- if (idx === -1) return undefined;
79
- const raw = rest[idx].includes("=") ? rest[idx].split("=").slice(1).join("=") : rest[idx + 1];
80
- if (!raw || raw.startsWith("--")) return [];
81
- return raw.split(",").map((s) => s.trim()).filter(Boolean);
106
+ /**
107
+ * The first failproofai that runs a pack's Jev checks as published: 1.0.7
108
+ * ignores `semantic` (and `minCliVersion`), and 1.0.7-beta.x replaces the
109
+ * built-in checks with a pack's. A pack with checks is held to at least this.
110
+ */
111
+ const JEV_PACK_MIN_CLI = "1.0.8-beta.0";
112
+
113
+ /**
114
+ * Every occurrence of any of `flags`, comma-split and de-duplicated: a repeated
115
+ * flag adds to the list rather than being silently dropped. `[]` when any
116
+ * occurrence has no value (callers refuse that), undefined when none appears.
117
+ */
118
+ function parseList(rest: string[], ...flags: string[]): string[] | undefined {
119
+ let found = false;
120
+ const out: string[] = [];
121
+ for (let i = 0; i < rest.length; i += 1) {
122
+ const flag = flags.find((f) => rest[i] === f || rest[i].startsWith(`${f}=`));
123
+ if (!flag) continue;
124
+ found = true;
125
+ const raw = rest[i] === flag ? rest[i + 1] : rest[i].slice(flag.length + 1);
126
+ if (!raw || raw.startsWith("--")) return [];
127
+ out.push(...raw.split(",").map((s) => s.trim()).filter(Boolean));
128
+ }
129
+ return found ? [...new Set(out)] : undefined;
82
130
  }
83
131
 
84
132
  /**
@@ -168,17 +216,25 @@ export function packAddSource(rest: string[]): string | undefined {
168
216
  * a third would be the one that drifts.
169
217
  */
170
218
  function parseCliList(rest: string[]): { clis?: string[] } | { error: string[] } {
171
- const idx = rest.findIndex((a) => a === "--cli" || a.startsWith("--cli="));
172
- if (idx === -1) return {};
219
+ // Every `--cli`, not just the first: a repeat adds agents. One that names
220
+ // none still refuses the whole selection, below.
173
221
  const values: string[] = [];
174
- if (rest[idx].startsWith("--cli=")) {
175
- values.push(...rest[idx].slice("--cli=".length).split(","));
176
- } else {
177
- for (let j = idx + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
178
- values.push(...rest[j].split(","));
179
- }
222
+ let found = false;
223
+ let emptyOccurrence = false;
224
+ for (let i = 0; i < rest.length; i += 1) {
225
+ const before = values.length;
226
+ if (rest[i].startsWith("--cli=")) {
227
+ values.push(...rest[i].slice("--cli=".length).split(",").filter((v) => v.trim()));
228
+ } else if (rest[i] === "--cli") {
229
+ for (let j = i + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
230
+ values.push(...rest[j].split(",").filter((v) => v.trim()));
231
+ }
232
+ } else continue;
233
+ found = true;
234
+ if (values.length === before) emptyOccurrence = true;
180
235
  }
181
- const names = values.map((v) => v.trim()).filter(Boolean);
236
+ if (!found) return {};
237
+ const names = emptyOccurrence ? [] : [...new Set(values.map((v) => v.trim()))];
182
238
  // `--cli` with nothing after it is a typo, not a scope.
183
239
  //
184
240
  // It used to become `clis: []`, which `installed.json` stores verbatim and
@@ -238,7 +294,7 @@ function selectionFrom(rest: string[]): {
238
294
  } {
239
295
  // `--policy` reads right for one ("give me this policy"), `--only` for a set.
240
296
  // They are the same switch; taking both means neither is the wrong guess.
241
- const only = parseList(rest, "--policy") ?? parseList(rest, "--only");
297
+ const only = parseList(rest, "--policy", "--only");
242
298
  const categories = parseList(rest, "--category");
243
299
  const parsedClis = parseCliList(rest);
244
300
  if ("error" in parsedClis) return { error: parsedClis.error };
@@ -294,12 +350,27 @@ async function build(rest: string[]): Promise<PackCliResult> {
294
350
  const version = flag("version");
295
351
  const commit = flag("commit");
296
352
  const effect = flag("effect") ?? "enforce";
353
+ const minCliVersion = flag("min-cli-version");
297
354
  const outDir = resolve(flag("out") ?? "dist-pack");
355
+ // Where it will be installed from, which is what a machine judges a pack by
356
+ // (`isFirstPartyPack` reads `source`, never the id). `publish` passes its
357
+ // repository; `pack build` has only the id to go on.
358
+ const repo = flag("repo");
298
359
 
299
360
  if (!entry || !id || !version) {
300
361
  return fail([
301
362
  "Usage: failproofai publish <entry.mjs> --repo <owner>/<repo> --version <version>",
302
- " [--out <dir>] [--effect enforce|observe]",
363
+ " [--out <dir>] [--effect enforce|observe] [--min-cli-version <version>]",
364
+ ]);
365
+ }
366
+ // Refused here rather than written and refused by every machine that installs
367
+ // it. A `minCliVersion` nobody can order is a requirement nobody can check,
368
+ // and the loader treats one as grounds to refuse the whole pack — so a typo
369
+ // published here is a pack that installs nowhere.
370
+ if (minCliVersion !== undefined && parseSemver(minCliVersion) === null) {
371
+ return fail([
372
+ `--min-cli-version ${JSON.stringify(minCliVersion)} is not a version that can be compared.`,
373
+ "It must be plain semver — 1.0.7, or 1.0.7-beta.0 for a pre-release.",
303
374
  ]);
304
375
  }
305
376
  let identity: { id: string; version: string; effect: PolicyEffect; commit?: string };
@@ -315,24 +386,34 @@ async function build(rest: string[]): Promise<PackCliResult> {
315
386
  // Refused rather than bundled: see the note above. A relative specifier is the
316
387
  // only kind that would be rewritten by the loader and left outside the digest.
317
388
  const source = readFileSync(entryPath, "utf8");
318
- const localImport = /(?:^|\n)\s*(?:import|export)[^;\n]*from\s+["'](\.[^"']*)["']/.exec(source);
389
+ const localImport = firstRelativeSpecifier(source);
319
390
  if (localImport) {
320
391
  return fail([
321
- `${entryPath} imports ${localImport[1]}, and only the entry file is digest-pinned.`,
392
+ `${entryPath} imports ${localImport}, and only the entry file is digest-pinned.`,
322
393
  "Bundle it to a single file first (esbuild, bun build, rollup), then build the pack from that.",
323
394
  ]);
324
395
  }
325
396
 
326
397
  let hooks;
398
+ let semanticDeclarations: SemanticPolicyDeclaration[];
327
399
  try {
328
400
  hooks = await loadCustomHooks(entryPath, { strict: true });
401
+ // Read AFTER the load, from the same registration pass `clearCustomHooks`
402
+ // resets: a semantic policy has no `fn`, so it never travels back through
403
+ // `loadCustomHooks`'s return value.
404
+ semanticDeclarations = [...getSemanticRegistrations()];
329
405
  } catch (err) {
330
406
  return fail([`Could not load ${entryPath}: ${err instanceof Error ? err.message : String(err)}`]);
331
407
  }
332
- if (hooks.length === 0) {
408
+ // BOTH registries, or a pack that ships only Jev questions — a legitimate
409
+ // thing to publish, since the regex floor may already be somebody else's pack
410
+ // — is refused for registering nothing.
411
+ if (hooks.length === 0 && semanticDeclarations.length === 0) {
333
412
  return fail([
334
413
  `${entryPath} registered no policies.`,
335
- "A pack entry calls customPolicies.add({ name, description, match, fn }) for each one.",
414
+ "A pack entry calls customPolicies.add({ name, description, match, fn }) for each one,",
415
+ "and semanticPolicies.add({ name, title, appliesTo, mode, userCanOverride, probes, guidance })",
416
+ "for each Jev question set.",
336
417
  ]);
337
418
  }
338
419
 
@@ -359,6 +440,98 @@ async function build(rest: string[]): Promise<PackCliResult> {
359
440
  ]);
360
441
  }
361
442
 
443
+ // The pack's Jev question sets, validated with the LOADER's own rules — the
444
+ // same deal `parsePackPolicy` gets above, so a semantic policy that could
445
+ // never load fails here, where the author can fix it.
446
+ const semantic: SemanticManifestEntry[] = [];
447
+ const packSource = { source: `github:${repo ?? identity.id}` };
448
+ for (const [index, declaration] of semanticDeclarations.entries()) {
449
+ try {
450
+ const parsed = parsePackSemanticPolicy(identity.id, declaration, index);
451
+ // Same reason two regex policies cannot share a name, one layer over: the
452
+ // answer map is keyed `<policy>.<probe>`, so a duplicate would overwrite
453
+ // the first policy's questions in the compiled request.
454
+ if (semantic.some((s) => s.name === parsed.name)) {
455
+ throw new Error(`two semantic policies are called ${JSON.stringify(parsed.name)}`);
456
+ }
457
+ // The loader's own rule: a built-in check name from anyone but FailproofAI
458
+ // is void on every machine, never asked and never a reviewer.
459
+ if (isReservedClaim(packSource, parsed.name)) {
460
+ throw new Error(
461
+ `${JSON.stringify(parsed.name)} is a name reserved for FailproofAI's own Jev checks, so a pack from ` +
462
+ `${repo ?? identity.id} would never have it asked. Pick a name of your own.`,
463
+ );
464
+ }
465
+ semantic.push(parsed);
466
+ } catch (err) {
467
+ return fail([err instanceof Error ? err.message : String(err)]);
468
+ }
469
+ }
470
+
471
+ // A pack with Jev checks needs a build that runs them, so its minimum is at
472
+ // least the first one — written when the author gave none, refused when lower.
473
+ if (semantic.length > 0 && minCliVersion !== undefined && (compareVersions(minCliVersion, JEV_PACK_MIN_CLI) ?? 0) < 0) {
474
+ return fail([
475
+ `--min-cli-version ${minCliVersion} is older than the first failproofai that runs a pack's Jev checks (${JEV_PACK_MIN_CLI}):`,
476
+ "1.0.7 ignores them, and 1.0.7-beta.x replaces the built-in checks with them.",
477
+ `Pass --min-cli-version ${JEV_PACK_MIN_CLI} or newer, or leave it out and ${JEV_PACK_MIN_CLI} is written.`,
478
+ ]);
479
+ }
480
+ const requiredCli = semantic.length > 0 ? (minCliVersion ?? JEV_PACK_MIN_CLI) : minCliVersion;
481
+
482
+ // The question budget, checked against what one Jev request can carry. A pack
483
+ // over it installs and then has its overflow policies dropped one by one on
484
+ // the user's machine, in manifest order — a pack that enforces less than it
485
+ // says, which is the failure this whole lane is built to avoid.
486
+ //
487
+ // A pack from outside FailproofAI is ADDED to the built-in checks, which every
488
+ // machine spends the budget on first (`semanticPoliciesFromPacks`), so it gets
489
+ // only what they leave.
490
+ const questionCost = semantic.reduce((total, entry) => total + questionChars(entry), 0);
491
+ const firstParty = isFirstPartyPack(packSource);
492
+ const questionBudget = MAX_PACK_QUESTION_CHARS - (firstParty ? 0 : BUILTIN_QUESTION_CHARS);
493
+ if (questionCost > questionBudget) {
494
+ return fail([
495
+ `This pack's ${semantic.length} semantic policies compile to ${questionCost} characters of questions, ` +
496
+ (firstParty
497
+ ? `over the ${MAX_PACK_QUESTION_CHARS} one Jev request has room for.`
498
+ : `over the ${questionBudget} a machine leaves a pack from outside FailproofAI: one Jev request has room ` +
499
+ `for ${MAX_PACK_QUESTION_CHARS}, and the 16 built-in checks every machine asks take ${BUILTIN_QUESTION_CHARS} of it first.`),
500
+ "Shorten the probe instructions and criteria, or ship fewer policies per pack.",
501
+ ]);
502
+ }
503
+
504
+ // An authority declaration the manifest would lose, or that registration
505
+ // would quietly downgrade to hard, is refused HERE. Every machine that
506
+ // installs the pack would otherwise make the policy hard with only a warning
507
+ // in its own log — the author's decision, silently not taking effect — while
508
+ // at build time nothing is installed yet and refusing costs nobody anything.
509
+ // The same rule `scripts/build-policy-pack.mjs` applies to the core pack.
510
+ //
511
+ // Judged against the reviewers the PACK SHIPS WITH, not this build's: a pack
512
+ // that carries both tiers replaces the compiled semantic set on every machine
513
+ // that installs it, so `reviewedBy: ["its-own-check"]` is exactly right and
514
+ // the builtin list would call it a name "this build does not have". A pack
515
+ // with no semantic entries still answers to the builtin set, which is what its
516
+ // machines will be running.
517
+ const reviewers = semantic.length > 0 ? new Set(semantic.map((s) => s.name)) : undefined;
518
+ const authorityProblems = hooks.flatMap((hook) => {
519
+ const problem = authorityProblem({ authority: hook.authority, reviewedBy: hook.reviewedBy }, reviewers);
520
+ return problem ? [` ${hook.name}: ${problem}`] : [];
521
+ });
522
+ if (authorityProblems.length > 0) {
523
+ return fail([
524
+ authorityProblems.length === 1
525
+ ? "One policy declares an authority this build cannot publish:"
526
+ : `${authorityProblems.length} policies declare an authority this build cannot publish:`,
527
+ ...authorityProblems,
528
+ ...(reviewers
529
+ ? ["This pack declares Jev checks of its own, so reviewedBy may name only those."]
530
+ : []),
531
+ 'Fix the declaration, or leave authority out and the policy is "hard". See https://docs.befailproof.ai/policies/authority',
532
+ ]);
533
+ }
534
+
362
535
  const policies: unknown[] = [];
363
536
  for (const [index, hook] of hooks.entries()) {
364
537
  // `category` and `defaultEnabled` are pack-manifest fields a plain custom
@@ -367,13 +540,41 @@ async function build(rest: string[]): Promise<PackCliResult> {
367
540
  // FALSE: a pack's declared defaults are what `pack add` switches on with no
368
541
  // flags, and switching on a stranger's every policy unattended is the
369
542
  // installer opinion this lane already refused once.
370
- const extra = hook as unknown as { category?: unknown; defaultEnabled?: unknown };
543
+ const extra = hook as unknown as {
544
+ category?: unknown; defaultEnabled?: unknown; params?: unknown; alwaysOn?: unknown;
545
+ };
546
+ // `params` is read off the registration for the same reason, and leaving it
547
+ // out was not cosmetic: `registerPolicy` takes a pack policy's schema from the
548
+ // MANIFEST by name, so a pack published without it evaluates every one of its
549
+ // policies with `ctx.params = {}` — which discards the user's own configured
550
+ // values too, not just the defaults. Of the 39 builtins, 20 carry a schema:
551
+ // `prefer-package-manager` would have been inert, and `block-sudo`'s
552
+ // `allowPatterns`, `block-rm-rf`'s and `block-read-outside-cwd`'s `allowPaths`
553
+ // would all have stopped working — every one of them failing silently
554
+ // STRICTER, which is the direction users work around instead of reporting.
555
+ const paramsProblem = packParamsProblem(extra.params);
556
+ if (paramsProblem) {
557
+ return fail([
558
+ `${hook.name} declares a params schema this build cannot publish: ${paramsProblem}`,
559
+ "A machine reads the schema from the manifest, so an unusable one is a policy",
560
+ "that quietly runs without its parameters. Fix the declaration, or leave params out.",
561
+ ]);
562
+ }
371
563
  const candidate = {
372
564
  name: hook.name,
373
565
  description: hook.description ?? "",
374
566
  category: typeof extra.category === "string" && extra.category ? extra.category : "General",
375
567
  defaultEnabled: extra.defaultEnabled === true,
376
568
  match: hook.match ?? {},
569
+ ...(extra.params !== undefined ? { params: extra.params } : {}),
570
+ // Carried so parsePackPolicy refuses it as the loader would; left off, an
571
+ // alwaysOn registration published as an ordinary (even reviewable) policy.
572
+ ...("alwaysOn" in extra ? { alwaysOn: extra.alwaysOn } : {}),
573
+ // Whether Jev may clear this policy's verdict. A pack's MANIFEST is what
574
+ // a machine reads it from, so a declaration left on the registration
575
+ // alone would be published as nothing — silently hard. Checked above, so
576
+ // everything declared here is shape-valid; absent stays absent.
577
+ ...authorityFieldsOf(hook as unknown as Record<string, unknown>),
377
578
  };
378
579
  try {
379
580
  policies.push(parsePackPolicy(identity.id, candidate, index));
@@ -386,14 +587,21 @@ async function build(rest: string[]): Promise<PackCliResult> {
386
587
  // The manifest is hashed and the hash is the pin, so every byte in here is
387
588
  // part of what a machine verifies — a field carrying "there was nothing to
388
589
  // say" earns none of that cost. Readers already treat absence as ordinary.
590
+ //
591
+ // `minCliVersion` and `semantic` follow the same rule, and for `semantic` it is
592
+ // load-bearing rather than tidy: an EMPTY array would still be "a pack that
593
+ // declares semantic entries" to a careless reader, and the replacement rule
594
+ // turns that into "this pack replaced the compiled-in set with nothing".
389
595
  const manifest =
390
596
  JSON.stringify(
391
597
  {
392
598
  id: identity.id,
393
599
  version: identity.version,
600
+ ...(requiredCli ? { minCliVersion: requiredCli } : {}),
394
601
  effect: identity.effect,
395
602
  ...(identity.commit ? { commit: identity.commit } : {}),
396
603
  policies,
604
+ ...(semantic.length > 0 ? { semantic } : {}),
397
605
  },
398
606
  null,
399
607
  2,
@@ -411,15 +619,35 @@ async function build(rest: string[]): Promise<PackCliResult> {
411
619
  );
412
620
 
413
621
  const on = policies.filter((p) => (p as { defaultEnabled?: boolean }).defaultEnabled).length;
622
+ const semanticOnly = policies.length === 0 && semantic.length > 0;
414
623
  return ok([
415
624
  `Built ${identity.id}@${identity.version} — ${policies.length} policies, ${on} on by default.`,
625
+ // Said separately, and said at all, because this half changes what Jev asks
626
+ // on every machine that installs the pack. An author who did not mean to
627
+ // ship it should find that out here.
628
+ ...(semantic.length > 0
629
+ ? [
630
+ ` ${semantic.length} semantic ${semantic.length === 1 ? "policy" : "policies"} for Jev ` +
631
+ `(${questionCost} characters of questions), ` +
632
+ (identity.effect === "observe"
633
+ ? "not asked where it installs: Jev asks only the checks of packs that enforce."
634
+ : `${firstParty ? "replacing" : "added to"} the built-in checks where it installs.`),
635
+ ]
636
+ : []),
637
+ ...(requiredCli ? [` Requires failproofai ${requiredCli} or newer.`] : []),
638
+ ...(semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : []),
416
639
  ` ${outDir}/${PACK_MANIFEST_ASSET}`,
417
640
  ` ${outDir}/${PACK_ENTRY_ASSET}`,
418
641
  ` ${outDir}/${PACK_CHECKSUMS_ASSET}`,
419
642
  "",
420
643
  `Publish: attach all three to a GitHub release tagged ${identity.version}, then anyone runs:`,
421
644
  ` failproofai policies add <owner>/<repo>`,
422
- ], { policies: policies.length, defaultOn: on, ...(identity.commit ? { commit: identity.commit } : {}) });
645
+ ], {
646
+ policies: policies.length,
647
+ defaultOn: on,
648
+ ...(identity.commit ? { commit: identity.commit } : {}),
649
+ ...(semanticOnly ? { semanticOnly } : {}),
650
+ });
423
651
  }
424
652
 
425
653
  /**
@@ -999,7 +1227,13 @@ function inferTaggedVersion(entryPath: string, sources?: string[]): string | nul
999
1227
  * Identified by CONTENT, not by filename. A name convention would either miss
1000
1228
  * `guards.mjs` or match an unrelated `policies.mjs` that configures something
1001
1229
  * else entirely; a file that imports `failproofai` and calls
1002
- * `customPolicies.add` is a policy file whatever it is called.
1230
+ * `customPolicies.add` — or `semanticPolicies.add` — is a policy file whatever
1231
+ * it is called.
1232
+ *
1233
+ * Both registries count, and leaving one out was not a cosmetic gap: a file that
1234
+ * registers only Jev questions was undiscoverable, so `failproofai publish` in
1235
+ * its own directory reported no policy file at all rather than reaching the
1236
+ * "registered no policies" check and explaining itself.
1003
1237
  *
1004
1238
  * Returns ALL of them. Splitting policies across files is the normal thing to
1005
1239
  * do past about three, and they are one pack — so several files is an answer,
@@ -1008,8 +1242,11 @@ function inferTaggedVersion(entryPath: string, sources?: string[]): string | nul
1008
1242
  *
1009
1243
  * Non-recursive on purpose. Walking the tree finds fixtures, examples and
1010
1244
  * anything vendored, and publishing those is the failure this avoids.
1245
+ *
1246
+ * Takes the directory rather than reading `process.cwd()` so the discovery rule
1247
+ * can be tested against a real folder without a `chdir`.
1011
1248
  */
1012
- function findEntry(dir: string): string[] {
1249
+ export function findEntry(dir: string): string[] {
1013
1250
  let names: string[];
1014
1251
  try {
1015
1252
  names = readdirSync(dir);
@@ -1025,7 +1262,10 @@ function findEntry(dir: string): string[] {
1025
1262
  // would be the expensive half of this scan.
1026
1263
  if (statSync(full).size > 512 * 1024) continue;
1027
1264
  const text = readFileSync(full, "utf8");
1028
- if (/from\s+["']failproofai["']/.test(text) && /customPolicies\s*\.\s*add\s*\(/.test(text)) {
1265
+ if (
1266
+ /from\s+["']failproofai["']/.test(text) &&
1267
+ /(?:customPolicies|semanticPolicies)\s*\.\s*add\s*\(/.test(text)
1268
+ ) {
1029
1269
  found.push(full);
1030
1270
  }
1031
1271
  } catch {
@@ -1727,8 +1967,46 @@ function bundleEntry(
1727
1967
  */
1728
1968
  const PUBLISH_VALUE_FLAGS = new Set([
1729
1969
  "--repo", "--version", "--id", "--tag", "--notes", "--out", "--effect", "--entry", "--init",
1730
- "--commit",
1970
+ "--commit", "--min-cli-version",
1731
1971
  ]);
1972
+ /**
1973
+ * The first relative specifier an entry imports, or null.
1974
+ *
1975
+ * Two callers read this and they must agree, because between them they are the
1976
+ * only two outcomes: `publish` bundles a multi-file entry into the one artifact
1977
+ * a pack has to be, and `build` REFUSES one, since only the entry file is
1978
+ * digest-pinned. A shape that neither recognises gets bundled by neither and
1979
+ * refused by neither, and the author sees whatever the loader's rewrite makes of
1980
+ * it — which is an esbuild parse error naming a token, not a file.
1981
+ *
1982
+ * Both sites previously used `[^;\n]*` between `import` and `from`, which cannot
1983
+ * cross a newline and requires a `from` clause to exist. So both missed:
1984
+ *
1985
+ * import "./policies/regex" // side-effect only: there is no `from`
1986
+ * import {
1987
+ * aLongList,
1988
+ * } from "./shared" // the `from` is on another line
1989
+ *
1990
+ * and a pack entry written either way — the natural way to write one whose
1991
+ * halves register at module scope — failed to publish with `Expected "from" but
1992
+ * found "{"`. Now `[^;]*?` spans newlines and stops at a statement end, and the
1993
+ * bare form is matched on its own. Over-reaching to a later `from` is harmless:
1994
+ * the answer is only ever used as "is there a relative import, and name one".
1995
+ */
1996
+ export function firstRelativeSpecifier(source: string): string | null {
1997
+ const patterns = [
1998
+ /(?:^|\n)\s*(?:import|export)\b[^;]*?\bfrom\s*["'](\.[^"']*)["']/,
1999
+ /(?:^|\n)\s*import\s*["'](\.[^"']*)["']/,
2000
+ /\bimport\s*\(\s*["'](\.[^"']*)["']\s*\)/,
2001
+ /\brequire\s*\(\s*["'](\.[^"']*)["']\s*\)/,
2002
+ ];
2003
+ for (const re of patterns) {
2004
+ const m = re.exec(source);
2005
+ if (m) return m[1];
2006
+ }
2007
+ return null;
2008
+ }
2009
+
1732
2010
  function publishEntryArg(rest: string[]): string | undefined {
1733
2011
  const consumed = new Set<number>();
1734
2012
  for (let i = 0; i < rest.length; i += 1) {
@@ -2106,10 +2384,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2106
2384
  // a bundler for a step this tool already performs for its own pack.
2107
2385
  let entryToBuild = entry;
2108
2386
  const needsBundle =
2109
- discovered.length > 1 ||
2110
- /(?:^|\n)\s*(?:import|export)[^;\n]*from\s+["']\.[^"']*["']/.test(
2111
- readFileSync(entry, "utf8"),
2112
- );
2387
+ discovered.length > 1 || firstRelativeSpecifier(readFileSync(entry, "utf8")) !== null;
2113
2388
  if (needsBundle) {
2114
2389
  const sources = discovered.length > 1 ? discovered : [entry];
2115
2390
  const bundled = bundleEntry(sources, outDirEarly);
@@ -2120,6 +2395,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2120
2395
  const built = await build([
2121
2396
  entryToBuild,
2122
2397
  "--id", id,
2398
+ ...(repo ? ["--repo", repo] : []),
2123
2399
  "--version", version,
2124
2400
  // NOT when the tree is dirty, even though `--version` let the publish
2125
2401
  // through. `commit` claims these bytes came from that commit, and on a
@@ -2129,6 +2405,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2129
2405
  ...(provenance && !provenance.dirty ? ["--commit", provenance.sha] : []),
2130
2406
  ...outFlagFrom(rest),
2131
2407
  ...effectFlagFrom(rest),
2408
+ ...minCliVersionFlagFrom(rest),
2132
2409
  ]);
2133
2410
  if (built.exitCode !== 0) return built;
2134
2411
 
@@ -2143,7 +2420,9 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2143
2420
  ...gitLines,
2144
2421
  ...(gitLines.length ? [""] : []),
2145
2422
  ...bundleNote,
2146
- ...built.lines.slice(0, 4),
2423
+ // Everything above build's blank line: a fixed count cut the rollback
2424
+ // reminder mid-sentence and dropped the asset paths.
2425
+ ...built.lines.slice(0, built.lines.indexOf("")),
2147
2426
  "",
2148
2427
  ...(repo
2149
2428
  ? ["Dry run — nothing was published.", `Drop --dry-run to release it on ${repo}.`]
@@ -2259,6 +2538,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2259
2538
  `Published ${id}@${version} to ${repo} at tag ${tag}.` +
2260
2539
  (versionFromSha ? " That names the commit it was built from." : ""),
2261
2540
  ` ${assets.length} assets attached`,
2541
+ ...(built.meta?.semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : []),
2262
2542
  "",
2263
2543
  // The install lines are the whole point of the success message, and on a
2264
2544
  // private repository they are a lie — `policies add` 404s there. Print the
@@ -2292,6 +2572,18 @@ function effectFlagFrom(rest: string[]): string[] {
2292
2572
  if (i === -1) return [];
2293
2573
  return rest[i].includes("=") ? [rest[i]] : [rest[i], rest[i + 1]];
2294
2574
  }
2575
+ /**
2576
+ * Same again for `--min-cli-version`, and it has to be forwarded explicitly
2577
+ * because `publish` hands `build` a list it assembles rather than its own
2578
+ * `rest`. Left off, the flag parsed, validated, refused an uncomparable value —
2579
+ * and then never reached the manifest, so the field the whole ordering
2580
+ * constraint rests on was silently absent from the published artifact.
2581
+ */
2582
+ function minCliVersionFlagFrom(rest: string[]): string[] {
2583
+ const i = rest.findIndex((a) => a === "--min-cli-version" || a.startsWith("--min-cli-version="));
2584
+ if (i === -1) return [];
2585
+ return rest[i].includes("=") ? [rest[i]] : [rest[i], rest[i + 1]];
2586
+ }
2295
2587
 
2296
2588
  /** `failproofai publish` — exported for bin/failproofai.mjs and the tests. */
2297
2589
  export async function runPublishCommand(rest: string[]): Promise<PackCliResult> {
@@ -2345,11 +2637,65 @@ async function pickClis(
2345
2637
  * Returns null when the user cancelled, which must not be confused with an
2346
2638
  * empty selection: one means "install nothing", the other means "do nothing".
2347
2639
  */
2640
+ /**
2641
+ * How a pack's semantic half is named to a person: "3 Jev checks", never "3
2642
+ * semantic policies".
2643
+ *
2644
+ * They are not policies in the sense every other number on these screens uses —
2645
+ * nothing switches them on or off, `--policy` cannot name one, and they do not
2646
+ * appear in `failproofai policies`. Calling them policies would make every count
2647
+ * beside them read as the same kind of thing.
2648
+ */
2649
+ function semanticPhrase(count: number): string {
2650
+ return `${count} Jev ${count === 1 ? "check" : "checks"}`;
2651
+ }
2652
+
2653
+ /**
2654
+ * How a pack's Jev checks sit beside this build's, for add, show and the
2655
+ * picker alike: a FailproofAI pack's replace them, anyone else's are added
2656
+ * (`replacesBuiltinChecks`), and only where `jevPacks` lets the pack take part —
2657
+ * never for an observe pack, only for its agents when scoped. One phrase, so
2658
+ * the three cannot drift apart again.
2659
+ */
2660
+ function besideBuiltinChecks(pack: { source?: string; effect?: PolicyEffect; clis?: string[] | null }): string {
2661
+ if (jevPacks([{ effect: pack.effect ?? "enforce", clis: null }]).length === 0) {
2662
+ return "not asked: this pack only observes, and Jev asks only the checks of packs that enforce";
2663
+ }
2664
+ const how = isFirstPartyPack(pack) ? "replacing this build's own set" : "added to this build's own checks";
2665
+ return pack.clis && pack.clis.length > 0 ? `${how}, for ${pack.clis.join(", ")} only` : how;
2666
+ }
2667
+
2668
+ /**
2669
+ * Which of a pack's checks the resolver would leave out beside what is
2670
+ * installed (the shared budget, a reserved or contested name). `preview` is a
2671
+ * pack not installed yet, judged as if it were. A diagnostic: never throws.
2672
+ */
2673
+ function jevCheckWarnings(id: string, preview?: PackPreview): string[] {
2674
+ try {
2675
+ const installed = readInstalledPacks().packs.filter((p) => p.id !== preview?.id);
2676
+ const packs = preview ? [...installed, { ...preview, clis: null }] : installed;
2677
+ return semanticPoliciesFromPacks(jevPacks(packs)).errors.filter((e) => e.includes(id));
2678
+ } catch {
2679
+ return [];
2680
+ }
2681
+ }
2682
+
2348
2683
  async function pickFromSource(
2349
2684
  source: string,
2350
2685
  io: { stdin: TTYIn; stdout: TTYOut },
2351
2686
  ): Promise<string[] | null> {
2352
2687
  const preview = await fetchPackPreview(source);
2688
+ // Nothing to pick. A pack of Jev questions alone has no selectable policies —
2689
+ // its semantic set is not switchable per entry — so the picker would draw an
2690
+ // empty list and ask a question with no answers. `[]` rather than `null`: the
2691
+ // user cancelled nothing, there was simply nothing to choose.
2692
+ if (preview.policies.length === 0) {
2693
+ io.stdout.write(
2694
+ `\n ${preview.id}@${preview.version} carries no selectable policies` +
2695
+ `${preview.semantic.length > 0 ? `, only ${semanticPhrase(preview.semantic.length)}` : ""}.\n\n`,
2696
+ );
2697
+ return [];
2698
+ }
2353
2699
  const categories = [...new Set(preview.policies.map((p) => p.category))];
2354
2700
  const rows: MultiChoice<string>[] = [];
2355
2701
  for (const category of categories) {
@@ -2364,6 +2710,15 @@ async function pickFromSource(
2364
2710
  }
2365
2711
  }
2366
2712
  const on = preview.policies.filter((p) => p.defaultEnabled).length;
2713
+ // Said BEFORE the list, because the list cannot say it: a pack's Jev checks are
2714
+ // not rows here, and they arrive whole. Somebody ticking boxes should know
2715
+ // that is part of what they are agreeing to.
2716
+ if (preview.semantic.length > 0) {
2717
+ io.stdout.write(
2718
+ `\n This pack also brings ${semanticPhrase(preview.semantic.length)} (not selectable), ` +
2719
+ `${besideBuiltinChecks(preview)}.\n See: failproofai policies show ${source}\n\n`,
2720
+ );
2721
+ }
2367
2722
  const picked = await multiSelect<string>({
2368
2723
  message: `${preview.id}@${preview.version} — which of these should be on?`,
2369
2724
  choices: rows,
@@ -2463,7 +2818,7 @@ async function add(rest: string[]): Promise<PackCliResult> {
2463
2818
  ];
2464
2819
  const skipped = result.available.filter((n) => !result.enabled.includes(n));
2465
2820
 
2466
- // Say WHY this set is on. Without it, "10 of 38 enabled" reads like
2821
+ // Say WHY this set is on. Without it, "10 of 39 enabled" reads like
2467
2822
  // something went wrong rather than like the pack's own recommendation.
2468
2823
  const why = {
2469
2824
  defaults: "the pack's defaults",
@@ -2480,9 +2835,34 @@ async function add(rest: string[]): Promise<PackCliResult> {
2480
2835
  // rendered as if something had gone missing.
2481
2836
  lines.push(
2482
2837
  result.enabled.length === 0
2483
- ? ` enabled (0/${result.available.length}, ${why}): none — the pack is installed and enforcing nothing`
2838
+ ? // "enforcing nothing" is only true when the pack brought nothing else.
2839
+ // A pack of Jev checks alone enforces through the semantic tier with no
2840
+ // regex policy enabled at all, and telling that user their install does
2841
+ // nothing is worse than saying nothing.
2842
+ ` enabled (0/${result.available.length}, ${why}): none` +
2843
+ (result.semantic > 0 ? "" : " — the pack is installed and enforcing nothing")
2484
2844
  : ` enabled (${result.enabled.length}/${result.available.length}, ${why}): ${summarise(result.enabled)}`,
2485
2845
  );
2846
+ // Counted separately, and without an on/off ratio, because there is none:
2847
+ // a pack's Jev checks arrive whole, and a FailproofAI pack's REPLACE the ones
2848
+ // this build ships (anyone else's are added to them).
2849
+ // Silence here was the shape of the original bug — the manifest's semantic
2850
+ // half was fetched, verified and then never written to `installed.json`, so
2851
+ // the half somebody installed did nothing and nothing said so.
2852
+ if (result.semantic > 0) {
2853
+ lines.push(
2854
+ ` ${semanticPhrase(result.semantic)}, ${besideBuiltinChecks(result)}. ` +
2855
+ "They apply only where you configured Jev (`failproofai jev status`).",
2856
+ );
2857
+ }
2858
+ // Nothing after this screen says it: the record keeps no unreadable minimum.
2859
+ if (result.minCliVersionNote) lines.push(` ${result.minCliVersionNote}`);
2860
+ // Which of its checks the resolver will leave out beside what is already
2861
+ // installed (the shared question budget, a reserved or contested name).
2862
+ // Otherwise said only in the hook log, on the first call that asks Jev.
2863
+ if (result.semantic > 0) {
2864
+ for (const e of jevCheckWarnings(result.id)) lines.push(` ▲ ${e}`);
2865
+ }
2486
2866
 
2487
2867
  if (skipped.length > 0) {
2488
2868
  lines.push(` not enabled (${skipped.length}): ${summarise(skipped)}`);
@@ -2741,6 +3121,149 @@ function relativeAge(iso: string): string {
2741
3121
  return `${value} ${label}${value === 1 ? "" : "s"} ago`;
2742
3122
  }
2743
3123
 
3124
+ /**
3125
+ * Which of a pack's OWN regex policies each of its Jev checks can clear, keyed
3126
+ * by check name and empty for a check nothing names.
3127
+ *
3128
+ * INVERTED out of the same manifest's `reviewedBy` rather than declared beside
3129
+ * the checks. A semantic entry says nothing about the policies that name it, so
3130
+ * the only other way to show this column is a second list kept by hand — which
3131
+ * is wrong the first time a publisher adds a reviewer to one policy, and wrong
3132
+ * in the direction that overstates what Jev may clear.
3133
+ *
3134
+ * Judged with `resolvePolicyAuthority` against the pack's OWN check names,
3135
+ * because that is the rule the installing machine applies: a pack declaring
3136
+ * semantic entries replaces the compiled-in set (`effectiveReviewerNames`), so
3137
+ * its `reviewedBy` may name only its own checks, and a declaration that names
3138
+ * anything else — or one sitting under `authority: "hard"`, which a manifest may
3139
+ * carry because the two fields are parsed independently — registers HARD. A
3140
+ * policy in either state can never be cleared, so naming it under a check would
3141
+ * promise a clear that cannot happen.
3142
+ */
3143
+ function reviewersByCheck(
3144
+ policies: ReadonlyArray<PolicyCatalogEntry>,
3145
+ semantic: ReadonlyArray<SemanticManifestEntry>,
3146
+ ): Map<string, string[]> {
3147
+ const ownChecks = new Set(semantic.map((s) => s.name));
3148
+ const byCheck = new Map<string, string[]>(semantic.map((s) => [s.name, []]));
3149
+ for (const policy of policies) {
3150
+ const { authority, reviewedBy } = resolvePolicyAuthority(policy, ownChecks);
3151
+ if (authority !== "reviewable") continue;
3152
+ for (const check of reviewedBy ?? []) byCheck.get(check)?.push(policy.name);
3153
+ }
3154
+ return byCheck;
3155
+ }
3156
+
3157
+ /** Reviewers named in full. Past this the cell names two and counts the rest. */
3158
+ const REVIEWERS_LISTED = 3;
3159
+
3160
+ /**
3161
+ * A check's reviewers as one cell.
3162
+ *
3163
+ * The question a reader brings to this column is "can this check clear anything
3164
+ * of mine", which two names and a count answer as well as ten do — and ten
3165
+ * names in a cell push the column past the terminal, where `table` cuts one of
3166
+ * them mid-word and the row stops being readable at all.
3167
+ */
3168
+ function reviewersCell(names: ReadonlyArray<string>): string {
3169
+ if (names.length <= REVIEWERS_LISTED) return names.join(", ");
3170
+ return `${names.slice(0, 2).join(", ")}, +${names.length - 2}`;
3171
+ }
3172
+
3173
+ /**
3174
+ * Why a check that no policy names is in the pack at all — and the two answers
3175
+ * are different facts, so the row has to tell them apart.
3176
+ *
3177
+ * A `deny` check with no reviewer is a concern the pack's regex half does not
3178
+ * cover: it can only ever ADD a deny, never clear one, and that is a gap in the
3179
+ * regex tier rather than a mistake in the manifest. An `instruct` check can
3180
+ * never answer deny, so every conjunction naming it has one reachable outcome —
3181
+ * a clear — which is why nothing names it and why nothing should (the
3182
+ * `block-work-on-main` entry in CHANGELOG 1.0.7-beta.1 is that bug, shipped).
3183
+ */
3184
+ function unreviewedCell(mode: SemanticManifestEntry["mode"], policies: number): string {
3185
+ if (mode === "instruct") return "instruct-only — it can never deny";
3186
+ // "the 38" borrows the count from the heading above, where it has just been
3187
+ // read. The other two spellings exist because that sentence has no grammatical
3188
+ // form at one or zero — and a pack of Jev checks alone, with no regex half at
3189
+ // all, is a legitimate thing to publish.
3190
+ if (policies === 0) return "this pack has no policies to clear";
3191
+ if (policies === 1) return "the one policy here does not cover this";
3192
+ return `nothing in the ${policies} covers this`;
3193
+ }
3194
+
3195
+ /**
3196
+ * A pack's Jev checks as a section of rows — the shape the category sections
3197
+ * use, deliberately NOT their columns.
3198
+ *
3199
+ * It replaces a prose paragraph that listed sixteen names comma-separated above
3200
+ * a screen where everything else was a scannable row. The rows are not policy
3201
+ * rows and must not read as ones: there is no on/off chip, because nothing
3202
+ * toggles a check, `--policy` cannot name one, and none of them appears in
3203
+ * `failproofai policies`. What the columns carry instead is the pair of facts
3204
+ * that decide what a check does on a real machine — its mode, and which of this
3205
+ * pack's own policies it can clear.
3206
+ *
3207
+ * Null when the pack declares none, so a pack with no semantic half prints no
3208
+ * section rather than an empty one.
3209
+ */
3210
+ export function jevChecksSection(
3211
+ pack: {
3212
+ policies: ReadonlyArray<PolicyCatalogEntry>;
3213
+ semantic: ReadonlyArray<SemanticManifestEntry>;
3214
+ /** Where it is (or would be) installed from, which decides replace or add. */
3215
+ source?: string;
3216
+ },
3217
+ opts?: RenderOpts,
3218
+ ): string[] | null {
3219
+ if (pack.semantic.length === 0) return null;
3220
+ const reviewers = reviewersByCheck(pack.policies, pack.semantic);
3221
+ const rows = pack.semantic.map((check) => {
3222
+ const named = reviewers.get(check.name) ?? [];
3223
+ // `reviews` and `—` lead the same cell rather than sitting in a column of
3224
+ // their own. A fourth column would pad every `—` out to the width of
3225
+ // `reviews`, which costs nine characters of the only column allowed to
3226
+ // shrink — enough to ellipsize the reason on an 80-column terminal, where
3227
+ // this way everything fits whole.
3228
+ return [
3229
+ check.mode,
3230
+ check.name,
3231
+ named.length > 0
3232
+ ? `reviews ${reviewersCell(named)}`
3233
+ : `— ${unreviewedCell(check.mode, pack.policies.length)}`,
3234
+ ];
3235
+ });
3236
+ return [
3237
+ // The count is on the heading rather than in a row, the way every other
3238
+ // section on this screen carries its own total.
3239
+ ...rule(
3240
+ `Jev checks — ${pack.semantic.length} · not selectable · only where Jev is configured`,
3241
+ opts,
3242
+ ),
3243
+ // The reviewers column is the only one allowed to shrink: a mode or a check
3244
+ // name cut in half is a fact nobody can act on, while a cut reviewer list is
3245
+ // one that was already abbreviated.
3246
+ ...table({ head: ["", "", ""], rows, protect: [0, 1] }, opts),
3247
+ "",
3248
+ ...note(
3249
+ "Nothing toggles them: `--policy` cannot name one and `failproofai policies` never lists them. " +
3250
+ `They arrive whole, ${besideBuiltinChecks(pack)}.`,
3251
+ opts,
3252
+ ),
3253
+ ...note(
3254
+ // A pack of Jev checks alone has no regex half for the column to be about,
3255
+ // so the sentence says what IS true of it rather than "its own 0 policies".
3256
+ pack.policies.length === 0
3257
+ ? "This pack ships no regex policies, so none of these checks clears anything here: a deny " +
3258
+ "check can only ADD a deny, and an instruct check only ever warns."
3259
+ : `\`reviews\` is which of this pack's own ${pack.policies.length} policies each check can clear. ` +
3260
+ "A deny check that reviews nothing can only ever ADD a deny; an instruct check never answers " +
3261
+ "deny, so nothing pairs with one.",
3262
+ opts,
3263
+ ),
3264
+ ];
3265
+ }
3266
+
2744
3267
  async function listRemote(source: string): Promise<PackCliResult> {
2745
3268
  const opts = optsFor(process.stdout);
2746
3269
  let preview;
@@ -2774,7 +3297,8 @@ async function listRemote(source: string): Promise<PackCliResult> {
2774
3297
  stack(
2775
3298
  title(
2776
3299
  `${preview.id}@${preview.version}`,
2777
- `${preview.policies.length} policies · ${categories.length} categories`,
3300
+ `${preview.policies.length} policies · ${categories.length} categories` +
3301
+ (preview.semantic.length > 0 ? ` · ${semanticPhrase(preview.semantic.length)}` : ""),
2778
3302
  opts,
2779
3303
  ),
2780
3304
  note(
@@ -2782,8 +3306,15 @@ async function listRemote(source: string): Promise<PackCliResult> {
2782
3306
  (preview.effect === "observe" ? " This pack OBSERVES — it records and blocks nothing." : ""),
2783
3307
  opts,
2784
3308
  ),
3309
+ preview.minCliVersion ? note(`Requires failproofai ${preview.minCliVersion} or newer.`, opts) : null,
3310
+ preview.minCliVersionNote ? note(preview.minCliVersionNote, opts) : null,
2785
3311
  preview.resolvedFromLatest ? note(`Newest release: ${preview.source}`, opts) : null,
2786
3312
  table({ head: ["", "", ""], rows }, opts),
3313
+ // The half the table above cannot hold: its own section, under the
3314
+ // policies rather than above them, because a check is read against the
3315
+ // policies it can clear and those are the rows just passed.
3316
+ jevChecksSection(preview, opts),
3317
+ preview.semantic.length > 0 ? jevCheckWarnings(preview.id, preview).flatMap((e) => warning([e], opts)) : null,
2787
3318
  nextStep(`failproofai policies add ${source}`, "Install the defaults with:", opts),
2788
3319
  note("Or take part of it: --policy <a,b>, --category <x,y>, --all", opts),
2789
3320
  ),