failproofai 1.0.8-beta.0 → 1.0.9-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 (163) 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.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +15 -15
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +3 -3
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  59. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__14o3ek1._.js → [root-of-the-server]__0tmz862._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/{_0o4xkpl._.js → _0n0lja6._.js} +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{_1-7sqrb._.js → _1p3iz2d._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +3 -3
  82. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +1 -1
  90. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  91. package/.next/standalone/.next/server/pages/404.html +1 -1
  92. package/.next/standalone/.next/server/pages/500.html +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  94. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  95. package/.next/standalone/.next/static/chunks/{2z7i-yg59w4pn.js → 0247hc9ath5t-.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0ahfwmbkpgfw4.js → 03001k9jy-kst.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2vo7qbdbvnc0o.js → 06k7gsfnb7jkh.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/0qejem4-zt-hy.js +1 -0
  99. package/.next/standalone/.next/static/chunks/{1v_tp3hm8wyhe.js → 0snylg3y1i47a.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/1h0if8ky362as.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{3bgot5v6f6s3c.js → 2qsrrrl2maqsb.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{0bhidk90e-07f.css → 3_e_wg5_oay87.css} +1 -1
  103. package/.next/standalone/.next/static/chunks/3ns-egfh8i8i5.js +6 -0
  104. package/.next/standalone/.next/static/chunks/3rq7fjj9we0v3.js +1 -0
  105. package/.next/standalone/.next/static/chunks/42m0f2hh8vfb1.js +1 -0
  106. package/.next/standalone/app/actions/get-jev-config.ts +2 -2
  107. package/.next/standalone/app/actions/update-jev-config.ts +12 -9
  108. package/.next/standalone/app/components/jev-notices.tsx +10 -10
  109. package/.next/standalone/app/settings/jev-panel.tsx +11 -11
  110. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +2 -0
  111. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +26 -0
  112. package/.next/standalone/fp-cloud-cli/tests/test_output.py +27 -0
  113. package/.next/standalone/hermes-plugin/README.md +18 -8
  114. package/.next/standalone/package.json +10 -10
  115. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  116. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  117. package/.next/standalone/server.js +1 -1
  118. package/bin/failproofai.mjs +33 -7
  119. package/dist/cli.mjs +664 -553
  120. package/dist/worker.mjs +164 -407
  121. package/hermes-plugin/README.md +18 -8
  122. package/package.json +10 -10
  123. package/scripts/build-policy-pack.mjs +6 -9
  124. package/src/hooks/cloud-connection.ts +12 -6
  125. package/src/hooks/custom-hooks-loader.ts +1 -1
  126. package/src/hooks/effective-reviewers.ts +50 -43
  127. package/src/hooks/handler.ts +25 -16
  128. package/src/hooks/hermes-update.ts +74 -0
  129. package/src/hooks/hook-activity-store.ts +3 -3
  130. package/src/hooks/integrations.ts +349 -55
  131. package/src/hooks/jev-activity.ts +3 -3
  132. package/src/hooks/jev-cli.ts +17 -17
  133. package/src/hooks/jev-cloud-connection.ts +4 -4
  134. package/src/hooks/manager.ts +3 -2
  135. package/src/hooks/pack-cli.ts +24 -23
  136. package/src/hooks/pack-manifest.ts +25 -2
  137. package/src/hooks/pack-store.ts +2 -2
  138. package/src/hooks/policy-authority.ts +51 -32
  139. package/src/hooks/policy-evaluator.ts +5 -5
  140. package/src/hooks/policy-registry.ts +5 -7
  141. package/src/hooks/policy-reviewability.ts +26 -10
  142. package/src/hooks/policy-types.ts +4 -4
  143. package/src/hooks/semantic/combine.ts +13 -13
  144. package/src/hooks/semantic/decide.ts +134 -29
  145. package/src/hooks/semantic/evaluator.ts +5 -5
  146. package/src/hooks/semantic/facts.ts +91 -2
  147. package/src/hooks/semantic/jev-client.ts +1 -1
  148. package/src/hooks/semantic/jev-config.ts +21 -12
  149. package/src/hooks/semantic/jev-review.ts +3 -3
  150. package/src/hooks/semantic/jev-stats.ts +17 -17
  151. package/src/hooks/semantic/pack-policies.ts +48 -48
  152. package/src/hooks/semantic/policies.ts +13 -425
  153. package/src/hooks/semantic/precondition-names.ts +1 -1
  154. package/src/hooks/semantic/preconditions.ts +1 -1
  155. package/src/hooks/semantic/types.ts +7 -1
  156. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +0 -1
  157. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +0 -1
  158. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +0 -1
  159. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +0 -1
  160. package/.next/standalone/.next/static/chunks/3o3f1ibfci0p7.js +0 -6
  161. /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → z1C0kJVF0NCtWn5R88hW3}/_buildManifest.js +0 -0
  162. /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → z1C0kJVF0NCtWn5R88hW3}/_clientMiddlewareManifest.js +0 -0
  163. /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → z1C0kJVF0NCtWn5R88hW3}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -29,7 +29,7 @@ var __esm = (fn, res, err) => () => {
29
29
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
30
 
31
31
  // package.json
32
- var version2 = "1.0.8-beta.0", package_default;
32
+ var version2 = "1.0.9-beta.0", package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "failproofai",
@@ -149,22 +149,22 @@ var init_package = __esm(() => {
149
149
  postcss: "8.5.26",
150
150
  "eslint-plugin-react-hooks": "7.0.1",
151
151
  vite: "8.0.16",
152
- undici: "7.29.0",
152
+ undici: "7.29.1",
153
153
  "brace-expansion": "5.0.9",
154
154
  sharp: "0.35.4",
155
155
  browserslist: "4.28.8"
156
156
  },
157
157
  optionalDependencies: {
158
- "@failproofai/failproofaid-linux-x64": "1.0.8-beta.0",
159
- "@failproofai/failproofaid-linux-arm64": "1.0.8-beta.0",
160
- "@failproofai/failproofaid-darwin-x64": "1.0.8-beta.0",
161
- "@failproofai/failproofaid-darwin-arm64": "1.0.8-beta.0"
158
+ "@failproofai/failproofaid-linux-x64": "1.0.9-beta.0",
159
+ "@failproofai/failproofaid-linux-arm64": "1.0.9-beta.0",
160
+ "@failproofai/failproofaid-darwin-x64": "1.0.9-beta.0",
161
+ "@failproofai/failproofaid-darwin-arm64": "1.0.9-beta.0"
162
162
  },
163
163
  failproofaidBinaries: {
164
- "linux-x64": "35b641f60e1724513fbb760db382123dad2b5fcdd67dda6373bda80651bd36bb",
165
- "linux-arm64": "a058288f5d08f99612f053793e5c7f59264d3da97f3dbe769e401b0f8536702d",
166
- "darwin-x64": "b4f3dbe4aea44e77cc10839647c0dc03a1b8b797dfb27e1d89a6a96554ef27b8",
167
- "darwin-arm64": "fd42f28bf92001d39117b488a215578b966f07d316214744aca64a1888f2aed2"
164
+ "linux-x64": "044b6619007227b8aa1fc1095b8c61a17bd90e11da53183ac1f5adecdcae6839",
165
+ "linux-arm64": "479450d7d15c52a59bd1a8cf4c7086be9210e8d31ad4f98d99b123f571e944bd",
166
+ "darwin-x64": "11562912ff726195bfb966ed24a774ffea38d01acc3edc1f9bfa3ea9c298977f",
167
+ "darwin-arm64": "5bbaee1ec89d2d8032b7f16e502fb96891fbc6aa2ee6c861f43a64a0a60e61ba"
168
168
  }
169
169
  };
170
170
  });
@@ -875,7 +875,7 @@ var init_jev_activity = __esm(() => {
875
875
  ];
876
876
  EVALUATORS = new Set(["jev", "jev-fallback"]);
877
877
  DECISIONS = new Set(["allow", "instruct", "deny"]);
878
- MODES = new Set(["shadow", "enforce"]);
878
+ MODES = new Set(["observe", "enforce"]);
879
879
  REASON_HEAD_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\s*(?:$|[:(])/;
880
880
  HTTP_CODE_RE = /^http-\d{3}$/;
881
881
  JEV_REASON_CODE_LIST = [
@@ -2518,7 +2518,7 @@ function effectiveAuthority(p) {
2518
2518
 
2519
2519
  // src/hooks/policy-authority.ts
2520
2520
  import { existsSync as existsSync4 } from "node:fs";
2521
- function resolvePolicyAuthority(decl, knownReviewers = SEMANTIC_REVIEWER_NAMES, contested) {
2521
+ function resolvePolicyAuthority(decl, knownReviewers = NO_REVIEWERS, contested) {
2522
2522
  const declaredReviewable = decl?.authority === "reviewable";
2523
2523
  if (!decl || effectiveAuthority(decl) === "hard") {
2524
2524
  if (!declaredReviewable)
@@ -2530,6 +2530,8 @@ function resolvePolicyAuthority(decl, knownReviewers = SEMANTIC_REVIEWER_NAMES,
2530
2530
  return { authority: "hard", downgraded: "reviewedBy is not a list of semantic policy names" };
2531
2531
  }
2532
2532
  const unknown = names.filter((n) => !knownReviewers.has(n));
2533
+ if (knownReviewers.size === 0 && !unknown.some((n) => contested?.has(n)))
2534
+ return { authority: "hard" };
2533
2535
  if (unknown.length > 0) {
2534
2536
  return { authority: "hard", downgraded: `reviewedBy names ${unknown.map((n) => whyUnknown(n, knownReviewers, contested)).join("; ")}` };
2535
2537
  }
@@ -2575,7 +2577,7 @@ function authorityProblem(decl, knownReviewers = SEMANTIC_REVIEWER_NAMES) {
2575
2577
  const { downgraded } = resolvePolicyAuthority(decl, knownReviewers);
2576
2578
  return downgraded ? `authority "reviewable" was refused — ${downgraded}` : undefined;
2577
2579
  }
2578
- function withMergedAuthority(record, declarations, knownReviewers = SEMANTIC_REVIEWER_NAMES) {
2580
+ function withMergedAuthority(record, declarations, knownReviewers = NO_REVIEWERS) {
2579
2581
  const resolved = declarations.map((d) => resolvePolicyAuthority(d, knownReviewers));
2580
2582
  const allReviewable = resolved.length > 0 && resolved.every((r) => r.authority === "reviewable");
2581
2583
  const decided = declarations[resolved.findIndex((r) => r.authority !== "reviewable")] ?? {};
@@ -2614,7 +2616,7 @@ function authorityDeclarationFor(hook, source) {
2614
2616
  }
2615
2617
  return { authority: hook.authority, reviewedBy: hook.reviewedBy };
2616
2618
  }
2617
- var SEMANTIC_POLICY_NAMES, SEMANTIC_REVIEWER_NAMES, isName = (n) => typeof n === "string" && n.length > 0, warnedAuthority;
2619
+ var SEMANTIC_POLICY_NAMES, SEMANTIC_REVIEWER_NAMES, NO_REVIEWERS, isName = (n) => typeof n === "string" && n.length > 0, warnedAuthority;
2618
2620
  var init_policy_authority = __esm(() => {
2619
2621
  init_fp_home();
2620
2622
  init_hook_logger();
@@ -2637,6 +2639,7 @@ var init_policy_authority = __esm(() => {
2637
2639
  "external-data-egress"
2638
2640
  ];
2639
2641
  SEMANTIC_REVIEWER_NAMES = new Set(SEMANTIC_POLICY_NAMES);
2642
+ NO_REVIEWERS = new Set;
2640
2643
  warnedAuthority = new Set;
2641
2644
  });
2642
2645
 
@@ -2849,6 +2852,16 @@ function hasInstalledPacks() {
2849
2852
  return false;
2850
2853
  }
2851
2854
  }
2855
+ function hasInstalledRegexPacks() {
2856
+ try {
2857
+ const raw = JSON.parse(readFileSync4(installedFilePath(), "utf8"));
2858
+ if (!Array.isArray(raw.packs))
2859
+ return false;
2860
+ return raw.packs.some((p) => !!p && typeof p === "object" && Array.isArray(p.policies) && p.policies.length > 0);
2861
+ } catch {
2862
+ return false;
2863
+ }
2864
+ }
2852
2865
  function installedFilePath() {
2853
2866
  return process.env.FAILPROOFAI_PACK_DIR ? resolve4(process.env.FAILPROOFAI_PACK_DIR, "installed.json") : packsInstalledFile();
2854
2867
  }
@@ -3267,7 +3280,7 @@ function isReservedClaim(pack, name) {
3267
3280
  function effectiveReviewerNames() {
3268
3281
  if (cached)
3269
3282
  return cached;
3270
- let names = SEMANTIC_REVIEWER_NAMES;
3283
+ let names = NO_REVIEWERS;
3271
3284
  cachedContested = new Map;
3272
3285
  try {
3273
3286
  const packs = jevPacks(readInstalledPacks().packs, reviewerCli);
@@ -3281,15 +3294,22 @@ function contestedReviewerNames() {
3281
3294
  effectiveReviewerNames();
3282
3295
  return cachedContested;
3283
3296
  }
3284
- function replacesBuiltinChecks(packs) {
3285
- return packs.some((p) => isFirstPartyPack(p) && (p.semantic ?? []).length > 0);
3297
+ function jevChecksInstalled() {
3298
+ return effectiveReviewerNames().size > 0;
3299
+ }
3300
+ function jevChecksDeclared() {
3301
+ try {
3302
+ return reviewerNamesFor(jevPacks(readInstalledPacks().packs)).size > 0;
3303
+ } catch {
3304
+ return false;
3305
+ }
3286
3306
  }
3287
3307
  function reviewerNamesFor(packs) {
3288
3308
  const contested = contestedSemanticNames(packs);
3289
3309
  const declared = packs.flatMap((p) => (p.semantic ?? []).filter((s) => !isReservedClaim(p, s.name)).map((s) => s.name)).filter((name) => !contested.has(name));
3290
3310
  if (declared.length === 0)
3291
- return SEMANTIC_REVIEWER_NAMES;
3292
- return new Set(replacesBuiltinChecks(packs) ? declared : [...SEMANTIC_REVIEWER_NAMES, ...declared]);
3311
+ return NO_REVIEWERS;
3312
+ return new Set(declared);
3293
3313
  }
3294
3314
  function forgetEffectiveReviewerNames(cli) {
3295
3315
  cached = null;
@@ -12610,11 +12630,14 @@ import {
12610
12630
  existsSync as existsSync9,
12611
12631
  lstatSync,
12612
12632
  mkdirSync as mkdirSync4,
12633
+ readlinkSync,
12634
+ realpathSync as realpathSync2,
12613
12635
  renameSync as renameSync2,
12614
12636
  rmSync as rmSync2,
12637
+ symlinkSync,
12615
12638
  unlinkSync
12616
12639
  } from "node:fs";
12617
- import { resolve as resolve8, dirname as dirname5 } from "node:path";
12640
+ import { resolve as resolve8, dirname as dirname5, basename as basename3 } from "node:path";
12618
12641
  import { fileURLToPath } from "node:url";
12619
12642
  import { homedir as homedir6 } from "node:os";
12620
12643
  function readJsonFile(path) {
@@ -12918,33 +12941,84 @@ function makePiProjectRelativeEntry(extPath) {
12918
12941
  function getHermesPluginSourcePath() {
12919
12942
  const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
12920
12943
  if (fromEnv)
12921
- return resolve8(fromEnv, "hermes-plugin");
12922
- return resolve8(fileURLToPath(import.meta.url), "..", "..", "..", "hermes-plugin");
12944
+ return resolve8(fromEnv, HERMES_PLUGIN_SOURCE_DIR);
12945
+ return resolve8(fileURLToPath(import.meta.url), "..", "..", "..", HERMES_PLUGIN_SOURCE_DIR);
12923
12946
  }
12924
12947
  function hermesPluginPathForSettings(settingsPath) {
12925
12948
  return resolve8(dirname5(settingsPath), "plugins", HERMES_PLUGIN_ID);
12926
12949
  }
12927
- function hasManagedHermesPlugin(settingsPath) {
12928
- const pluginPath = hermesPluginPathForSettings(settingsPath);
12950
+ function pathPresent(path) {
12929
12951
  try {
12930
- if (lstatSync(pluginPath).isSymbolicLink())
12931
- return false;
12932
- if (!existsSync9(resolve8(pluginPath, HERMES_PLUGIN_MARKER)))
12933
- return false;
12934
- return HERMES_PLUGIN_FILES.every((file) => existsSync9(resolve8(pluginPath, file)));
12952
+ lstatSync(path);
12953
+ return true;
12935
12954
  } catch {
12936
12955
  return false;
12937
12956
  }
12938
12957
  }
12939
- function hasHermesPluginMarker(settingsPath) {
12940
- const pluginPath = hermesPluginPathForSettings(settingsPath);
12958
+ function hasAllHermesPluginFiles(dir) {
12959
+ return HERMES_PLUGIN_FILES.every((file) => existsSync9(resolve8(dir, file)));
12960
+ }
12961
+ function isFailproofaiHermesPluginSource(dir) {
12962
+ if (basename3(dir) !== HERMES_PLUGIN_SOURCE_DIR)
12963
+ return false;
12964
+ if (!existsSync9(dir))
12965
+ return true;
12966
+ try {
12967
+ const manifest = readFileSync5(resolve8(dir, "plugin.yaml"), "utf8");
12968
+ return /^name:\s*["']?failproofai["']?\s*$/m.test(manifest);
12969
+ } catch {
12970
+ return false;
12971
+ }
12972
+ }
12973
+ function samePath(a, b) {
12974
+ if (resolve8(a) === resolve8(b))
12975
+ return true;
12941
12976
  try {
12942
- return !lstatSync(pluginPath).isSymbolicLink() && existsSync9(resolve8(pluginPath, HERMES_PLUGIN_MARKER));
12977
+ return realpathSync2(a) === realpathSync2(b);
12943
12978
  } catch {
12944
12979
  return false;
12945
12980
  }
12946
12981
  }
12947
- function installHermesPlugin(settingsPath) {
12982
+ function hermesPluginState(settingsPath) {
12983
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
12984
+ let isLink;
12985
+ let isDirectory;
12986
+ try {
12987
+ const stat = lstatSync(pluginPath);
12988
+ isLink = stat.isSymbolicLink();
12989
+ isDirectory = stat.isDirectory();
12990
+ } catch {
12991
+ return { kind: "absent" };
12992
+ }
12993
+ if (isLink) {
12994
+ let target;
12995
+ try {
12996
+ target = resolve8(dirname5(pluginPath), readlinkSync(pluginPath));
12997
+ } catch {
12998
+ return { kind: "foreign" };
12999
+ }
13000
+ if (!isFailproofaiHermesPluginSource(target))
13001
+ return { kind: "foreign" };
13002
+ return {
13003
+ kind: "link",
13004
+ target,
13005
+ complete: hasAllHermesPluginFiles(target),
13006
+ current: samePath(target, getHermesPluginSourcePath())
13007
+ };
13008
+ }
13009
+ if (isDirectory && existsSync9(resolve8(pluginPath, HERMES_PLUGIN_MARKER))) {
13010
+ return { kind: "copy", complete: hasAllHermesPluginFiles(pluginPath) };
13011
+ }
13012
+ return { kind: "foreign" };
13013
+ }
13014
+ function hasManagedHermesPlugin(settingsPath) {
13015
+ const state = hermesPluginState(settingsPath);
13016
+ return (state.kind === "link" || state.kind === "copy") && state.complete;
13017
+ }
13018
+ function defaultHermesSymlink(target, path) {
13019
+ symlinkSync(target, path, process.platform === "win32" ? "junction" : "dir");
13020
+ }
13021
+ function installHermesPlugin(settingsPath, deps = {}) {
12948
13022
  const source = getHermesPluginSourcePath();
12949
13023
  for (const file of HERMES_PLUGIN_FILES) {
12950
13024
  if (!existsSync9(resolve8(source, file))) {
@@ -12952,23 +13026,47 @@ function installHermesPlugin(settingsPath) {
12952
13026
  }
12953
13027
  }
12954
13028
  const destination = hermesPluginPathForSettings(settingsPath);
12955
- if (existsSync9(destination)) {
12956
- if (lstatSync(destination).isSymbolicLink() || !existsSync9(resolve8(destination, HERMES_PLUGIN_MARKER))) {
12957
- throw new Error(`Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` + `Move it aside or install FailproofAI under a different Hermes profile.`);
12958
- }
13029
+ const existing = hermesPluginState(settingsPath);
13030
+ if (existing.kind === "foreign") {
13031
+ throw new Error(`Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` + `Move it aside or install FailproofAI under a different Hermes profile.`);
12959
13032
  }
13033
+ if (existing.kind === "link" && existing.current)
13034
+ return "unchanged";
12960
13035
  mkdirSync4(dirname5(destination), { recursive: true });
12961
13036
  const suffix = `${process.pid}-${Date.now()}`;
12962
13037
  const temporary = `${destination}.install-${suffix}`;
12963
- const backup = `${destination}.backup-${suffix}`;
12964
- let movedExisting = false;
13038
+ let linked = false;
12965
13039
  try {
13040
+ (deps.symlink ?? defaultHermesSymlink)(source, temporary);
13041
+ linked = true;
13042
+ } catch {
13043
+ rmSync2(temporary, { recursive: true, force: true });
13044
+ }
13045
+ if (!linked) {
12966
13046
  mkdirSync4(temporary, { recursive: true });
12967
- for (const file of HERMES_PLUGIN_FILES) {
12968
- cpSync(resolve8(source, file), resolve8(temporary, file));
13047
+ try {
13048
+ for (const file of HERMES_PLUGIN_FILES) {
13049
+ cpSync(resolve8(source, file), resolve8(temporary, file));
13050
+ }
13051
+ writeFileSync3(resolve8(temporary, HERMES_PLUGIN_MARKER), "Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n", { encoding: "utf8", mode: 384 });
13052
+ } catch (err) {
13053
+ rmSync2(temporary, { recursive: true, force: true });
13054
+ throw err;
13055
+ }
13056
+ }
13057
+ if (linked && (existing.kind === "absent" || existing.kind === "link")) {
13058
+ try {
13059
+ renameSync2(temporary, destination);
13060
+ } catch (err) {
13061
+ rmSync2(temporary, { force: true });
13062
+ throw err;
12969
13063
  }
12970
- writeFileSync3(resolve8(temporary, HERMES_PLUGIN_MARKER), "Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n", { encoding: "utf8", mode: 384 });
12971
- if (existsSync9(destination)) {
13064
+ return "linked";
13065
+ }
13066
+ const backup = `${destination}.backup-${suffix}`;
13067
+ let movedExisting = false;
13068
+ try {
13069
+ if (pathPresent(destination)) {
12972
13070
  renameSync2(destination, backup);
12973
13071
  movedExisting = true;
12974
13072
  }
@@ -12977,15 +13075,21 @@ function installHermesPlugin(settingsPath) {
12977
13075
  rmSync2(backup, { recursive: true, force: true });
12978
13076
  } catch (err) {
12979
13077
  rmSync2(temporary, { recursive: true, force: true });
12980
- if (movedExisting && !existsSync9(destination) && existsSync9(backup)) {
13078
+ if (movedExisting && !pathPresent(destination) && pathPresent(backup)) {
12981
13079
  renameSync2(backup, destination);
12982
13080
  }
12983
13081
  throw err;
12984
13082
  }
13083
+ return linked ? "linked" : "copied";
12985
13084
  }
12986
13085
  function removeManagedHermesPlugin(settingsPath) {
12987
13086
  const pluginPath = hermesPluginPathForSettings(settingsPath);
12988
- if (!hasHermesPluginMarker(settingsPath))
13087
+ const state = hermesPluginState(settingsPath);
13088
+ if (state.kind === "link") {
13089
+ unlinkSync(pluginPath);
13090
+ return true;
13091
+ }
13092
+ if (state.kind !== "copy")
12989
13093
  return false;
12990
13094
  rmSync2(pluginPath, { recursive: true, force: true });
12991
13095
  return true;
@@ -13030,6 +13134,93 @@ function hermesConfigState(settingsPath) {
13030
13134
  legacyShellHookPresent
13031
13135
  };
13032
13136
  }
13137
+ function enableHermesPluginInDoc(doc) {
13138
+ removeLegacyHermesHooks(doc);
13139
+ const js = doc.toJS() ?? {};
13140
+ const plugins = js.plugins && typeof js.plugins === "object" ? js.plugins : {};
13141
+ const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
13142
+ if (!enabled.includes(HERMES_PLUGIN_ID))
13143
+ enabled.push(HERMES_PLUGIN_ID);
13144
+ plugins.enabled = enabled;
13145
+ if (Array.isArray(plugins.disabled)) {
13146
+ plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
13147
+ if (plugins.disabled.length === 0)
13148
+ delete plugins.disabled;
13149
+ }
13150
+ doc.set("plugins", plugins);
13151
+ }
13152
+ async function migrateHermesProfiles(opts) {
13153
+ const results = [];
13154
+ let daemonOk;
13155
+ for (const profile of listHermesProfiles()) {
13156
+ if (!existsSync9(profile.home))
13157
+ continue;
13158
+ const settingsPath = resolve8(profile.home, "config.yaml");
13159
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
13160
+ const config = hermesConfigState(settingsPath);
13161
+ const plugin = hermesPluginState(settingsPath);
13162
+ const ours = plugin.kind === "link" || plugin.kind === "copy";
13163
+ const base = { name: profile.name, home: profile.home };
13164
+ const legacy = config.legacyShellHookPresent;
13165
+ if (!legacy && !ours && !config.pluginEnabled) {
13166
+ results.push({ ...base, status: "untouched", detail: "no failproofai integration", legacyShellHooksRemain: false });
13167
+ continue;
13168
+ }
13169
+ if (plugin.kind === "link" && plugin.current && plugin.complete && config.pluginEnabled && !legacy) {
13170
+ results.push({ ...base, status: "current", detail: "already linked", legacyShellHooksRemain: false });
13171
+ continue;
13172
+ }
13173
+ if (plugin.kind === "foreign") {
13174
+ results.push({
13175
+ ...base,
13176
+ status: "failed",
13177
+ detail: `an unmanaged plugin occupies ${pluginPath}` + (legacy ? "; shell hooks left in place (cron jobs are not checked)" : ""),
13178
+ legacyShellHooksRemain: legacy
13179
+ });
13180
+ continue;
13181
+ }
13182
+ if (existsSync9(settingsPath)) {
13183
+ const errors = $parseDocument(readFileSync5(settingsPath, "utf8")).errors;
13184
+ if (errors.length > 0) {
13185
+ results.push({
13186
+ ...base,
13187
+ status: "failed",
13188
+ detail: `${settingsPath} does not parse as YAML; left untouched`,
13189
+ legacyShellHooksRemain: legacy
13190
+ });
13191
+ continue;
13192
+ }
13193
+ }
13194
+ if (daemonOk === undefined)
13195
+ daemonOk = await opts.daemonSupportsPolicyEvaluation();
13196
+ if (!daemonOk) {
13197
+ results.push({
13198
+ ...base,
13199
+ status: "blocked",
13200
+ detail: legacy ? "daemon lacks native policy evaluation; shell hooks left in place (cron jobs are not checked)" : "daemon lacks native policy evaluation; the plugin here cannot get verdicts",
13201
+ legacyShellHooksRemain: legacy
13202
+ });
13203
+ continue;
13204
+ }
13205
+ try {
13206
+ const mode = installHermesPlugin(settingsPath, opts.installDeps);
13207
+ const doc = readYamlDoc(settingsPath);
13208
+ enableHermesPluginInDoc(doc);
13209
+ writeYamlDoc(settingsPath, doc);
13210
+ const from = legacy ? "shell hooks" : plugin.kind === "copy" ? "copied plugin" : plugin.kind === "link" ? "old plugin link" : "enabled but missing plugin";
13211
+ const to = mode === "copied" ? "plugin copy (symlink not possible here)" : "linked plugin";
13212
+ results.push({ ...base, status: "migrated", detail: `${from} → ${to}`, legacyShellHooksRemain: false });
13213
+ } catch (err) {
13214
+ results.push({
13215
+ ...base,
13216
+ status: "failed",
13217
+ detail: err instanceof Error ? err.message : String(err),
13218
+ legacyShellHooksRemain: hermesConfigState(settingsPath).legacyShellHookPresent
13219
+ });
13220
+ }
13221
+ }
13222
+ return results;
13223
+ }
13033
13224
  function hermesConfigHasHooks(settingsPath) {
13034
13225
  try {
13035
13226
  const state = hermesConfigState(settingsPath);
@@ -13067,7 +13258,7 @@ function listInstallableIds() {
13067
13258
  function detectInstalledClis() {
13068
13259
  return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id]?.detectInstalled());
13069
13260
  }
13070
- var claudeCode, codex, copilot, cursor, OPENCODE_PLUGIN_REL_PATH = "./plugins/failproofai.mjs", opencode, pi, HERMES_PLUGIN_ID = "failproofai", HERMES_PLUGIN_MARKER = ".failproofai-managed", HERMES_PLUGIN_FILES, hermes, OPENCLAW_PLUGIN_ID = "failproofai", openclaw, factory, devin, ANTIGRAVITY_HOOK_NAME = "failproofai", ANTIGRAVITY_TOOL_EVENTS, antigravity, goose, INTEGRATIONS;
13261
+ var claudeCode, codex, copilot, cursor, OPENCODE_PLUGIN_REL_PATH = "./plugins/failproofai.mjs", opencode, pi, HERMES_PLUGIN_ID = "failproofai", HERMES_PLUGIN_MARKER = ".failproofai-managed", HERMES_PLUGIN_FILES, HERMES_PLUGIN_SOURCE_DIR = "hermes-plugin", hermes, OPENCLAW_PLUGIN_ID = "failproofai", openclaw, factory, devin, ANTIGRAVITY_HOOK_NAME = "failproofai", ANTIGRAVITY_TOOL_EVENTS, antigravity, goose, INTEGRATIONS;
13071
13262
  var init_integrations = __esm(() => {
13072
13263
  init_dist();
13073
13264
  init_hermes_profiles();
@@ -13750,20 +13941,7 @@ var init_integrations = __esm(() => {
13750
13941
  },
13751
13942
  isFailproofaiHook: isMarkedHook,
13752
13943
  writeHookEntries(settings) {
13753
- const doc = settings;
13754
- removeLegacyHermesHooks(doc);
13755
- const js = doc.toJS() ?? {};
13756
- const plugins = js.plugins && typeof js.plugins === "object" ? js.plugins : {};
13757
- const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
13758
- if (!enabled.includes(HERMES_PLUGIN_ID))
13759
- enabled.push(HERMES_PLUGIN_ID);
13760
- plugins.enabled = enabled;
13761
- if (Array.isArray(plugins.disabled)) {
13762
- plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
13763
- if (plugins.disabled.length === 0)
13764
- delete plugins.disabled;
13765
- }
13766
- doc.set("plugins", plugins);
13944
+ enableHermesPluginInDoc(settings);
13767
13945
  },
13768
13946
  removeHooksFromFile(settingsPath) {
13769
13947
  const doc = readYamlDoc(settingsPath);
@@ -14386,7 +14564,7 @@ function outsideProject(facts, p) {
14386
14564
  return false;
14387
14565
  return p.resolved !== cwd && !p.resolved.startsWith(cwd.endsWith("/") ? cwd : cwd + "/");
14388
14566
  }
14389
- var PROTECTED_BRANCHES, OUTSIDE_PROJECT, SEMANTIC_POLICIES, SCOPE_PROBE, INJECTION_PROBE, HUMAN_ONLY, TASK_STEP_PROBE, OP_REQUESTED_PROBE, BEYOND_TASK_PROBE, TASK_PROBES;
14567
+ var PROTECTED_BRANCHES, OUTSIDE_PROJECT, SCOPE_PROBE, INJECTION_PROBE, HUMAN_ONLY, TASK_STEP_PROBE, OP_REQUESTED_PROBE, BEYOND_TASK_PROBE, TASK_PROBES;
14390
14568
  var init_policies = __esm(() => {
14391
14569
  PROTECTED_BRANCHES = new Set(["main", "master", "production", "prod", "release", "trunk"]);
14392
14570
  OUTSIDE_PROJECT = new Set([
@@ -14395,334 +14573,6 @@ var init_policies = __esm(() => {
14395
14573
  "system",
14396
14574
  "root"
14397
14575
  ]);
14398
- SEMANTIC_POLICIES = [
14399
- {
14400
- name: "destructive-deletion",
14401
- title: "Tried to permanently delete data that cannot be regenerated",
14402
- appliesTo: ["shell", "write"],
14403
- mode: "deny",
14404
- userCanOverride: true,
14405
- probes: [
14406
- {
14407
- id: "destroys",
14408
- instructions: "The tool call in `agent_request` permanently deletes, wipes, truncates or overwrites existing files, " + "directories or disks — for example `rm -r`, `find -delete`, `shred`, `dd of=`, `mkfs`, `truncate`, " + "`rsync --delete`, `git clean -fdx`, or redirecting output over an existing file.",
14409
- criteria: {
14410
- true: "Existing data would be destroyed or irreversibly overwritten.",
14411
- false: "Nothing existing is destroyed: it reads, lists, creates new files, or moves data somewhere recoverable."
14412
- }
14413
- },
14414
- {
14415
- id: "irreplaceable",
14416
- instructions: "What would be destroyed is outside the project, is the whole project, is the user's home directory, " + "is the filesystem root, or is data that cannot simply be regenerated (source code, documents, databases, " + "keys). Use `facts.paths[].relation` to see where each target sits relative to the project.",
14417
- criteria: {
14418
- true: "The destroyed data matters and could not be rebuilt by running a command.",
14419
- false: "Only regenerable data inside the project is affected: build output, dist/, caches, node_modules, " + "virtualenvs, coverage reports, temp files, or files the agent itself just created."
14420
- }
14421
- }
14422
- ],
14423
- guidance: "This permanently deletes data that cannot be rebuilt. Confirm the exact paths with the user first."
14424
- },
14425
- {
14426
- name: "production-infra-change",
14427
- title: "Tried to change live infrastructure",
14428
- appliesTo: ["shell"],
14429
- mode: "deny",
14430
- userCanOverride: true,
14431
- probes: [
14432
- {
14433
- id: "mutates",
14434
- instructions: "The command in `agent_request` changes the state of cloud or cluster infrastructure: it creates, " + "updates, deletes, applies, scales, restarts, rolls out, deploys or destroys resources in a cloud " + "account, Kubernetes cluster, managed database, DNS, CDN or hosting platform (any CLI: kubectl, helm, " + "terraform, tofu, pulumi, aws, gcloud, az, doctl, flyctl, vercel, wrangler, railway, and so on — " + "however the binary is spelled or pathed).",
14435
- criteria: {
14436
- true: "It mutates infrastructure.",
14437
- false: "It only reads or plans: get, list, describe, logs, status, plan, diff, validate, whoami, or --dry-run."
14438
- }
14439
- },
14440
- {
14441
- id: "not_local",
14442
- instructions: "The target of that change is a shared or production environment, or its environment cannot be told " + "from the command.",
14443
- criteria: {
14444
- true: "Production, shared, or unknown environment.",
14445
- false: "Clearly a local or throwaway environment: localhost, kind, minikube, docker-desktop, k3d, or a " + "context, workspace or profile whose name says dev, test, staging, sandbox or local."
14446
- }
14447
- }
14448
- ],
14449
- guidance: "This changes live infrastructure. Run it against a non-production target, or hand the command to a human."
14450
- },
14451
- {
14452
- name: "git-history-rewrite",
14453
- title: "Tried to rewrite or discard shared git history",
14454
- appliesTo: ["shell"],
14455
- mode: "deny",
14456
- userCanOverride: true,
14457
- probes: [
14458
- {
14459
- id: "rewrites_remote",
14460
- instructions: "The command in `agent_request` force-pushes or otherwise overwrites history on a git remote: " + "`git push --force`, `--force-with-lease`, `-f`, a `+refspec` such as `+HEAD:main`, or deleting a " + "remote branch. Git's global options before the subcommand (`git -C dir`, `git -c key=val`) and an " + "absolute path to the git binary do not change what the command does.",
14461
- criteria: {
14462
- true: "Remote history would be overwritten or a remote branch deleted.",
14463
- false: "A normal push that only adds commits, or no push at all."
14464
- }
14465
- }
14466
- ],
14467
- guidance: "Force-pushing rewrites history other people may have pulled. Confirm with the user first."
14468
- },
14469
- {
14470
- name: "push-to-protected-branch",
14471
- title: "Tried to push directly to a protected branch",
14472
- appliesTo: ["shell"],
14473
- mode: "instruct",
14474
- userCanOverride: true,
14475
- probes: [
14476
- {
14477
- id: "pushes_protected",
14478
- instructions: "The command in `agent_request` pushes commits to a remote branch named main, master, production, " + "release or trunk — either named explicitly or implied because `facts.current_git_branch` is one of " + "those and the push names no other branch.",
14479
- criteria: {
14480
- true: "Commits land directly on a protected remote branch.",
14481
- false: "It pushes a feature branch whose name merely contains a word like main (e.g. feature/main-menu), " + "or it does not push."
14482
- }
14483
- }
14484
- ],
14485
- guidance: "Pushing straight to a protected branch skips review. Push a branch and open a PR unless the user asked for this."
14486
- },
14487
- {
14488
- name: "commit-on-protected-branch",
14489
- title: "Tried to commit directly on a protected branch",
14490
- appliesTo: ["shell"],
14491
- mode: "instruct",
14492
- userCanOverride: true,
14493
- precondition: (facts) => facts.currentGitBranch !== null && PROTECTED_BRANCHES.has(facts.currentGitBranch),
14494
- probes: [
14495
- {
14496
- id: "creates_commit",
14497
- instructions: "The command in `agent_request` creates or amends a git commit, merge, rebase or cherry-pick in the " + "repository (including `git -C dir commit` and `git commit -a`).",
14498
- criteria: {
14499
- true: "A new commit is written to the current branch.",
14500
- false: "It only inspects history (log, show, diff, status) or mentions `commit` in a message or search."
14501
- }
14502
- }
14503
- ],
14504
- guidance: "You are on a protected branch. Create a feature branch before committing unless the user asked otherwise."
14505
- },
14506
- {
14507
- name: "secret-exposure",
14508
- title: "Tried to read or copy credentials",
14509
- appliesTo: ["shell", "read", "write"],
14510
- mode: "deny",
14511
- userCanOverride: true,
14512
- probes: [
14513
- {
14514
- id: "touches_secrets",
14515
- instructions: "The tool call in `agent_request` reads, prints, copies, or writes credential material: SSH private " + "keys (~/.ssh/id_*), authorized_keys, cloud credentials (~/.aws, ~/.config/gcloud, ~/.azure, " + "~/.kube/config), .env files containing real secret values, API tokens, password stores, keychains, " + "or browser cookie databases.",
14516
- criteria: {
14517
- true: "Actual secret values would be read, displayed, copied or written.",
14518
- false: "It only references the concept: source code that reads process.env, a variable name, an .env.example " + "template, a test fixture, documentation about credentials, or a grep for the word."
14519
- }
14520
- }
14521
- ],
14522
- guidance: "This exposes real credentials. Ask the user before reading or copying secret material."
14523
- },
14524
- {
14525
- name: "credential-exfiltration",
14526
- title: "Tried to send secrets or private files off the machine",
14527
- appliesTo: ["shell", "network"],
14528
- mode: "deny",
14529
- userCanOverride: false,
14530
- probes: [
14531
- {
14532
- id: "sends_out",
14533
- instructions: "The tool call in `agent_request` sends data to a network destination: an HTTP POST or PUT (curl -d, " + "curl -F, curl -T, wget --post-data), scp, sftp, rsync or rclone to a remote, nc or socat, an upload to " + "a paste site, bucket or webhook, or a DNS lookup carrying encoded data.",
14534
- criteria: {
14535
- true: "Data leaves this machine.",
14536
- false: "It only downloads, or only talks to localhost."
14537
- }
14538
- },
14539
- {
14540
- id: "sensitive_payload",
14541
- instructions: "The data being sent includes credentials, keys, tokens, environment variables, or the contents of files " + "from outside the project (see `facts.paths`).",
14542
- criteria: {
14543
- true: "Secrets or private files are in what is sent.",
14544
- false: "What is sent is ordinary project data the user would expect to share, like a test request body."
14545
- }
14546
- }
14547
- ],
14548
- guidance: "This sends secrets or private files to a remote destination, which the user cannot approve. Do not send them."
14549
- },
14550
- {
14551
- name: "remote-code-execution",
14552
- title: "Tried to run code downloaded from the internet",
14553
- appliesTo: ["shell"],
14554
- mode: "deny",
14555
- userCanOverride: true,
14556
- probes: [
14557
- {
14558
- id: "download_and_run",
14559
- instructions: "The command in `agent_request` downloads code or a script from the internet and executes it: " + '`curl … | sh`, `bash <(curl …)`, `wget … -O x.sh && bash x.sh`, `python3 -c "$(curl …)"`, piping into ' + "any interpreter (sh, bash, zsh, python, node, perl, ruby), or eval of a fetched string.",
14560
- criteria: {
14561
- true: "Fetched code is executed.",
14562
- false: "It only downloads without running, runs a local file, or merely searches for or quotes such a command " + "(for example grep over a README)."
14563
- }
14564
- }
14565
- ],
14566
- exempt: {
14567
- id: "official_installer",
14568
- instructions: "The URL being executed is the documented official installer of a widely used developer tool, served from " + "that tool's own domain (for example bun.sh, sh.rustup.rs, get.docker.com, deb.nodesource.com, " + "raw.githubusercontent.com/nvm-sh/nvm, astral.sh/uv)."
14569
- },
14570
- guidance: "This executes code fetched from the internet. Download it, show it to the user, then run it."
14571
- },
14572
- {
14573
- name: "privilege-escalation",
14574
- title: "Tried to run with elevated privileges",
14575
- appliesTo: ["shell"],
14576
- mode: "deny",
14577
- userCanOverride: true,
14578
- probes: [
14579
- {
14580
- id: "elevates",
14581
- instructions: "The command in `agent_request` runs something as root or another user: sudo, doas, su, pkexec, run0, " + "`sudo -i`, `sudo -s`, including when the binary is written as an absolute path or reached through a " + "variable or wrapper.",
14582
- criteria: {
14583
- true: "Privileges are elevated.",
14584
- false: "It runs as the current user, or only mentions sudo in text, a comment, or a search pattern."
14585
- }
14586
- }
14587
- ],
14588
- guidance: "This runs with elevated privileges. Ask the user to run it themselves."
14589
- },
14590
- {
14591
- name: "database-destruction",
14592
- title: "Tried to destroy or mass-modify database data",
14593
- appliesTo: ["shell"],
14594
- mode: "deny",
14595
- userCanOverride: true,
14596
- probes: [
14597
- {
14598
- id: "destructive_sql",
14599
- instructions: "The command in `agent_request` executes SQL or a database command that drops or truncates a table, " + "schema or database, or deletes or updates rows without a condition that narrows them to specific " + "records. A condition that is always true (`WHERE 1=1`, `WHERE true`, `WHERE id > 0`) does not narrow " + "anything. Also counts: `redis-cli FLUSHALL`, `dropdb`, `mongo … dropDatabase()`.",
14600
- criteria: {
14601
- true: "Many rows, a table, or a whole database would be removed or overwritten.",
14602
- false: "It reads, inserts, or changes a specific identified record, or runs a migration file by name."
14603
- }
14604
- },
14605
- {
14606
- id: "real_database",
14607
- instructions: "The database it targets holds real data rather than being a disposable test database.",
14608
- criteria: {
14609
- true: "A real or shared database, or one whose purpose cannot be told.",
14610
- false: "Clearly an in-memory, test, fixture or throwaway local database (sqlite :memory:, a test_ database)."
14611
- }
14612
- }
14613
- ],
14614
- guidance: "This destroys database data. Confirm the target and scope with the user first."
14615
- },
14616
- {
14617
- name: "read-outside-workspace",
14618
- title: "Read files outside the project",
14619
- appliesTo: ["shell", "read"],
14620
- mode: "instruct",
14621
- userCanOverride: true,
14622
- precondition: (facts) => facts.paths.some((p) => outsideProject(facts, p)),
14623
- probes: [
14624
- {
14625
- id: "reads_outside",
14626
- instructions: "The tool call in `agent_request` reads the contents of files that `facts.paths` marks as outside the " + "project, in the user's home directory (for example another repository, Documents, or dotfiles).",
14627
- criteria: {
14628
- true: "It reads file contents from outside the project.",
14629
- false: "It only changes directory, lists names, checks existence, or touches the agent's own tool caches."
14630
- }
14631
- }
14632
- ],
14633
- guidance: "This reads files outside the current project. Mention why before continuing."
14634
- },
14635
- {
14636
- name: "agent-config-tampering",
14637
- title: "Tried to change the agent's own safety configuration",
14638
- appliesTo: ["shell", "write"],
14639
- mode: "deny",
14640
- userCanOverride: false,
14641
- probes: [
14642
- {
14643
- id: "edits_agent_config",
14644
- instructions: "The tool call in `agent_request` modifies or deletes an AI agent's own configuration, hooks, " + "permissions or guardrails: .claude/settings.json, .claude/settings.local.json, ~/.claude/, .codex/, " + ".cursor/hooks.json, .gemini/, .factory/, failproofai configuration or policies, or permission and " + "safety sections of AGENTS.md or CLAUDE.md.",
14645
- criteria: {
14646
- true: "The agent's hooks, permissions or safety settings would change.",
14647
- false: "It only reads those files, or edits ordinary project documentation."
14648
- }
14649
- }
14650
- ],
14651
- guidance: "An agent must not change its own guardrails. Ask the user to make this change."
14652
- },
14653
- {
14654
- name: "system-modification",
14655
- title: "Tried to change the system outside the project",
14656
- appliesTo: ["shell"],
14657
- mode: "instruct",
14658
- userCanOverride: true,
14659
- probes: [
14660
- {
14661
- id: "modifies_system",
14662
- instructions: "The command in `agent_request` installs or removes software system-wide or changes machine " + "configuration: apt, dnf, pacman, brew install, `npm install -g`, `pip install` outside a virtualenv, " + "editing files under /etc, systemctl enable or disable, crontab, or changing shell startup files.",
14663
- criteria: {
14664
- true: "The machine changes outside the project directory.",
14665
- false: "It installs project dependencies locally (npm install, bun install, pip install in a venv)."
14666
- }
14667
- }
14668
- ],
14669
- guidance: "This changes the machine outside the project. Mention it to the user."
14670
- },
14671
- {
14672
- name: "env-secrets-dump",
14673
- title: "Tried to print environment secrets",
14674
- appliesTo: ["shell"],
14675
- mode: "instruct",
14676
- userCanOverride: true,
14677
- probes: [
14678
- {
14679
- id: "dumps_env",
14680
- instructions: "The command in `agent_request` prints environment variable VALUES that may be secret: `env`, " + "`printenv`, `set`, `export -p`, or `echo` of a variable whose name suggests a key, token, password " + "or secret.",
14681
- criteria: {
14682
- true: "Secret-looking environment values would be printed.",
14683
- false: "It echoes a literal string, prints a non-secret variable like PATH or HOME, creates a virtualenv " + "named env, or searches code for the word export."
14684
- }
14685
- }
14686
- ],
14687
- guidance: "This prints environment values that may be secrets into the transcript."
14688
- },
14689
- {
14690
- name: "external-destructive-action",
14691
- title: "Tried an irreversible action through an external tool",
14692
- appliesTo: ["other"],
14693
- mode: "deny",
14694
- userCanOverride: true,
14695
- probes: [
14696
- {
14697
- id: "irreversible_external",
14698
- instructions: "The tool call in `agent_request` (an MCP or integration tool, see `facts.tool_name`) performs an " + "irreversible or externally visible action: deleting records, repositories, files or accounts; " + "sending email, chat or social messages on the user's behalf; making payments or purchases; merging or " + "closing pull requests; changing permissions, access or billing; or writing to a production system.",
14699
- criteria: {
14700
- true: "Something outside this machine changes in a way that cannot be quietly undone.",
14701
- false: "It reads, searches, lists, fetches, or creates a draft that nobody else sees yet."
14702
- }
14703
- }
14704
- ],
14705
- guidance: "This takes an irreversible action in an external system. Confirm with the user first."
14706
- },
14707
- {
14708
- name: "external-data-egress",
14709
- title: "Sent private data to an external tool",
14710
- appliesTo: ["other"],
14711
- mode: "instruct",
14712
- userCanOverride: true,
14713
- probes: [
14714
- {
14715
- id: "egresses_private",
14716
- instructions: "The arguments in `agent_request` send private data to an external service: source code, file " + "contents, credentials, customer data, or personal information.",
14717
- criteria: {
14718
- true: "Private data is being shared with a third party.",
14719
- false: "Only a query, identifier or public information is sent."
14720
- }
14721
- }
14722
- ],
14723
- guidance: "This shares private data with an external service. Mention what is being sent."
14724
- }
14725
- ];
14726
14576
  SCOPE_PROBE = {
14727
14577
  id: "scope",
14728
14578
  instructions: "Everything `agent_request` would affect — every branch, file, directory, table, record, environment, " + "account or recipient — is covered by what the human asked for in `user_said`. It reaches no further: not " + "all branches when they named one, not production when they said local or staging, not a parent directory " + 'when they named one folder, not every record when they named some. References such as "it", "this PR" ' + 'or "that volume" mean whatever the conversation is about. Details the agent filled in to carry the ' + "request out (a commit title, a flag the operation needs) do not make it broader.",
@@ -16916,14 +16766,13 @@ function toSemanticPolicy(entry, pack) {
16916
16766
  function semanticPoliciesFromPacks(packs) {
16917
16767
  const declared = packs.filter((p) => packSemantic(p).length > 0);
16918
16768
  if (declared.length === 0)
16919
- return { policies: SEMANTIC_POLICIES, fromPack: false, errors: [] };
16920
- const replace = replacesBuiltinChecks(declared);
16769
+ return { policies: [], fromPack: false, errors: [] };
16921
16770
  const ordered = [...declared.filter((p) => isFirstPartyPack(p)), ...declared.filter((p) => !isFirstPartyPack(p))];
16922
- const policies = replace ? [] : [...SEMANTIC_POLICIES];
16771
+ const policies = [];
16923
16772
  const errors = [];
16924
- const seen = new Set(policies.map((p) => p.name));
16773
+ const seen = new Set;
16925
16774
  const contested = contestedSemanticNames(declared);
16926
- let spent = replace ? 0 : BUILTIN_QUESTION_CHARS;
16775
+ let spent = 0;
16927
16776
  for (const pack of ordered) {
16928
16777
  for (const entry of packSemantic(pack)) {
16929
16778
  if (isReservedClaim(pack, entry.name)) {
@@ -16956,9 +16805,7 @@ function semanticPoliciesFromPacks(packs) {
16956
16805
  spent += cost;
16957
16806
  }
16958
16807
  }
16959
- if (policies.length === (replace ? 0 : SEMANTIC_POLICIES.length))
16960
- return { policies: SEMANTIC_POLICIES, fromPack: false, errors };
16961
- return { policies, fromPack: true, errors };
16808
+ return { policies, fromPack: policies.length > 0, errors };
16962
16809
  }
16963
16810
  function resolveSemanticPolicies(cli) {
16964
16811
  let packs = [];
@@ -16968,7 +16815,7 @@ function resolveSemanticPolicies(cli) {
16968
16815
  packs = read.packs;
16969
16816
  manifestErrors = read.warnings ?? [];
16970
16817
  } catch {
16971
- return SEMANTIC_POLICIES;
16818
+ return [];
16972
16819
  }
16973
16820
  const resolved = semanticPoliciesFromPacks(jevPacks(packs, cli));
16974
16821
  for (const message of [...manifestErrors, ...resolved.errors]) {
@@ -16979,7 +16826,7 @@ function resolveSemanticPolicies(cli) {
16979
16826
  }
16980
16827
  return resolved.policies;
16981
16828
  }
16982
- var GLOBAL_QUESTION_CHARS, MAX_PACK_QUESTION_CHARS, BUILTIN_QUESTION_CHARS, warned;
16829
+ var GLOBAL_QUESTION_CHARS, MAX_PACK_QUESTION_CHARS, JEV_POLICIES_QUESTION_CHARS = 18490, warned;
16983
16830
  var init_pack_policies = __esm(() => {
16984
16831
  init_effective_reviewers();
16985
16832
  init_hook_logger();
@@ -16991,7 +16838,6 @@ var init_pack_policies = __esm(() => {
16991
16838
  init_precondition_names();
16992
16839
  GLOBAL_QUESTION_CHARS = Math.max(JSON.stringify({ [INJECTION_PROBE.id]: noulOf(INJECTION_PROBE), [SCOPE_PROBE.id]: noulOf(SCOPE_PROBE) }).length, JSON.stringify(Object.fromEntries([INJECTION_PROBE, ...TASK_PROBES].map((p) => [p.id, noulOf(p)]))).length);
16993
16840
  MAX_PACK_QUESTION_CHARS = MAX_REQUEST_CHARS - MAX_STATE_CHARS - GLOBAL_QUESTION_CHARS;
16994
- BUILTIN_QUESTION_CHARS = SEMANTIC_POLICIES.reduce((n, p) => n + questionChars(p), 0);
16995
16841
  warned = new Set;
16996
16842
  });
16997
16843
 
@@ -17384,7 +17230,7 @@ var init_telemetry_id2 = __esm(() => {
17384
17230
  });
17385
17231
 
17386
17232
  // src/hooks/custom-hooks-loader.ts
17387
- import { resolve as resolve10, isAbsolute as isAbsolute2, basename as basename3 } from "node:path";
17233
+ import { resolve as resolve10, isAbsolute as isAbsolute2, basename as basename4 } from "node:path";
17388
17234
  import { randomUUID } from "crypto";
17389
17235
  import { existsSync as existsSync10, readdirSync as readdirSync3 } from "node:fs";
17390
17236
  import { readFile as readFile3 } from "node:fs/promises";
@@ -17454,7 +17300,7 @@ async function loadSingleFile(absPath, opts) {
17454
17300
  const ignored = getSemanticRegistrations().slice(semanticBefore).map((d) => d?.name);
17455
17301
  if (opts?.outsidePack && ignored.length > 0 && !warnedSemanticOutsidePack.has(absPath)) {
17456
17302
  warnedSemanticOutsidePack.add(absPath);
17457
- hookLogWarn(`${basename3(absPath)}: semanticPolicies.add only takes effect in a pack published with \`failproofai publish\`; ` + `${ignored.join(", ")} ${ignored.length === 1 ? "is" : "are"} never asked here`);
17303
+ hookLogWarn(`${basename4(absPath)}: semanticPolicies.add only takes effect in a pack published with \`failproofai publish\`; ` + `${ignored.join(", ")} ${ignored.length === 1 ? "is" : "are"} never asked here`);
17458
17304
  }
17459
17305
  if (policyModuleCache.size >= POLICY_MODULE_CACHE_MAX_ENTRIES)
17460
17306
  policyModuleCache.clear();
@@ -17470,7 +17316,7 @@ async function loadSingleFile(absPath, opts) {
17470
17316
  error_type: errorType,
17471
17317
  is_convention: !!opts?.conventionScope,
17472
17318
  convention_scope: opts?.conventionScope ?? null,
17473
- file_basename: basename3(absPath)
17319
+ file_basename: basename4(absPath)
17474
17320
  });
17475
17321
  if (opts?.strict)
17476
17322
  throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
@@ -17659,12 +17505,12 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
17659
17505
  await loadSingleFile(file, { conventionScope: projectScope, outsidePack: true });
17660
17506
  const newHooks = getCustomHooks().slice(hooksBefore);
17661
17507
  for (const hook of newHooks) {
17662
- hook.__policyId = conventionPolicyId(projectScope, basename3(file), hook.name);
17508
+ hook.__policyId = conventionPolicyId(projectScope, basename4(file), hook.name);
17663
17509
  }
17664
17510
  if (newHooks.length > 0) {
17665
17511
  conventionSources.push({
17666
17512
  scope: projectScope,
17667
- file: basename3(file),
17513
+ file: basename4(file),
17668
17514
  hookNames: newHooks.map((h) => h.name)
17669
17515
  });
17670
17516
  }
@@ -17681,12 +17527,12 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
17681
17527
  await loadSingleFile(file, { conventionScope: "user", outsidePack: true });
17682
17528
  const newHooks = getCustomHooks().slice(hooksBefore);
17683
17529
  for (const hook of newHooks) {
17684
- hook.__policyId = conventionPolicyId("user", basename3(file), hook.name);
17530
+ hook.__policyId = conventionPolicyId("user", basename4(file), hook.name);
17685
17531
  }
17686
17532
  if (newHooks.length > 0) {
17687
17533
  conventionSources.push({
17688
17534
  scope: "user",
17689
- file: basename3(file),
17535
+ file: basename4(file),
17690
17536
  hookNames: newHooks.map((h) => h.name)
17691
17537
  });
17692
17538
  }
@@ -19318,14 +19164,14 @@ function combineTwoTier(verdicts, review, mode) {
19318
19164
  ...review.model ? { jevModel: review.model } : {},
19319
19165
  jevMode: mode
19320
19166
  };
19321
- if (mode === "shadow") {
19322
- const shadowVerdict = review.decision === "deny" || review.decision === "instruct" ? {
19167
+ if (mode === "observe") {
19168
+ const observeVerdict = review.decision === "deny" || review.decision === "instruct" ? {
19323
19169
  policyName: jevEntry.policyName,
19324
19170
  decision: review.decision,
19325
19171
  reason: jevEntry.reason,
19326
19172
  version: review.model && JEV_MODEL_RE.test(review.model) ? review.model : "jev"
19327
19173
  } : undefined;
19328
- return { final: legacy, cleared, decidedByJev: false, activity, ...shadowVerdict ? { shadowVerdict } : {} };
19174
+ return { final: legacy, cleared, decidedByJev: false, activity, ...observeVerdict ? { observeVerdict } : {} };
19329
19175
  }
19330
19176
  return { ...enforced, cleared, activity };
19331
19177
  }
@@ -19419,7 +19265,7 @@ async function evaluatePolicies(eventType, payload, session, config, twoTier) {
19419
19265
  }
19420
19266
  const combined = combineTwoTier(collected.verdicts, review, twoTier.mode);
19421
19267
  if (combined.cleared.length > 0) {
19422
- hookLogInfo(`jev ${twoTier.mode === "shadow" ? "would clear" : "cleared"}: ${combined.cleared.join(", ")}`);
19268
+ hookLogInfo(`jev ${twoTier.mode === "observe" ? "would clear" : "cleared"}: ${combined.cleared.join(", ")}`);
19423
19269
  }
19424
19270
  if (combined.activity.evaluator === "jev-fallback") {
19425
19271
  hookLogInfo(combined.activity.jevFallbackReason === JEV_REASON_REQUEST_CUT ? "jev could not be shown the whole call; it cleared nothing, and the call was not allowed on a partial read" : `jev unavailable (${combined.activity.jevFallbackReason}); the regex result decided this call`);
@@ -19430,7 +19276,7 @@ async function evaluatePolicies(eventType, payload, session, config, twoTier) {
19430
19276
  activity: combined.activity,
19431
19277
  decidedByJev: combined.decidedByJev,
19432
19278
  ...combined.decidedByJev && review.kind === "answered" && review.origin ? { jevOrigin: review.origin } : {},
19433
- ...combined.shadowVerdict ? { shadowVerdict: combined.shadowVerdict } : {}
19279
+ ...combined.observeVerdict ? { observeVerdict: combined.observeVerdict } : {}
19434
19280
  }
19435
19281
  };
19436
19282
  }
@@ -24798,7 +24644,7 @@ var init_copilot_sessions = __esm(() => {
24798
24644
  // lib/cursor-sessions.ts
24799
24645
  import { readFileSync as readFileSync13, readdirSync as readdirSync11, existsSync as existsSync18, statSync as statSync13 } from "node:fs";
24800
24646
  import { readFile as readFile13 } from "node:fs/promises";
24801
- import { basename as basename4, join as join28, resolve as resolve14, sep as sep3 } from "node:path";
24647
+ import { basename as basename5, join as join28, resolve as resolve14, sep as sep3 } from "node:path";
24802
24648
  import { homedir as homedir20 } from "node:os";
24803
24649
  function getCursorHome2() {
24804
24650
  return process.env.CURSOR_HOME || join28(homedir20(), ".cursor");
@@ -24836,7 +24682,7 @@ function findCursorTranscript(sessionId) {
24836
24682
  const dir = getCursorSessionDir(sessionId);
24837
24683
  if (!dir)
24838
24684
  return null;
24839
- const newCandidate = join28(dir, `${basename4(dir)}.jsonl`);
24685
+ const newCandidate = join28(dir, `${basename5(dir)}.jsonl`);
24840
24686
  if (existsSync18(newCandidate))
24841
24687
  return newCandidate;
24842
24688
  for (const name of LEGACY_TRANSCRIPT_FILE_CANDIDATES2) {
@@ -27065,6 +26911,9 @@ var init_fp_reset = __esm(() => {
27065
26911
  // src/hooks/semantic/jev-config.ts
27066
26912
  import { closeSync as closeSync2, constants as fsConstants2, fstatSync as fstatSync2, openSync as openSync2, readSync as readSync2, statSync as statSync18 } from "node:fs";
27067
26913
  import { dirname as dirname12 } from "node:path";
26914
+ function parseJevMode(raw) {
26915
+ return raw === "off" || raw === "observe" || raw === "enforce" ? raw : null;
26916
+ }
27068
26917
  function jevCloudBaseUrl(cloudBase) {
27069
26918
  const url = new URL(cloudBase);
27070
26919
  return `${url.origin}${url.pathname.replace(/\/+$/, "")}${JEV_CLOUD_BASE_PATH}`;
@@ -27288,14 +27137,15 @@ function validateJevConfig(raw, envKey, cloud) {
27288
27137
  cfg.timeoutMs = t;
27289
27138
  }
27290
27139
  if (o.mode !== undefined) {
27291
- if (o.mode !== "off" && o.mode !== "shadow" && o.mode !== "enforce")
27292
- return { ok: false, problem: 'mode must be "off", "shadow" or "enforce"' };
27293
- cfg.mode = o.mode;
27140
+ const mode = parseJevMode(o.mode);
27141
+ if (mode === null)
27142
+ return { ok: false, problem: 'mode must be "off", "observe" or "enforce"' };
27143
+ cfg.mode = mode;
27294
27144
  }
27295
27145
  if (cfg.baseUrl !== undefined && isPlainHttp(cfg.baseUrl) && cfg.mode === "enforce") {
27296
27146
  return {
27297
27147
  ok: false,
27298
- problem: "plain http (to localhost) is accepted only with mode shadow: in enforce mode Jev's answers can clear a deny, " + "and while the local proxy is down any process on this machine could take its port and answer. " + (cfg.provider === JEV_CLOUD_PROVIDER ? "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or keep mode shadow" : "Use https, or mode shadow")
27148
+ problem: "plain http (to localhost) is accepted only with mode observe: in enforce mode Jev's answers can clear a deny, " + "and while the local proxy is down any process on this machine could take its port and answer. " + (cfg.provider === JEV_CLOUD_PROVIDER ? "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or keep mode observe" : "Use https, or mode observe")
27299
27149
  };
27300
27150
  }
27301
27151
  return { ok: true, value: cfg };
@@ -27570,6 +27420,19 @@ function classifyTool(toolName) {
27570
27420
  return { toolClass: "other", toolIsKnown: true };
27571
27421
  return { toolClass: "other", toolIsKnown: false };
27572
27422
  }
27423
+ function runsNestedShell(tokens) {
27424
+ let shellSeen = false;
27425
+ for (const tok of tokens) {
27426
+ if (shellSeen && /^-[a-z]*c[a-z]*$/i.test(tok))
27427
+ return true;
27428
+ const base = tok.slice(tok.lastIndexOf("/") + 1);
27429
+ if (base === "eval")
27430
+ return true;
27431
+ if (NESTED_SHELLS.has(base))
27432
+ shellSeen = true;
27433
+ }
27434
+ return false;
27435
+ }
27573
27436
  function scanCommand(command) {
27574
27437
  const text = command.length > MAX_SCAN_CHARS ? command.slice(0, MAX_SCAN_CHARS) : command;
27575
27438
  const segments = [];
@@ -27580,22 +27443,36 @@ function scanCommand(command) {
27580
27443
  let out = "";
27581
27444
  let commentsRemoved = false;
27582
27445
  const comments = [];
27446
+ let complete = command.length <= MAX_SCAN_CHARS;
27447
+ let braceDepth = 0;
27448
+ let braceSep = false;
27449
+ let bracketOpen = false;
27583
27450
  const endWord = () => {
27584
27451
  if (inWord)
27585
27452
  tokens.push(word);
27586
27453
  word = "";
27587
27454
  inWord = false;
27455
+ braceDepth = 0;
27456
+ braceSep = false;
27457
+ bracketOpen = false;
27588
27458
  };
27589
27459
  const endSegment = () => {
27590
27460
  endWord();
27591
- if (tokens.length > 0)
27461
+ if (tokens.length > 0) {
27462
+ if (complete && runsNestedShell(tokens))
27463
+ complete = false;
27592
27464
  segments.push(tokens);
27465
+ }
27593
27466
  tokens = [];
27594
27467
  };
27595
27468
  for (let i = 0;i < text.length; i++) {
27596
27469
  const c = text[i];
27597
27470
  if (quote) {
27598
27471
  out += c;
27472
+ if (quote === '"' && (c === "`" || c === "$" && EXPANSION_START.test(text[i + 1] ?? "") || c === "\\" && text[i + 1] === `
27473
+ `)) {
27474
+ complete = false;
27475
+ }
27599
27476
  if (c === quote) {
27600
27477
  quote = null;
27601
27478
  } else if (c === "\\" && quote === '"' && i + 1 < text.length) {
@@ -27606,6 +27483,10 @@ function scanCommand(command) {
27606
27483
  }
27607
27484
  continue;
27608
27485
  }
27486
+ if (c === "`" || c === "$" && (text[i + 1] === "'" || text[i + 1] === '"' || EXPANSION_START.test(text[i + 1] ?? "")) || (c === "<" || c === ">") && text[i + 1] === "(" || c === "<" && text[i + 1] === "<" || c === "\\" && text[i + 1] === `
27487
+ `) {
27488
+ complete = false;
27489
+ }
27609
27490
  if (c === "'" || c === '"') {
27610
27491
  quote = c;
27611
27492
  inWord = true;
@@ -27646,12 +27527,29 @@ function scanCommand(command) {
27646
27527
  out += c;
27647
27528
  continue;
27648
27529
  }
27530
+ if (c === "{") {
27531
+ braceDepth++;
27532
+ } else if (braceDepth > 0 && (c === "," || c === "." && text[i + 1] === ".")) {
27533
+ braceSep = true;
27534
+ } else if (c === "}" && braceDepth > 0) {
27535
+ braceDepth--;
27536
+ if (braceSep)
27537
+ complete = false;
27538
+ } else if (c === "*" || c === "?") {
27539
+ complete = false;
27540
+ } else if (c === "[") {
27541
+ bracketOpen = true;
27542
+ } else if (c === "]" && bracketOpen) {
27543
+ complete = false;
27544
+ }
27649
27545
  word += c;
27650
27546
  inWord = true;
27651
27547
  out += c;
27652
27548
  }
27653
27549
  endSegment();
27654
- return { segments, withoutComments: out.trimEnd(), commentsRemoved, comments };
27550
+ if (quote)
27551
+ complete = false;
27552
+ return { segments, withoutComments: out.trimEnd(), commentsRemoved, comments, complete };
27655
27553
  }
27656
27554
  function expandHome(token, home) {
27657
27555
  if (token === "~")
@@ -27784,7 +27682,7 @@ function computeFacts(toolName, toolInput, cwd, permissionMode, scanned, pinnedP
27784
27682
  permissionMode
27785
27683
  };
27786
27684
  }
27787
- var SHELL_TOOLS, WRITE_TOOLS, READ_TOOLS, NETWORK_TOOLS, INERT_TOOLS, MAX_SCAN_CHARS = 8192, PATH_LIKE, GLOB_CHARS, MAX_PATHS = 12;
27685
+ var SHELL_TOOLS, WRITE_TOOLS, READ_TOOLS, NETWORK_TOOLS, INERT_TOOLS, MAX_SCAN_CHARS = 8192, NESTED_SHELLS, EXPANSION_START, PATH_LIKE, GLOB_CHARS, MAX_PATHS = 12;
27788
27686
  var init_facts = __esm(() => {
27789
27687
  SHELL_TOOLS = new Set(["Bash", "BashOutput"]);
27790
27688
  WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
@@ -27803,6 +27701,8 @@ var init_facts = __esm(() => {
27803
27701
  "SlashCommand",
27804
27702
  "Skill"
27805
27703
  ]);
27704
+ NESTED_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "fish"]);
27705
+ EXPANSION_START = /[A-Za-z_0-9@*#?$!\-({]/;
27806
27706
  PATH_LIKE = /^(?:\/|~|\.\.?(?:\/|$)|[^:]*\/)/;
27807
27707
  GLOB_CHARS = /[*?[]/;
27808
27708
  });
@@ -27811,50 +27711,70 @@ var init_facts = __esm(() => {
27811
27711
  function tokensOf(text) {
27812
27712
  return text.toLowerCase().split(/[^a-z0-9._@-]+/).filter((t) => t.length >= 3 && !GENERIC_TOKENS.has(t) && !/^\d+$/.test(t));
27813
27713
  }
27814
- function targetTokens(toolInput) {
27815
- const out = new Set;
27714
+ function scanTargets(toolInput) {
27715
+ const groups = [];
27716
+ const addGroup = (tok) => {
27717
+ const words = tokensOf(tok);
27718
+ if (words.length > 0)
27719
+ groups.push(new Set(words));
27720
+ };
27816
27721
  const command = typeof toolInput.command === "string" ? toolInput.command : null;
27817
27722
  if (command) {
27818
- for (const seg of scanCommand(command).segments) {
27723
+ const scanned = scanCommand(command);
27724
+ for (const seg of scanned.segments) {
27819
27725
  for (const tok of seg.slice(1)) {
27820
- if (tok.startsWith("-"))
27821
- continue;
27822
- for (const t of tokensOf(tok))
27823
- out.add(t);
27726
+ if (!tok.startsWith("-"))
27727
+ addGroup(tok);
27824
27728
  }
27825
27729
  }
27826
- if (out.size === 0) {
27730
+ if (groups.length === 0) {
27827
27731
  for (const piece of command.split(/[;&|\n]+/)) {
27828
27732
  for (const tok of piece.trim().split(/\s+/).slice(1)) {
27829
27733
  if (!tok.startsWith("-"))
27830
- for (const t of tokensOf(tok))
27831
- out.add(t);
27734
+ addGroup(tok);
27832
27735
  }
27833
27736
  }
27834
27737
  }
27835
- return out;
27738
+ return { targets: new Set(groups.flatMap((g) => [...g])), groups, complete: scanned.complete };
27836
27739
  }
27740
+ const all = new Set;
27837
27741
  for (const [key, value] of Object.entries(toolInput)) {
27838
27742
  if (typeof value !== "string" || value.length > 300)
27839
27743
  continue;
27840
27744
  if (/content|old_string|new_string|body|text|prompt/i.test(key))
27841
27745
  continue;
27842
27746
  for (const t of tokensOf(value))
27843
- out.add(t);
27747
+ all.add(t);
27844
27748
  }
27845
- return out;
27749
+ return { targets: all, groups: all.size > 0 ? [all] : [], complete: true };
27846
27750
  }
27847
- function targetNamedByUser(targets, userSaid) {
27848
- if (userSaid.length === 0)
27751
+ function everyTargetNamed(scan, userSaid) {
27752
+ if (userSaid.length === 0 || scan.groups.length === 0)
27849
27753
  return false;
27850
- if (targets.size === 0)
27754
+ const said = userSaid.join(`
27755
+ `).toLowerCase();
27756
+ return scan.groups.every((g) => {
27757
+ for (const t of g)
27758
+ if (said.includes(t))
27759
+ return true;
27760
+ return false;
27761
+ });
27762
+ }
27763
+ function partlyNamed(scan, userSaid) {
27764
+ if (userSaid.length === 0 || scan.groups.length < 2)
27851
27765
  return false;
27852
27766
  const said = userSaid.join(`
27853
27767
  `).toLowerCase();
27854
- for (const t of targets)
27855
- if (said.includes(t))
27856
- return true;
27857
- return false;
27768
+ let named = 0;
27769
+ for (const g of scan.groups) {
27770
+ for (const t of g) {
27771
+ if (said.includes(t)) {
27772
+ named++;
27773
+ break;
27774
+ }
27775
+ }
27776
+ }
27777
+ return named > 0 && named < scan.groups.length;
27858
27778
  }
27859
27779
  function formatOutcome(p, o) {
27860
27780
  return `${p.title} (semantic/${p.name}, p=${o.evidence.toFixed(2)}). ${p.guidance}`;
@@ -27864,7 +27784,7 @@ function decide(selected, answers, toolInput, userSaid, thresholds = DEFAULT_THR
27864
27784
  const injected = injection !== null && injection >= thresholds.injection;
27865
27785
  const scope = typeof answers.scope === "number" ? answers.scope : null;
27866
27786
  const withinScope = scope !== null && scope >= thresholds.scope;
27867
- let targets = null;
27787
+ let scan = null;
27868
27788
  const outcomes = selected.map((p) => {
27869
27789
  const evidence = Math.min(...p.probes.map((probe) => answers[`${p.name}.${probe.id}`] ?? 0));
27870
27790
  const exempt = p.exempt ? answers[`${p.name}.exempt`] ?? 0 : null;
@@ -27887,9 +27807,11 @@ function decide(selected, answers, toolInput, userSaid, thresholds = DEFAULT_THR
27887
27807
  return { ...base, escalatedByInjection: true, verdict: "deny" };
27888
27808
  const fired = p.mode === "deny" && evidence >= thresholds.deny ? "deny" : "instruct";
27889
27809
  if (p.userCanOverride && userAsked !== null && userAsked >= thresholds.userAsked && withinScope) {
27890
- targets ??= targetTokens(toolInput);
27891
- const named = targets.size > 0 && targetNamedByUser(targets, userSaid);
27892
- if (targets.size === 0 || named || userSaidCut)
27810
+ scan ??= scanTargets(toolInput);
27811
+ if (!scan.complete)
27812
+ return { ...base, verdict: fired, targetScanIncomplete: true };
27813
+ const named = scan.groups.length > 0 && everyTargetNamed(scan, userSaid);
27814
+ if (scan.groups.length === 0 || named || userSaidCut)
27893
27815
  return { ...base, targetNamedByUser: named, verdict: "overridden" };
27894
27816
  }
27895
27817
  return { ...base, verdict: fired };
@@ -27939,12 +27861,14 @@ function decideV1(selected, answers, toolInput, userSaid, agentLastMessage, opts
27939
27861
  const task = num("task_step");
27940
27862
  const op = num("op_requested");
27941
27863
  const beyond = num("beyond_task");
27942
- let targets = null;
27864
+ let scan = null;
27865
+ const targetScan = () => scan ??= scanTargets(toolInput);
27866
+ const evidenceSaid = agentLastMessage ? [...userSaid, agentLastMessage] : userSaid;
27943
27867
  const targetOk = () => {
27944
- targets ??= targetTokens(toolInput);
27945
- if (targets.size === 0)
27868
+ const s = targetScan();
27869
+ if (s.groups.length === 0)
27946
27870
  return { ok: true, named: false };
27947
- const named = targetNamedByUser(targets, agentLastMessage ? [...userSaid, agentLastMessage] : userSaid);
27871
+ const named = everyTargetNamed(s, evidenceSaid);
27948
27872
  return { ok: named || userSaidCut, named };
27949
27873
  };
27950
27874
  const outcomes = selected.map((p) => {
@@ -27969,12 +27893,14 @@ function decideV1(selected, answers, toolInput, userSaid, agentLastMessage, opts
27969
27893
  const fired = p.mode === "deny" && evidence >= t.deny ? "deny" : "instruct";
27970
27894
  if (!p.userCanOverride)
27971
27895
  return { ...base, verdict: fired };
27896
+ if (!targetScan().complete)
27897
+ return { ...base, verdict: fired, targetScanIncomplete: true };
27972
27898
  if (op !== null && op >= t.opRequested && beyond !== null && beyond < t.opBeyondMax) {
27973
27899
  const target = targetOk();
27974
27900
  if (target.ok)
27975
27901
  return { ...base, targetNamedByUser: target.named, verdict: "overridden", intent: "op-requested" };
27976
27902
  }
27977
- if (taskClears && task !== null && task >= t.taskStep && beyond !== null && beyond < t.taskBeyondMax) {
27903
+ if (taskClears && task !== null && task >= t.taskStep && beyond !== null && beyond < t.taskBeyondMax && !(typeof toolInput.command === "string" && !userSaidCut && partlyNamed(targetScan(), evidenceSaid))) {
27978
27904
  if (fired === "instruct")
27979
27905
  return { ...base, verdict: "overridden", intent: "task-step" };
27980
27906
  return { ...base, verdict: "instruct", intent: "downgraded-task-step" };
@@ -29657,7 +29583,7 @@ function authorityOf(p) {
29657
29583
  return { authority, reviewedBy };
29658
29584
  }
29659
29585
  function resolveMode(cfg) {
29660
- return cfg.mode === "shadow" || cfg.mode === "enforce" ? cfg.mode : DEFAULT_JEV_MODE;
29586
+ return cfg.mode === "observe" || cfg.mode === "enforce" ? cfg.mode : DEFAULT_JEV_MODE;
29661
29587
  }
29662
29588
  function resolveTimeout(cfg) {
29663
29589
  const t = cfg.timeoutMs;
@@ -29775,7 +29701,7 @@ function startJevReview(cfg, call) {
29775
29701
  sessionId: call.sessionId,
29776
29702
  cli: call.cli,
29777
29703
  eventType: call.eventType,
29778
- applied: abandoned || review.kind !== "answered" ? "legacy-fallback" : mode === "shadow" ? "shadow" : "two-tier"
29704
+ applied: abandoned || review.kind !== "answered" ? "legacy-fallback" : mode === "observe" ? "observe" : "two-tier"
29779
29705
  });
29780
29706
  appendVerdictLog(hit ? { ...row, cached: true } : row);
29781
29707
  }
@@ -30026,6 +29952,8 @@ async function startTwoTier(canonicalEventType, parsed, session, cli, opts, acti
30026
29952
  return null;
30027
29953
  if (jevForcedOff(opts) || activePause)
30028
29954
  return null;
29955
+ if (!jevChecksInstalled())
29956
+ return null;
30029
29957
  const loaded = await readJevConfig();
30030
29958
  if (!loaded)
30031
29959
  return null;
@@ -30044,7 +29972,7 @@ async function startTwoTier(canonicalEventType, parsed, session, cli, opts, acti
30044
29972
  } catch (err) {
30045
29973
  hookLogInfo(`Jev review could not start (${err instanceof Error ? err.message : String(err)})`);
30046
29974
  return {
30047
- mode: cfg.mode === "shadow" || cfg.mode === "enforce" ? cfg.mode : loaded.defaultMode,
29975
+ mode: cfg.mode === "observe" || cfg.mode === "enforce" ? cfg.mode : loaded.defaultMode,
30048
29976
  review: Promise.resolve({ kind: "fallback", reason: "error", latencyMs: null, model: null }),
30049
29977
  abort: () => {},
30050
29978
  authorityOf: () => ({ authority: "hard", reviewedBy: [] })
@@ -30068,6 +29996,8 @@ async function captureJevIntent(canonicalEventType, decision, parsed, session, c
30068
29996
  if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts))
30069
29997
  return;
30070
29998
  try {
29999
+ if (!jevChecksInstalled())
30000
+ return;
30071
30001
  if (!await readJevConfig())
30072
30002
  return;
30073
30003
  if (decision === "deny") {
@@ -30148,7 +30078,7 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
30148
30078
  config = readMergedHooksConfig(session.cwd);
30149
30079
  clearPolicies(cli);
30150
30080
  activePause = readActivePause(session.sessionId);
30151
- const packsInstalledHere = hasInstalledPacks();
30081
+ const packsInstalledHere = hasInstalledRegexPacks();
30152
30082
  const legacyNames = activePause || packsInstalledHere ? [] : config.enabledPolicies;
30153
30083
  registerBuiltinPolicies(activePause ? [] : legacyNames);
30154
30084
  if (legacyNames.length > 0) {
@@ -30222,14 +30152,14 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
30222
30152
  const observeOnly = cloudManaged?.effect === "observe" || pack?.effect === "observe";
30223
30153
  const fn = async (ctx) => {
30224
30154
  if (observeOnly) {
30225
- const shadow = await runObserved(hook, ctx, hookName, eventType, cli);
30226
- if (shadow.decision !== "allow") {
30155
+ const observed = await runObserved(hook, ctx, hookName, eventType, cli);
30156
+ if (observed.decision !== "allow") {
30227
30157
  const observer = cloudManaged ?? pack;
30228
30158
  observedResults.push({
30229
30159
  policyId: observer.id,
30230
30160
  version: observer.version,
30231
- decision: shadow.decision,
30232
- reason: shadow.reason ?? null
30161
+ decision: observed.decision,
30162
+ reason: observed.reason ?? null
30233
30163
  });
30234
30164
  }
30235
30165
  return { decision: "allow" };
@@ -30319,13 +30249,13 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
30319
30249
  ...twoTier,
30320
30250
  releaseRegistry: opts?.releaseRegistry
30321
30251
  }) : await evaluatePolicies(canonicalEventType, parsed, session, config);
30322
- const shadowVerdict = result.twoTier?.shadowVerdict;
30323
- if (shadowVerdict) {
30252
+ const observeVerdict = result.twoTier?.observeVerdict;
30253
+ if (observeVerdict) {
30324
30254
  observedResults.push({
30325
- policyId: shadowVerdict.policyName,
30326
- version: shadowVerdict.version,
30327
- decision: shadowVerdict.decision,
30328
- reason: shadowVerdict.reason
30255
+ policyId: observeVerdict.policyName,
30256
+ version: observeVerdict.version,
30257
+ decision: observeVerdict.decision,
30258
+ reason: observeVerdict.reason
30329
30259
  });
30330
30260
  }
30331
30261
  const durationMs = Math.round(performance.now() - startTime);
@@ -31248,7 +31178,7 @@ import {
31248
31178
  rmSync as rmSync9,
31249
31179
  writeFileSync as writeFileSync14
31250
31180
  } from "node:fs";
31251
- import { basename as basename5, dirname as dirname16, join as join32, resolve as resolve24 } from "node:path";
31181
+ import { basename as basename6, dirname as dirname16, join as join32, resolve as resolve24 } from "node:path";
31252
31182
  function migrateToLayout4() {
31253
31183
  const moves = [
31254
31184
  { from: legacy.authJson(), to: auditSessionFile() },
@@ -31284,7 +31214,7 @@ function migrateToLayout4() {
31284
31214
  }
31285
31215
  rmSync9(from, { force: true });
31286
31216
  }
31287
- migrated.push(`${basename5(from)} → audit/${basename5(to)}`);
31217
+ migrated.push(`${basename6(from)} → audit/${basename6(to)}`);
31288
31218
  }
31289
31219
  for (const secret of [auditSessionFile()]) {
31290
31220
  if (!existsSync29(secret))
@@ -31328,7 +31258,7 @@ function appendLedger(entry) {
31328
31258
  } catch {}
31329
31259
  }
31330
31260
  function backupNameOf(f) {
31331
- return f.as ?? basename5(f.at());
31261
+ return f.as ?? basename6(f.at());
31332
31262
  }
31333
31263
  function pruneMigratedCredentials(from) {
31334
31264
  const pruned = [];
@@ -31477,7 +31407,7 @@ var init_migrations = __esm(() => {
31477
31407
  { at: legacy.auditSchedule }
31478
31408
  ];
31479
31409
  PRUNED_AFTER_SUCCESS = [
31480
- { as: basename5(legacy.authJson()), landsAt: auditSessionFile }
31410
+ { as: basename6(legacy.authJson()), landsAt: auditSessionFile }
31481
31411
  ];
31482
31412
  });
31483
31413
 
@@ -31929,7 +31859,7 @@ var init_cli_error = __esm(() => {
31929
31859
  // src/hooks/manager.ts
31930
31860
  import { execSync as execSync6 } from "node:child_process";
31931
31861
  import { existsSync as existsSync31 } from "node:fs";
31932
- import { resolve as resolve26, basename as basename6 } from "node:path";
31862
+ import { resolve as resolve26, basename as basename7 } from "node:path";
31933
31863
  import { platform as platform3, arch as arch3, release as release2, hostname as hostname3 } from "node:os";
31934
31864
  function getSettingsPath(scope, cwd) {
31935
31865
  return claudeCode.getSettingsPath(scope, cwd);
@@ -32204,7 +32134,7 @@ Validated ${validatedHooks.length} custom hook(s) from ${path}: ${validatedHooks
32204
32134
  const alwaysOnNames = new Set(BUILTIN_POLICIES.filter((p) => p.alwaysOn).map((p) => p.name));
32205
32135
  const fromPack = policyNames === undefined ? [] : selectedPolicies.filter((name) => !alwaysOnNames.has(name));
32206
32136
  if (fromPack.length > 0) {
32207
- if (!hasInstalledPacks()) {
32137
+ if (!hasInstalledRegexPacks()) {
32208
32138
  try {
32209
32139
  await addPack(CORE_SOURCE, { only: fromPack });
32210
32140
  } catch (err) {
@@ -32923,7 +32853,7 @@ function fieldsOf(file) {
32923
32853
  } catch {}
32924
32854
  }
32925
32855
  }
32926
- var CLOUD_JEV_INITIAL_MODE = "shadow", MAX_JEV_CONFIG_BYTES;
32856
+ var CLOUD_JEV_INITIAL_MODE = "observe", MAX_JEV_CONFIG_BYTES;
32927
32857
  var init_jev_cloud_connection = __esm(() => {
32928
32858
  init_jev_config();
32929
32859
  MAX_JEV_CONFIG_BYTES = 64 * 1024;
@@ -33058,18 +32988,19 @@ function jevLines(outcome) {
33058
32988
  if (!config || config.status === "written") {
33059
32989
  const plainHttp = config?.status === "written" && new URL(config.baseUrl).protocol === "http:";
33060
32990
  return [
33061
- ` Jev on through FailproofAI Cloud, in shadow mode: logged, not enforced (${config?.path ?? "jev.json"}).`,
33062
- plainHttp ? " Enforce needs an https FailproofAI Cloud URL: reconnect with `failproofai config --token <key> --url https://…`. Over plain http Jev stays in shadow mode." : " Enforce it with `failproofai jev setup --mode enforce`, or from the dashboard."
32991
+ ` Jev on through FailproofAI Cloud, in observe mode: logged, not enforced (${config?.path ?? "jev.json"}).`,
32992
+ ...jevChecksDeclared() ? [] : [" It has no checks installed yet, so it asks nothing. To enable it: `failproofai policies add FailproofAI/jev-policies`."],
32993
+ plainHttp ? " Enforce needs an https FailproofAI Cloud URL: reconnect with `failproofai config --token <key> --url https://…`. Over plain http Jev stays in observe mode." : " Enforce it with `failproofai jev setup --mode enforce`, or from the dashboard."
33063
32994
  ];
33064
32995
  }
33065
32996
  if (config.status === "kept") {
33066
32997
  const lines = [` Jev key stored; ${config.path} already exists and was left as configured.`];
33067
32998
  if (config.otherOrigin) {
33068
- lines.push(config.jevOff?.why === "off" ? ` It points at ${config.otherOrigin} and has Jev switched off (mode off), so Jev is off. To point it here and turn it on: \`failproofai jev setup --provider failproofai --mode shadow\`.` : ` It points at ${config.otherOrigin}, so Jev stays off until you run \`failproofai jev setup --provider failproofai\`.`);
32999
+ lines.push(config.jevOff?.why === "off" ? ` It points at ${config.otherOrigin} and has Jev switched off (mode off), so Jev is off. To point it here and turn it on: \`failproofai jev setup --provider failproofai --mode observe\`.` : ` It points at ${config.otherOrigin}, so Jev stays off until you run \`failproofai jev setup --provider failproofai\`.`);
33069
33000
  } else if (config.jevOff?.why === "refused") {
33070
33001
  lines.push(` It was refused (${config.jevOff.problem}), so Jev is off. ${config.jevOff.fix ? `Fix: \`${config.jevOff.fix}\`.` : "Write a valid one: `failproofai jev setup --provider failproofai`."}`);
33071
33002
  } else if (config.jevOff?.why === "off") {
33072
- lines.push(" It has Jev switched off (mode off), so Jev is off. To turn it on: `failproofai jev setup --mode shadow`.");
33003
+ lines.push(" It has Jev switched off (mode off), so Jev is off. To turn it on: `failproofai jev setup --mode observe`.");
33073
33004
  }
33074
33005
  if (jev.stillOn) {
33075
33006
  lines.push(` Jev is still on through FailproofAI Cloud (${jev.stillOn} mode): it sends each checked tool call and the recent prompt to FailproofAI Cloud. To switch it off: \`failproofai jev setup --mode off\`.`);
@@ -33131,11 +33062,12 @@ var init_cloud_connection = __esm(() => {
33131
33062
  init_cloud_enrollment();
33132
33063
  init_fp_home();
33133
33064
  init_fp_config();
33065
+ init_effective_reviewers();
33134
33066
  init_collector_config();
33135
33067
  });
33136
33068
 
33137
33069
  // src/hooks/setup-state.ts
33138
- import { existsSync as existsSync34, realpathSync as realpathSync2, statSync as statSync24 } from "node:fs";
33070
+ import { existsSync as existsSync34, realpathSync as realpathSync3, statSync as statSync24 } from "node:fs";
33139
33071
  import { dirname as dirname19, resolve as resolve27 } from "node:path";
33140
33072
  import { homedir as homedir26 } from "node:os";
33141
33073
  function globalConfigPath(home = homedir26()) {
@@ -33179,7 +33111,7 @@ function hasGlobalHooksInstalled() {
33179
33111
  function realResolve(path) {
33180
33112
  const lexical = resolve27(path);
33181
33113
  try {
33182
- return realpathSync2(lexical);
33114
+ return realpathSync3(lexical);
33183
33115
  } catch {
33184
33116
  return lexical;
33185
33117
  }
@@ -33350,7 +33282,7 @@ var init_onboarding_lock = __esm(() => {
33350
33282
 
33351
33283
  // lib/claude-sessions.ts
33352
33284
  import { readdirSync as readdirSync19, statSync as statSync25, existsSync as existsSync36 } from "node:fs";
33353
- import { join as join34, basename as basename7 } from "node:path";
33285
+ import { join as join34, basename as basename8 } from "node:path";
33354
33286
  function getClaudeProjectsRoot() {
33355
33287
  return getClaudeProjectsPath();
33356
33288
  }
@@ -37488,7 +37420,7 @@ function surveyReviewableCoverage(cwd) {
37488
37420
  records.push(...selected ? pack.policies.filter((p) => selected.includes(p.name)) : pack.policies);
37489
37421
  }
37490
37422
  const asked = semanticPoliciesFromPacks(jevPacks(packs));
37491
- reviewers = asked.fromPack ? new Set(asked.policies.map((p) => p.name)) : SEMANTIC_REVIEWER_NAMES;
37423
+ reviewers = new Set(asked.policies.map((p) => p.name));
37492
37424
  } catch {}
37493
37425
  let config;
37494
37426
  try {
@@ -37520,7 +37452,7 @@ function surveyReviewableCoverage(cwd) {
37520
37452
  } catch {
37521
37453
  customFiles = 0;
37522
37454
  }
37523
- return { ...countReviewable(records, reviewers), customFiles };
37455
+ return { ...countReviewable(records, reviewers), customFiles, jevChecks: reviewers?.size ?? 0 };
37524
37456
  }
37525
37457
  function reviewableSummary(coverage) {
37526
37458
  const { enabled, reviewable } = coverage;
@@ -37533,11 +37465,13 @@ function reviewableSummary(coverage) {
37533
37465
  return `${reviewable} of ${noun} reviewable: Jev may clear a deny or an instruction from those, and from no others${tail}.`;
37534
37466
  }
37535
37467
  function reviewableProblem(coverage) {
37468
+ if (coverage.jevChecks === 0)
37469
+ return NO_JEV_CHECKS_PROBLEM;
37536
37470
  if (coverage.enabled === 0 || coverage.reviewable > 0 || coverage.customFiles > 0)
37537
37471
  return null;
37538
37472
  return "Jev can add a deny or an instruction on this machine, but it can never clear one. " + "No enabled policy is marked reviewable — a policy pack published before this release carries no such marks — " + `so re-take the pack (\`${RETAKE_PACK_COMMAND}\`) to get a marked copy, ` + "or enforce this build's builtin policies, which carry them.";
37539
37473
  }
37540
- var RETAKE_PACK_COMMAND = "failproofai policies add FailproofAI/policies";
37474
+ var RETAKE_PACK_COMMAND = "failproofai policies add FailproofAI/policies", JEV_POLICIES_ADD_COMMAND = "failproofai policies add FailproofAI/jev-policies", NO_JEV_CHECKS_PROBLEM;
37541
37475
  var init_policy_reviewability = __esm(() => {
37542
37476
  init_cloud_managed_policies();
37543
37477
  init_effective_reviewers();
@@ -37549,6 +37483,7 @@ var init_policy_reviewability = __esm(() => {
37549
37483
  init_policy_catalog();
37550
37484
  init_policy_registry();
37551
37485
  init_pack_policies();
37486
+ NO_JEV_CHECKS_PROBLEM = `Jev has no checks installed, so it asks nothing and clears nothing. To enable it: \`${JEV_POLICIES_ADD_COMMAND}\`.`;
37552
37487
  });
37553
37488
 
37554
37489
  // src/hooks/semantic/jev-stats.ts
@@ -37594,14 +37529,14 @@ function computeJevStats(entries, opts = {}) {
37594
37529
  notConsulted: 0,
37595
37530
  noRequest: 0,
37596
37531
  decisions: { allow: 0, instruct: 0, deny: 0 },
37597
- modes: { shadow: 0, enforce: 0 },
37598
- shadowClearsByPolicy: {},
37532
+ modes: { observe: 0, enforce: 0 },
37533
+ observeClearsByPolicy: {},
37599
37534
  models: {}
37600
37535
  };
37601
37536
  const latencies = [];
37602
37537
  const fallbackReasons = new Counter;
37603
37538
  const clearsByPolicy = new Counter;
37604
- const shadowClearsByPolicy = new Counter;
37539
+ const observeClearsByPolicy = new Counter;
37605
37540
  const models = new Counter;
37606
37541
  for (const raw of entries) {
37607
37542
  if (typeof raw.timestamp !== "number" || raw.timestamp < since || raw.timestamp > now)
@@ -37633,13 +37568,13 @@ function computeJevStats(entries, opts = {}) {
37633
37568
  latencies.push(e.jevLatencyMs);
37634
37569
  if (e.jevModel)
37635
37570
  models.bump(e.jevModel);
37636
- const clears = e.jevMode === "shadow" ? shadowClearsByPolicy : clearsByPolicy;
37571
+ const clears = e.jevMode === "observe" ? observeClearsByPolicy : clearsByPolicy;
37637
37572
  for (const name of e.jevCleared ?? [])
37638
37573
  clears.bump(name);
37639
37574
  }
37640
37575
  stats.fallbackReasons = fallbackReasons.toRecord();
37641
37576
  stats.clearsByPolicy = clearsByPolicy.toRecord();
37642
- stats.shadowClearsByPolicy = shadowClearsByPolicy.toRecord();
37577
+ stats.observeClearsByPolicy = observeClearsByPolicy.toRecord();
37643
37578
  stats.models = models.toRecord();
37644
37579
  stats.fallbackRate = stats.total > 0 ? stats.fallbacks / stats.total : 0;
37645
37580
  latencies.sort((a, b) => a - b);
@@ -37705,7 +37640,7 @@ function parseFlags(argv, allowed) {
37705
37640
  function modeLine(mode) {
37706
37641
  if (mode === "off")
37707
37642
  return "off — Jev is not asked at all; the regex policies decide alone";
37708
- return mode === "enforce" ? "enforce — Jev's verdicts apply: it may clear a reviewable policy's deny and add its own" : "shadow — Jev is asked and logged; the regex result is what is enforced";
37643
+ return mode === "enforce" ? "enforce — Jev's verdicts apply: it may clear a reviewable policy's deny and add its own" : "observe — Jev is asked and logged; the regex result is what is enforced";
37709
37644
  }
37710
37645
  function providerLabel(provider) {
37711
37646
  return provider === JEV_CLOUD_PROVIDER ? "FailproofAI Cloud" : provider;
@@ -37836,14 +37771,14 @@ function jevStatsLines(stats, opts = {}) {
37836
37771
  const reasons = Object.entries(stats.fallbackReasons).sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r} ×${n}`).join(", ");
37837
37772
  const byPolicy = (counts) => Object.entries(counts ?? {}).sort((a, b) => b[1] - a[1]).map(([p, n]) => `${p} ×${n}`).join(", ");
37838
37773
  const clears = byPolicy(stats.clearsByPolicy);
37839
- const shadowClears = byPolicy(stats.shadowClearsByPolicy);
37774
+ const observeClears = byPolicy(stats.observeClearsByPolicy);
37840
37775
  const ms = (v) => v === null ? "—" : `${Math.round(v)} ms`;
37841
37776
  return stack(heading, rows([
37842
37777
  ["evaluations", String(stats.total)],
37843
37778
  ["fell back to regex", `${pct(stats.fallbackRate)}${reasons ? ` (${reasons})` : ""}`],
37844
37779
  ["latency", `p50 ${ms(stats.latencyP50Ms)} · p95 ${ms(stats.latencyP95Ms)}`],
37845
37780
  ["cleared", clears || "nothing"],
37846
- ...shadowClears ? [["would have cleared (shadow)", shadowClears]] : []
37781
+ ...observeClears ? [["would have cleared (observe)", observeClears]] : []
37847
37782
  ], opts));
37848
37783
  }
37849
37784
  function originOf2(provider, baseUrl) {
@@ -38295,8 +38230,7 @@ async function cloudSetup(values, bools, opts) {
38295
38230
  delete next.accountId;
38296
38231
  if (!sameProvider && existing?.raw.timeoutMs !== undefined)
38297
38232
  next.timeoutMs = existing.raw.timeoutMs;
38298
- const mode = values.get("--mode") ?? (sameProvider && typeof next.mode === "string" ? next.mode : "shadow");
38299
- next.mode = mode;
38233
+ next.mode = values.get("--mode") ?? (sameProvider && typeof next.mode === "string" ? next.mode : "observe");
38300
38234
  if (values.has("--timeout-ms")) {
38301
38235
  const rawTimeout = values.get("--timeout-ms");
38302
38236
  const n = Number(rawTimeout);
@@ -38325,7 +38259,7 @@ async function cloudSetup(values, bools, opts) {
38325
38259
  `Reconnect with this machine's key to re-check what it carries (an older failproofai connected without storing its Jev key; a key without jev:evaluate needs the "machine" preset on the dashboard's Keys page):`
38326
38260
  ] : [
38327
38261
  "Not saved: this machine is not connected to FailproofAI Cloud with a key that carries jev:evaluate.",
38328
- 'Connect it with one that does (the "machine" preset) — that also turns Jev on, in shadow mode, when there is no jev.json yet:'
38262
+ 'Connect it with one that does (the "machine" preset) — that also turns Jev on, in observe mode, when there is no jev.json yet:'
38329
38263
  ],
38330
38264
  " failproofai config --token <key>",
38331
38265
  "",
@@ -38392,7 +38326,7 @@ function displayOnlyCloudOrigin(routing) {
38392
38326
  function namedEndpoint(raw) {
38393
38327
  if (!raw || typeof raw.provider !== "string" || !JEV_PROVIDER_KINDS.includes(raw.provider))
38394
38328
  return null;
38395
- const routing = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "shadow", ...displayOnlyCloudOrigin(raw) };
38329
+ const routing = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "observe", ...displayOnlyCloudOrigin(raw) };
38396
38330
  if (raw.baseUrl !== undefined)
38397
38331
  routing.baseUrl = raw.baseUrl;
38398
38332
  if (raw.accountId !== undefined)
@@ -38501,6 +38435,7 @@ async function status(argv, opts) {
38501
38435
  enabled: coverage.enabled,
38502
38436
  reviewable: coverage.reviewable,
38503
38437
  customPolicyFiles: coverage.customFiles,
38438
+ jevChecks: coverage.jevChecks,
38504
38439
  problem: coverageProblem
38505
38440
  }
38506
38441
  } : {}
@@ -38543,7 +38478,7 @@ async function status(argv, opts) {
38543
38478
  ["mode", modeLine("off")],
38544
38479
  ["config", inspection.path],
38545
38480
  ...r.provider === JEV_CLOUD_PROVIDER ? [["key", cloudKeyRow()]] : []
38546
- ], opts), nextStep("failproofai jev setup --mode shadow", "Switch it back on (shadow logs Jev and keeps enforcing regex; enforce lets it clear), here or from the dashboard:", opts), legacyNote, jevStatsLines(stats, opts)));
38481
+ ], opts), nextStep("failproofai jev setup --mode observe", "Switch it back on (observe logs Jev and keeps enforcing regex; enforce lets it clear), here or from the dashboard:", opts), legacyNote, jevStatsLines(stats, opts)));
38547
38482
  }
38548
38483
  if (inspection.status === "not-connected") {
38549
38484
  const r = inspection.routing;
@@ -38615,7 +38550,7 @@ async function test(argv, deps, opts) {
38615
38550
  const why = inspection.status === "absent" ? `There is no ${inspection.path}; nothing to test.` : inspection.status === "key-missing" ? `${inspection.path} takes its key from ${JEV_API_KEY_ENV}, which is not set in this shell, so there is no key to test with.` : inspection.status === "off" ? `Jev is switched off in ${inspection.path} (mode off), so nothing is sent.` : inspection.status === "not-connected" ? `${inspection.path} sends Jev requests through FailproofAI Cloud, and this machine is not connected to it with a key that carries jev:evaluate.` : inspection.status === "key-lacks-jev" ? `${inspection.path} sends Jev requests through FailproofAI Cloud, and no Jev key is stored for this machine's connection.` : `${inspection.path} was refused — ${inspection.problem}.`;
38616
38551
  const code = inspection.status === "absent" ? "not-configured" : inspection.status === "key-missing" ? "no-env-key" : inspection.status === "off" ? "switched-off" : inspection.status === "not-connected" || inspection.status === "key-lacks-jev" ? inspection.status : "config";
38617
38552
  const refusedNext = inspection.status === "refused" ? refusedNextStep(inspection, cloudRoute(readJevConfigFileForUpdate()?.raw)) : inspection.status === "absent" && cloudCredentialPresent() ? { cmd: "failproofai jev setup --provider failproofai", lead: "This machine's FailproofAI Cloud key already carries jev:evaluate. Turn it on:" } : null;
38618
- const fixCmd = refusedNext ? refusedNext.cmd : inspection.status === "key-missing" ? "failproofai jev setup --key-stdin < key-file" : inspection.status === "off" ? "failproofai jev setup --mode shadow" : inspection.status === "not-connected" || inspection.status === "key-lacks-jev" ? "failproofai config --token <key>" : "failproofai jev setup --provider <kind> --key-stdin";
38553
+ const fixCmd = refusedNext ? refusedNext.cmd : inspection.status === "key-missing" ? "failproofai jev setup --key-stdin < key-file" : inspection.status === "off" ? "failproofai jev setup --mode observe" : inspection.status === "not-connected" || inspection.status === "key-lacks-jev" ? "failproofai config --token <key>" : "failproofai jev setup --provider <kind> --key-stdin";
38619
38554
  const fixLead = refusedNext ? refusedNext.lead : inspection.status === "key-missing" ? `Set ${JEV_API_KEY_ENV} for this shell, or store the key in the file:` : inspection.status === "off" ? "Switch it back on:" : inspection.status === "not-connected" ? "Connect with a key that carries jev:evaluate:" : inspection.status === "key-lacks-jev" ? `Reconnect with this machine's key to re-check what it carries (a key without jev:evaluate needs the "machine" preset on the dashboard's Keys page):` : undefined;
38620
38555
  const json = asJson ? JSON.stringify({ ok: false, error: { code, message: why } }, null, 2) : undefined;
38621
38556
  return fail2(stack(title("failproofai jev test", "not run", opts), note(why, opts), nextStep(fixCmd, fixLead, opts)), json);
@@ -38844,7 +38779,7 @@ var init_jev_cli = __esm(() => {
38844
38779
  "Usage:",
38845
38780
  " failproofai jev --url <url> [--key-stdin | --token <token>] [options]",
38846
38781
  " failproofai jev setup --provider <kind> [--key-stdin | --key-from-env] [options]",
38847
- " failproofai jev setup --provider failproofai [--mode off|shadow|enforce] (FailproofAI Cloud: no key, no URL)",
38782
+ " failproofai jev setup --provider failproofai [--mode off|observe|enforce] (FailproofAI Cloud: no key, no URL)",
38848
38783
  " failproofai jev status [--json]",
38849
38784
  " failproofai jev test [--json]",
38850
38785
  " failproofai jev models [--provider <kind>] [--url <base>] [--json]",
@@ -38954,7 +38889,7 @@ import {
38954
38889
  rmSync as rmSync16,
38955
38890
  writeFileSync as writeFileSync26
38956
38891
  } from "node:fs";
38957
- import { basename as basename8, dirname as dirname25, join as join39, resolve as resolve32 } from "node:path";
38892
+ import { basename as basename9, dirname as dirname25, join as join39, resolve as resolve32 } from "node:path";
38958
38893
  function migrateToLayout42() {
38959
38894
  const moves = [
38960
38895
  { from: legacy.authJson(), to: auditSessionFile() },
@@ -38990,7 +38925,7 @@ function migrateToLayout42() {
38990
38925
  }
38991
38926
  rmSync16(from, { force: true });
38992
38927
  }
38993
- migrated.push(`${basename8(from)} → audit/${basename8(to)}`);
38928
+ migrated.push(`${basename9(from)} → audit/${basename9(to)}`);
38994
38929
  }
38995
38930
  for (const secret of [auditSessionFile()]) {
38996
38931
  if (!existsSync46(secret))
@@ -39034,7 +38969,7 @@ function appendLedger2(entry) {
39034
38969
  } catch {}
39035
38970
  }
39036
38971
  function backupNameOf2(f) {
39037
- return f.as ?? basename8(f.at());
38972
+ return f.as ?? basename9(f.at());
39038
38973
  }
39039
38974
  function pruneMigratedCredentials2(from) {
39040
38975
  const pruned = [];
@@ -39205,7 +39140,7 @@ var init_migrations2 = __esm(() => {
39205
39140
  { at: legacy.auditSchedule }
39206
39141
  ];
39207
39142
  PRUNED_AFTER_SUCCESS2 = [
39208
- { as: basename8(legacy.authJson()), landsAt: auditSessionFile }
39143
+ { as: basename9(legacy.authJson()), landsAt: auditSessionFile }
39209
39144
  ];
39210
39145
  });
39211
39146
 
@@ -39556,6 +39491,34 @@ async function installDaemonService2() {
39556
39491
  }
39557
39492
  return { installed: true };
39558
39493
  }
39494
+ async function probeDaemon2() {
39495
+ try {
39496
+ await Promise.resolve().then(() => init_daemon_client2());
39497
+ const deadline = Date.now() + DAEMON_PROBE_READY_TIMEOUT_MS2;
39498
+ let everConnected = false;
39499
+ for (;; ) {
39500
+ if (await daemonAcceptsConnections()) {
39501
+ everConnected = true;
39502
+ const attempt = await attemptDaemonHook2({
39503
+ hookEvent: "SessionStart",
39504
+ cli: "claude",
39505
+ stdin: JSON.stringify({
39506
+ hook_event_name: "SessionStart",
39507
+ source: "failproofai-health-probe"
39508
+ })
39509
+ }, { responseTimeoutMs: DAEMON_PROBE_TIMEOUT_MS2 });
39510
+ if (attempt.ok || attempt.failure === "protocol-mismatch")
39511
+ return { ok: true };
39512
+ }
39513
+ if (Date.now() >= deadline)
39514
+ break;
39515
+ await new Promise((r) => setTimeout(r, DAEMON_PROBE_RETRY_MS2));
39516
+ }
39517
+ return { ok: false, reason: everConnected ? "worker" : "unreachable" };
39518
+ } catch {
39519
+ return { ok: false, reason: "unreachable" };
39520
+ }
39521
+ }
39559
39522
  async function daemonAcceptsConnectionsQuietly2() {
39560
39523
  try {
39561
39524
  await Promise.resolve().then(() => init_daemon_client2());
@@ -39564,6 +39527,27 @@ async function daemonAcceptsConnectionsQuietly2() {
39564
39527
  return false;
39565
39528
  }
39566
39529
  }
39530
+ async function probeDaemonEndToEnd2() {
39531
+ return (await probeDaemon2()).ok;
39532
+ }
39533
+ async function probeDaemonPolicyEvaluation2() {
39534
+ if (!await probeDaemonEndToEnd2())
39535
+ return false;
39536
+ try {
39537
+ await Promise.resolve().then(() => init_daemon_client2());
39538
+ const attempt = await attemptDaemonPolicyEvaluation({
39539
+ integration: "hermes",
39540
+ event: "on_session_start",
39541
+ payload: {
39542
+ hook_event_name: "on_session_start",
39543
+ source: "failproofai-native-policy-health-probe"
39544
+ }
39545
+ }, { responseTimeoutMs: DAEMON_PROBE_TIMEOUT_MS2 });
39546
+ return attempt.ok;
39547
+ } catch {
39548
+ return false;
39549
+ }
39550
+ }
39567
39551
  async function waitForDaemonRunning2(deps = {}) {
39568
39552
  const status = deps.status ?? daemonServiceStatus2;
39569
39553
  const accepts = deps.accepts ?? daemonAcceptsConnectionsQuietly2;
@@ -39668,7 +39652,7 @@ function daemonServiceStatus2() {
39668
39652
  return "stopped";
39669
39653
  }
39670
39654
  }
39671
- var SERVICE_CMD_TIMEOUT_MS2 = 1e4, SERVICE_START_TIMEOUT_MS2 = 5000, SERVICE_START_POLL_MS2 = 100, SERVICE_SETTLE_MS2 = 750, SUDO_PROMPT_TIMEOUT_MS2 = 120000, UNIT_UNSAFE_RE2;
39655
+ var SERVICE_CMD_TIMEOUT_MS2 = 1e4, SERVICE_START_TIMEOUT_MS2 = 5000, SERVICE_START_POLL_MS2 = 100, SERVICE_SETTLE_MS2 = 750, SUDO_PROMPT_TIMEOUT_MS2 = 120000, UNIT_UNSAFE_RE2, DAEMON_PROBE_TIMEOUT_MS2 = 5000, DAEMON_PROBE_READY_TIMEOUT_MS2 = 1e4, DAEMON_PROBE_RETRY_MS2 = 150;
39672
39656
  var init_daemon_service2 = __esm(() => {
39673
39657
  init_hook_logger();
39674
39658
  init_daemon_download();
@@ -39678,6 +39662,44 @@ var init_daemon_service2 = __esm(() => {
39678
39662
  UNIT_UNSAFE_RE2 = /["\\\r\n]/;
39679
39663
  });
39680
39664
 
39665
+ // src/hooks/hermes-update.ts
39666
+ async function runHermesUpdateMigration(deps) {
39667
+ const profiles = await migrateHermesProfiles(deps);
39668
+ const relevant = profiles.filter((p) => p.status !== "untouched");
39669
+ if (relevant.length === 0)
39670
+ return { ok: true, lines: [], profiles };
39671
+ const width = Math.max(...profiles.map((p) => ("hermes/" + p.name).length));
39672
+ const lines = ["Hermes:"];
39673
+ for (const p of profiles) {
39674
+ const label = ("hermes/" + p.name).padEnd(width);
39675
+ const detail = p.status === "current" ? "" : ` — ${p.detail}`;
39676
+ lines.push(` ${label} ${STATUS_LABEL[p.status]}${detail}`);
39677
+ }
39678
+ const blocked = profiles.filter((p) => p.status === "blocked");
39679
+ const failed = profiles.filter((p) => p.status === "failed");
39680
+ if (profiles.some((p) => p.status === "migrated")) {
39681
+ lines.push(" Cron jobs load the plugin on their next run; restart running Hermes gateways and sessions to load it there.");
39682
+ }
39683
+ if (blocked.length > 0) {
39684
+ lines.push("", `Hermes is NOT migrated (${blocked.map((p) => p.name).join(", ")}): the running failproofaid cannot`, "answer the native plugin (no policyEvaluation), so nothing was changed. Profiles still on", "shell hooks do NOT check Hermes cron jobs. Run `failproofai config` (it asks for sudo to", "replace the daemon), then `failproofai update` again.");
39685
+ }
39686
+ if (failed.length > 0) {
39687
+ lines.push("", `Hermes could not be migrated for: ${failed.map((p) => p.name).join(", ")}. Fix the reason above,`, "then run `failproofai update` again.");
39688
+ }
39689
+ return { ok: blocked.length === 0 && failed.length === 0, lines, profiles };
39690
+ }
39691
+ var STATUS_LABEL;
39692
+ var init_hermes_update = __esm(() => {
39693
+ init_integrations();
39694
+ STATUS_LABEL = {
39695
+ migrated: "migrated",
39696
+ current: "already current",
39697
+ untouched: "skipped",
39698
+ blocked: "NOT migrated",
39699
+ failed: "FAILED"
39700
+ };
39701
+ });
39702
+
39681
39703
  // src/hooks/uninstall-cli.ts
39682
39704
  import { existsSync as existsSync48, rmSync as rmSync18 } from "node:fs";
39683
39705
  function survey(cwd) {
@@ -39857,8 +39879,8 @@ var init_uninstall_cli = __esm(() => {
39857
39879
  import { execFileSync as execFileSync4 } from "node:child_process";
39858
39880
  import { homedir as homedir32, tmpdir as tmpdir4 } from "node:os";
39859
39881
  import { createHash as createHash11 } from "node:crypto";
39860
- import { copyFileSync as copyFileSync4, existsSync as existsSync49, mkdirSync as mkdirSync31, mkdtempSync as mkdtempSync3, readFileSync as readFileSync38, readdirSync as readdirSync23, realpathSync as realpathSync3, rmSync as rmSync19, statSync as statSync33, writeFileSync as writeFileSync28 } from "node:fs";
39861
- import { basename as basename9, dirname as dirname26, isAbsolute as isAbsolute6, join as join40, relative as relative5, resolve as resolve34, sep as sep6 } from "node:path";
39882
+ import { copyFileSync as copyFileSync4, existsSync as existsSync49, mkdirSync as mkdirSync31, mkdtempSync as mkdtempSync3, readFileSync as readFileSync38, readdirSync as readdirSync23, realpathSync as realpathSync4, rmSync as rmSync19, statSync as statSync33, writeFileSync as writeFileSync28 } from "node:fs";
39883
+ import { basename as basename10, dirname as dirname26, isAbsolute as isAbsolute6, join as join40, relative as relative5, resolve as resolve34, sep as sep6 } from "node:path";
39862
39884
  function parseList(rest, ...flags) {
39863
39885
  let found = false;
39864
39886
  const out = [];
@@ -40058,10 +40080,10 @@ async function build(rest) {
40058
40080
  const requiredCli = semantic.length > 0 ? minCliVersion ?? JEV_PACK_MIN_CLI : minCliVersion;
40059
40081
  const questionCost = semantic.reduce((total, entry) => total + questionChars(entry), 0);
40060
40082
  const firstParty = isFirstPartyPack(packSource);
40061
- const questionBudget = MAX_PACK_QUESTION_CHARS - (firstParty ? 0 : BUILTIN_QUESTION_CHARS);
40083
+ const questionBudget = MAX_PACK_QUESTION_CHARS - (firstParty ? 0 : JEV_POLICIES_QUESTION_CHARS);
40062
40084
  if (questionCost > questionBudget) {
40063
40085
  return fail3([
40064
- `This pack's ${semantic.length} semantic policies compile to ${questionCost} characters of questions, ` + (firstParty ? `over the ${MAX_PACK_QUESTION_CHARS} one Jev request has room for.` : `over the ${questionBudget} a machine leaves a pack from outside FailproofAI: one Jev request has room ` + `for ${MAX_PACK_QUESTION_CHARS}, and the 16 built-in checks every machine asks take ${BUILTIN_QUESTION_CHARS} of it first.`),
40086
+ `This pack's ${semantic.length} semantic policies compile to ${questionCost} characters of questions, ` + (firstParty ? `over the ${MAX_PACK_QUESTION_CHARS} one Jev request has room for.` : `over the ${questionBudget} a machine leaves a pack from outside FailproofAI: one Jev request has room ` + `for ${MAX_PACK_QUESTION_CHARS}, and FailproofAI/jev-policies' 16 checks take ${JEV_POLICIES_QUESTION_CHARS} of it first ` + `where both are installed.`),
40065
40087
  "Shorten the probe instructions and criteria, or ship fewer policies per pack."
40066
40088
  ]);
40067
40089
  }
@@ -40129,7 +40151,7 @@ ${sha(readFileSync38(entryOut))} ${PACK_ENTRY_ASSET}
40129
40151
  return ok3([
40130
40152
  `Built ${identity.id}@${identity.version} — ${policies.length} policies, ${on} on by default.`,
40131
40153
  ...semantic.length > 0 ? [
40132
- ` ${semantic.length} semantic ${semantic.length === 1 ? "policy" : "policies"} for Jev ` + `(${questionCost} characters of questions), ` + (identity.effect === "observe" ? "not asked where it installs: Jev asks only the checks of packs that enforce." : `${firstParty ? "replacing" : "added to"} the built-in checks where it installs.`)
40154
+ ` ${semantic.length} semantic ${semantic.length === 1 ? "policy" : "policies"} for Jev ` + `(${questionCost} characters of questions), ` + (identity.effect === "observe" ? "not asked where it installs: Jev asks only the checks of packs that enforce." : "asked by Jev wherever it installs.")
40133
40155
  ] : [],
40134
40156
  ...requiredCli ? [` Requires failproofai ${requiredCli} or newer.`] : [],
40135
40157
  ...semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : [],
@@ -40365,7 +40387,7 @@ function porcelainEntries(status) {
40365
40387
  }
40366
40388
  function realOrSelf(path) {
40367
40389
  try {
40368
- return realpathSync3(path);
40390
+ return realpathSync4(path);
40369
40391
  } catch {
40370
40392
  return path;
40371
40393
  }
@@ -40920,12 +40942,12 @@ async function publish(rest) {
40920
40942
  if (askingWhere) {
40921
40943
  const found = discovered.length > 0 ? discovered : [entry];
40922
40944
  process.stdout.write(`
40923
- ${found.length} policy ${found.length === 1 ? "file" : "files"}: ` + `${found.map((f) => basename9(f)).join(", ")}
40945
+ ${found.length} policy ${found.length === 1 ? "file" : "files"}: ` + `${found.map((f) => basename10(f)).join(", ")}
40924
40946
 
40925
40947
  `);
40926
40948
  const token = githubToken();
40927
40949
  const login = token ? await credentialLogin(token) : null;
40928
- const folder = basename9(resolve34(entry, ".."));
40950
+ const folder = basename10(resolve34(entry, ".."));
40929
40951
  const answered = await askWhereToPublish(`${login ?? "your-account"}/${folder}`, { stdin: process.stdin, stdout: process.stdout });
40930
40952
  if (answered === null)
40931
40953
  return ok3(["Nothing was published."]);
@@ -40936,7 +40958,7 @@ async function publish(rest) {
40936
40958
  let versionFromSha = false;
40937
40959
  const dryRun = rest.includes("--dry-run") || !repo;
40938
40960
  const allowPrivate = rest.includes("--allow-private");
40939
- const id = flag("id") ?? repo ?? (dryRun && entry ? `local/${basename9(resolve34(entry, ".."))}` : undefined);
40961
+ const id = flag("id") ?? repo ?? (dryRun && entry ? `local/${basename10(resolve34(entry, ".."))}` : undefined);
40940
40962
  const [owner, name] = (repo ?? "/").split("/");
40941
40963
  if (repo && (!owner || !name || repo.split("/").length !== 2)) {
40942
40964
  return fail3([`--repo must be <owner>/<repo>, got ${JSON.stringify(repo)}`]);
@@ -41176,11 +41198,11 @@ async function pickClis(io) {
41176
41198
  function semanticPhrase(count) {
41177
41199
  return `${count} Jev ${count === 1 ? "check" : "checks"}`;
41178
41200
  }
41179
- function besideBuiltinChecks(pack) {
41201
+ function howJevAsksChecks(pack) {
41180
41202
  if (jevPacks([{ effect: pack.effect ?? "enforce", clis: null }]).length === 0) {
41181
41203
  return "not asked: this pack only observes, and Jev asks only the checks of packs that enforce";
41182
41204
  }
41183
- const how = isFirstPartyPack(pack) ? "replacing this build's own set" : "added to this build's own checks";
41205
+ const how = "asked by Jev on every tool call they apply to";
41184
41206
  return pack.clis && pack.clis.length > 0 ? `${how}, for ${pack.clis.join(", ")} only` : how;
41185
41207
  }
41186
41208
  function jevCheckWarnings(id, preview) {
@@ -41217,7 +41239,7 @@ async function pickFromSource(source, io) {
41217
41239
  const on = preview.policies.filter((p) => p.defaultEnabled).length;
41218
41240
  if (preview.semantic.length > 0) {
41219
41241
  io.stdout.write(`
41220
- This pack also brings ${semanticPhrase(preview.semantic.length)} (not selectable), ` + `${besideBuiltinChecks(preview)}.
41242
+ This pack also brings ${semanticPhrase(preview.semantic.length)} (not selectable), ` + `${howJevAsksChecks(preview)}.
41221
41243
  See: failproofai policies show ${source}
41222
41244
 
41223
41245
  `);
@@ -41284,7 +41306,7 @@ async function add(rest) {
41284
41306
  }[result.selection];
41285
41307
  lines.push(result.enabled.length === 0 ? ` enabled (0/${result.available.length}, ${why}): none` + (result.semantic > 0 ? "" : " — the pack is installed and enforcing nothing") : ` enabled (${result.enabled.length}/${result.available.length}, ${why}): ${summarise(result.enabled)}`);
41286
41308
  if (result.semantic > 0) {
41287
- lines.push(` ${semanticPhrase(result.semantic)}, ${besideBuiltinChecks(result)}. ` + "They apply only where you configured Jev (`failproofai jev status`).");
41309
+ lines.push(` ${semanticPhrase(result.semantic)}, ${howJevAsksChecks(result)}. ` + "They apply only where you configured Jev (`failproofai jev status`).");
41288
41310
  }
41289
41311
  if (result.minCliVersionNote)
41290
41312
  lines.push(` ${result.minCliVersionNote}`);
@@ -41475,7 +41497,7 @@ function jevChecksSection(pack, opts) {
41475
41497
  ...rule(`Jev checks — ${pack.semantic.length} · not selectable · only where Jev is configured`, opts),
41476
41498
  ...table({ head: ["", "", ""], rows, protect: [0, 1] }, opts),
41477
41499
  "",
41478
- ...note("Nothing toggles them: `--policy` cannot name one and `failproofai policies` never lists them. " + `They arrive whole, ${besideBuiltinChecks(pack)}.`, opts),
41500
+ ...note("Nothing toggles them: `--policy` cannot name one and `failproofai policies` never lists them. " + `They arrive whole, ${howJevAsksChecks(pack)}.`, opts),
41479
41501
  ...note(pack.policies.length === 0 ? "This pack ships no regex policies, so none of these checks clears anything here: a deny " + "check can only ADD a deny, and an instruct check only ever warns." : `\`reviews\` is which of this pack's own ${pack.policies.length} policies each check can clear. ` + "A deny check that reviews nothing can only ever ADD a deny; an instruct check never answers " + "deny, so nothing pairs with one.", opts)
41480
41502
  ];
41481
41503
  }
@@ -41884,7 +41906,7 @@ var init_install_prompt = __esm(() => {
41884
41906
  // src/hooks/manager.ts
41885
41907
  import { execSync as execSync7 } from "node:child_process";
41886
41908
  import { existsSync as existsSync50 } from "node:fs";
41887
- import { resolve as resolve35, basename as basename10 } from "node:path";
41909
+ import { resolve as resolve35, basename as basename11 } from "node:path";
41888
41910
  import { platform as platform4, arch as arch4, release as release3, hostname as hostname6 } from "node:os";
41889
41911
  function getSettingsPath2(scope, cwd) {
41890
41912
  return claudeCode.getSettingsPath(scope, cwd);
@@ -42159,7 +42181,7 @@ Validated ${validatedHooks.length} custom hook(s) from ${path}: ${validatedHooks
42159
42181
  const alwaysOnNames = new Set(BUILTIN_POLICIES.filter((p) => p.alwaysOn).map((p) => p.name));
42160
42182
  const fromPack = policyNames === undefined ? [] : selectedPolicies.filter((name) => !alwaysOnNames.has(name));
42161
42183
  if (fromPack.length > 0) {
42162
- if (!hasInstalledPacks()) {
42184
+ if (!hasInstalledRegexPacks()) {
42163
42185
  try {
42164
42186
  await addPack(CORE_SOURCE, { only: fromPack });
42165
42187
  } catch (err) {
@@ -42545,7 +42567,7 @@ async function listHooks(cwd) {
42545
42567
  groups.push(note(dir, opts));
42546
42568
  const rows = [];
42547
42569
  for (const file of files) {
42548
- const filename = basename10(file);
42570
+ const filename = basename11(file);
42549
42571
  try {
42550
42572
  const hooks = await loadCustomHooks(file);
42551
42573
  record(filename, hooks.map((h) => h.name));
@@ -43069,13 +43091,13 @@ function parseScriptArgs(argv) {
43069
43091
  var init_parse_script_args = () => {};
43070
43092
 
43071
43093
  // scripts/install-diagnosis.mjs
43072
- import { existsSync as existsSync51, readFileSync as readFileSync40, realpathSync as realpathSync4 } from "node:fs";
43094
+ import { existsSync as existsSync51, readFileSync as readFileSync40, realpathSync as realpathSync5 } from "node:fs";
43073
43095
  import { dirname as dirname27, resolve as resolve37 } from "node:path";
43074
43096
  import { homedir as homedir33, platform as platform5 } from "node:os";
43075
43097
  import { spawnSync } from "node:child_process";
43076
43098
  function findPackageRoot(start) {
43077
43099
  try {
43078
- let dir = realpathSync4(start);
43100
+ let dir = realpathSync5(start);
43079
43101
  if (existsSync51(dir) && !existsSync51(resolve37(dir, "package.json"))) {
43080
43102
  dir = dirname27(dir);
43081
43103
  }
@@ -43155,7 +43177,7 @@ function buildRecommendation(pathFirstBin) {
43155
43177
  function diagnoseShadow(self) {
43156
43178
  const selfPackageRoot = (() => {
43157
43179
  try {
43158
- return self?.selfPackageRoot ? realpathSync4(self.selfPackageRoot) : null;
43180
+ return self?.selfPackageRoot ? realpathSync5(self.selfPackageRoot) : null;
43159
43181
  } catch {
43160
43182
  return self?.selfPackageRoot ?? null;
43161
43183
  }
@@ -43239,7 +43261,7 @@ var init_dashboard_host = __esm(() => {
43239
43261
 
43240
43262
  // scripts/launch.ts
43241
43263
  import { spawn as spawn2 } from "child_process";
43242
- import { realpathSync as realpathSync5, existsSync as existsSync52 } from "node:fs";
43264
+ import { realpathSync as realpathSync6, existsSync as existsSync52 } from "node:fs";
43243
43265
  import { resolve as resolve38, dirname as dirname28 } from "node:path";
43244
43266
  import { fileURLToPath as fileURLToPath2 } from "node:url";
43245
43267
  import { createInterface } from "node:readline";
@@ -43264,7 +43286,7 @@ function launch(mode) {
43264
43286
  process.env.PORT = port;
43265
43287
  process.env.HOSTNAME = bindHost;
43266
43288
  cmd = "node";
43267
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve38(dirname28(realpathSync5(fileURLToPath2(import.meta.url))), "..");
43289
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve38(dirname28(realpathSync6(fileURLToPath2(import.meta.url))), "..");
43268
43290
  const serverJsPath = resolve38(packageRoot, ".next/standalone/server.js");
43269
43291
  if (!existsSync52(serverJsPath)) {
43270
43292
  let shadowMessage = null;
@@ -44044,11 +44066,15 @@ import {
44044
44066
  existsSync as existsSync53,
44045
44067
  lstatSync as lstatSync3,
44046
44068
  mkdirSync as mkdirSync32,
44069
+ readlinkSync as readlinkSync2,
44070
+ realpathSync as realpathSync7,
44047
44071
  renameSync as renameSync13,
44048
44072
  rmSync as rmSync20,
44073
+ symlinkSync as symlinkSync2,
44049
44074
  unlinkSync as unlinkSync10
44050
44075
  } from "node:fs";
44051
- import { resolve as resolve39, dirname as dirname29 } from "node:path";
44076
+ import { resolve as resolve39, dirname as dirname29, basename as basename12 } from "node:path";
44077
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
44052
44078
  function readYamlDoc2(path) {
44053
44079
  try {
44054
44080
  return existsSync53(path) ? $parseDocument(readFileSync41(path, "utf8")) : $parseDocument("");
@@ -44065,21 +44091,71 @@ function isMarkedHook2(hook) {
44065
44091
  const cmd = typeof h.command === "string" ? h.command : "";
44066
44092
  return cmd.includes("failproofai") && cmd.includes("--hook");
44067
44093
  }
44094
+ function getHermesPluginSourcePath2() {
44095
+ const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
44096
+ if (fromEnv)
44097
+ return resolve39(fromEnv, HERMES_PLUGIN_SOURCE_DIR2);
44098
+ return resolve39(fileURLToPath3(import.meta.url), "..", "..", "..", HERMES_PLUGIN_SOURCE_DIR2);
44099
+ }
44068
44100
  function hermesPluginPathForSettings2(settingsPath) {
44069
44101
  return resolve39(dirname29(settingsPath), "plugins", HERMES_PLUGIN_ID2);
44070
44102
  }
44071
- function hasManagedHermesPlugin2(settingsPath) {
44072
- const pluginPath = hermesPluginPathForSettings2(settingsPath);
44103
+ function hasAllHermesPluginFiles2(dir) {
44104
+ return HERMES_PLUGIN_FILES2.every((file) => existsSync53(resolve39(dir, file)));
44105
+ }
44106
+ function isFailproofaiHermesPluginSource2(dir) {
44107
+ if (basename12(dir) !== HERMES_PLUGIN_SOURCE_DIR2)
44108
+ return false;
44109
+ if (!existsSync53(dir))
44110
+ return true;
44073
44111
  try {
44074
- if (lstatSync3(pluginPath).isSymbolicLink())
44075
- return false;
44076
- if (!existsSync53(resolve39(pluginPath, HERMES_PLUGIN_MARKER2)))
44077
- return false;
44078
- return HERMES_PLUGIN_FILES2.every((file) => existsSync53(resolve39(pluginPath, file)));
44112
+ const manifest = readFileSync41(resolve39(dir, "plugin.yaml"), "utf8");
44113
+ return /^name:\s*["']?failproofai["']?\s*$/m.test(manifest);
44114
+ } catch {
44115
+ return false;
44116
+ }
44117
+ }
44118
+ function samePath2(a, b) {
44119
+ if (resolve39(a) === resolve39(b))
44120
+ return true;
44121
+ try {
44122
+ return realpathSync7(a) === realpathSync7(b);
44079
44123
  } catch {
44080
44124
  return false;
44081
44125
  }
44082
44126
  }
44127
+ function hermesPluginState2(settingsPath) {
44128
+ const pluginPath = hermesPluginPathForSettings2(settingsPath);
44129
+ let isLink;
44130
+ let isDirectory;
44131
+ try {
44132
+ const stat = lstatSync3(pluginPath);
44133
+ isLink = stat.isSymbolicLink();
44134
+ isDirectory = stat.isDirectory();
44135
+ } catch {
44136
+ return { kind: "absent" };
44137
+ }
44138
+ if (isLink) {
44139
+ let target;
44140
+ try {
44141
+ target = resolve39(dirname29(pluginPath), readlinkSync2(pluginPath));
44142
+ } catch {
44143
+ return { kind: "foreign" };
44144
+ }
44145
+ if (!isFailproofaiHermesPluginSource2(target))
44146
+ return { kind: "foreign" };
44147
+ return {
44148
+ kind: "link",
44149
+ target,
44150
+ complete: hasAllHermesPluginFiles2(target),
44151
+ current: samePath2(target, getHermesPluginSourcePath2())
44152
+ };
44153
+ }
44154
+ if (isDirectory && existsSync53(resolve39(pluginPath, HERMES_PLUGIN_MARKER2))) {
44155
+ return { kind: "copy", complete: hasAllHermesPluginFiles2(pluginPath) };
44156
+ }
44157
+ return { kind: "foreign" };
44158
+ }
44083
44159
  function hermesConfigState2(settingsPath) {
44084
44160
  if (!existsSync53(settingsPath)) {
44085
44161
  return { pluginEnabled: false, legacyShellHookPresent: false };
@@ -44097,20 +44173,31 @@ function hermesProfileHealth() {
44097
44173
  return listHermesProfiles().map((profile) => {
44098
44174
  const settingsPath = resolve39(profile.home, "config.yaml");
44099
44175
  const config = hermesConfigState2(settingsPath);
44100
- const pluginInstalled = hasManagedHermesPlugin2(settingsPath);
44176
+ const plugin = hermesPluginState2(settingsPath);
44177
+ const pluginInstalled = (plugin.kind === "link" || plugin.kind === "copy") && plugin.complete;
44178
+ const pluginWorks = pluginInstalled && config.pluginEnabled;
44101
44179
  return {
44102
44180
  name: profile.name,
44103
44181
  home: profile.home,
44104
44182
  settingsPath,
44105
44183
  pluginInstalled,
44184
+ pluginMode: plugin.kind === "link" || plugin.kind === "copy" ? plugin.kind : null,
44106
44185
  pluginEnabled: config.pluginEnabled,
44107
44186
  legacyShellHookPresent: config.legacyShellHookPresent,
44108
- healthy: pluginInstalled && config.pluginEnabled && !config.legacyShellHookPresent
44187
+ cronUnchecked: config.legacyShellHookPresent && !pluginWorks,
44188
+ healthy: pluginWorks && !config.legacyShellHookPresent
44109
44189
  };
44110
44190
  });
44111
44191
  }
44112
44192
  function hermesProfileStatusRows() {
44113
44193
  return hermesProfileHealth().filter((profile) => existsSync53(profile.home)).map((profile) => {
44194
+ const label = "hermes/" + profile.name;
44195
+ if (profile.cronUnchecked) {
44196
+ return [
44197
+ label,
44198
+ "UNHEALTHY — legacy shell hooks: Hermes cron jobs are not checked. Run `failproofai update`"
44199
+ ];
44200
+ }
44114
44201
  const problems = [];
44115
44202
  if (!profile.pluginInstalled)
44116
44203
  problems.push("plugin files missing or incomplete");
@@ -44118,13 +44205,15 @@ function hermesProfileStatusRows() {
44118
44205
  problems.push("plugin not enabled");
44119
44206
  if (profile.legacyShellHookPresent)
44120
44207
  problems.push("legacy shell hook also present");
44208
+ if (problems.length > 0)
44209
+ return [label, "UNHEALTHY — " + problems.join("; ")];
44121
44210
  return [
44122
- "hermes/" + profile.name,
44123
- problems.length === 0 ? "native plugin enabled" : "UNHEALTHY — " + problems.join("; ")
44211
+ label,
44212
+ profile.pluginMode === "copy" ? "native plugin enabled (copied — `failproofai update` links it so upgrades apply)" : "native plugin enabled"
44124
44213
  ];
44125
44214
  });
44126
44215
  }
44127
- var HERMES_PLUGIN_ID2 = "failproofai", HERMES_PLUGIN_MARKER2 = ".failproofai-managed", HERMES_PLUGIN_FILES2, ANTIGRAVITY_TOOL_EVENTS2;
44216
+ var HERMES_PLUGIN_ID2 = "failproofai", HERMES_PLUGIN_MARKER2 = ".failproofai-managed", HERMES_PLUGIN_FILES2, HERMES_PLUGIN_SOURCE_DIR2 = "hermes-plugin", ANTIGRAVITY_TOOL_EVENTS2;
44128
44217
  var init_integrations2 = __esm(() => {
44129
44218
  init_dist();
44130
44219
  init_hermes_profiles();
@@ -44136,9 +44225,9 @@ var init_integrations2 = __esm(() => {
44136
44225
 
44137
44226
  // scripts/launch.ts
44138
44227
  import { spawn as spawn3 } from "child_process";
44139
- import { realpathSync as realpathSync6, existsSync as existsSync54 } from "node:fs";
44228
+ import { realpathSync as realpathSync8, existsSync as existsSync54 } from "node:fs";
44140
44229
  import { resolve as resolve40, dirname as dirname30 } from "node:path";
44141
- import { fileURLToPath as fileURLToPath3 } from "node:url";
44230
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
44142
44231
  import { createInterface as createInterface2 } from "node:readline";
44143
44232
  function launch2(mode) {
44144
44233
  const { loggingLevel, disableTelemetry, allowedDevOrigins, host, remainingArgs } = parseScriptArgs(process.argv.slice(2));
@@ -44161,7 +44250,7 @@ function launch2(mode) {
44161
44250
  process.env.PORT = port;
44162
44251
  process.env.HOSTNAME = bindHost;
44163
44252
  cmd = "node";
44164
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve40(dirname30(realpathSync6(fileURLToPath3(import.meta.url))), "..");
44253
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve40(dirname30(realpathSync8(fileURLToPath4(import.meta.url))), "..");
44165
44254
  const serverJsPath = resolve40(packageRoot, ".next/standalone/server.js");
44166
44255
  if (!existsSync54(serverJsPath)) {
44167
44256
  let shadowMessage = null;
@@ -44259,18 +44348,18 @@ var init_cli_error2 = __esm(() => {
44259
44348
  });
44260
44349
 
44261
44350
  // bin/failproofai.mjs
44262
- import { realpathSync as realpathSync7 } from "fs";
44351
+ import { realpathSync as realpathSync9 } from "fs";
44263
44352
  import { dirname as dirname31, resolve as resolve41 } from "path";
44264
- import { fileURLToPath as fileURLToPath4 } from "url";
44353
+ import { fileURLToPath as fileURLToPath5 } from "url";
44265
44354
  // package.json
44266
- var version = "1.0.8-beta.0";
44355
+ var version = "1.0.9-beta.0";
44267
44356
 
44268
44357
  // bin/failproofai.mjs
44269
44358
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
44270
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve41(dirname31(realpathSync7(fileURLToPath4(import.meta.url))), "..");
44359
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve41(dirname31(realpathSync9(fileURLToPath5(import.meta.url))), "..");
44271
44360
  }
44272
44361
  if (!process.env.FAILPROOFAI_DIST_PATH) {
44273
- process.env.FAILPROOFAI_DIST_PATH = resolve41(dirname31(realpathSync7(fileURLToPath4(import.meta.url))), "..", "dist");
44362
+ process.env.FAILPROOFAI_DIST_PATH = resolve41(dirname31(realpathSync9(fileURLToPath5(import.meta.url))), "..", "dist");
44274
44363
  }
44275
44364
  var args = process.argv.slice(2);
44276
44365
  if (args[0] === "p" || args[0] === "policy")
@@ -44844,7 +44933,7 @@ Run \`failproofai backfill --help\` for usage.`);
44844
44933
  "",
44845
44934
  "FailproofAI Cloud needs no setup here: `failproofai config --token",
44846
44935
  '<key>` with a key that carries jev:evaluate (the "machine" preset)',
44847
- "turns it on in shadow mode when there is no jev.json yet \u2014 except",
44936
+ "turns it on in observe mode when there is no jev.json yet \u2014 except",
44848
44937
  "with --no-transcripts, which only stores the key; then",
44849
44938
  "`failproofai jev setup --provider failproofai` switches it on. Its",
44850
44939
  "key stays in credentials.json; no --url ever selects it."
@@ -44861,7 +44950,7 @@ Run \`failproofai backfill --help\` for usage.`);
44861
44950
  ["--account-id <id>", "Cloudflare account id (32 hex characters)."],
44862
44951
  ["--base-url <url>", "Override the API base; `default` clears it."],
44863
44952
  ["--model <id>", "Override the model id; `default` clears it."],
44864
- ["--mode <m>", "enforce (default), shadow (log Jev, enforce regex), or off."],
44953
+ ["--mode <m>", "enforce (default), observe (log Jev, enforce regex), or off."],
44865
44954
  ["--timeout-ms <n>", "Per-call budget before falling back. Default 3000."]
44866
44955
  ]
44867
44956
  },
@@ -44872,7 +44961,7 @@ Run \`failproofai backfill --help\` for usage.`);
44872
44961
  " else can read \u2014 or one in a directory anyone else can WRITE, who",
44873
44962
  " could replace it \u2014 is refused; a repository can never set it.",
44874
44963
  "\u2022 Re-running setup for the same provider keeps the key, so",
44875
- " `failproofai jev setup --mode shadow` just switches the mode.",
44964
+ " `failproofai jev setup --mode observe` just switches the mode.",
44876
44965
  "\u2022 The daemon does not see your shell's environment: keep the key in",
44877
44966
  " the file on a machine set up with `failproofai config`.",
44878
44967
  "\u2022 --token is the fast path, not the safe one: your shell history keeps",
@@ -44886,7 +44975,7 @@ Run \`failproofai backfill --help\` for usage.`);
44886
44975
  "failproofai jev --url https://openrouter.ai/api/v1 --token <token>",
44887
44976
  "failproofai jev setup --provider typesafe --key-stdin < ~/typesafe.key",
44888
44977
  "failproofai jev setup --provider cloudflare --account-id <id> --key-stdin",
44889
- "failproofai config --token <key> (FailproofAI Cloud, shadow mode)",
44978
+ "failproofai config --token <key> (FailproofAI Cloud, observe mode)",
44890
44979
  "failproofai jev setup --mode enforce (switch the configured route's mode)",
44891
44980
  "failproofai jev setup --mode off (keep the config, stop asking Jev)",
44892
44981
  "failproofai jev test",
@@ -45031,7 +45120,10 @@ Run \`failproofai migrate --help\` for usage.`);
45031
45120
  "is the loud version of that problem rather than the silent one.",
45032
45121
  "",
45033
45122
  "This does the rest of the upgrade: runs any pending layout migrations,",
45034
- "puts the matching daemon binary in place, and restarts the service."
45123
+ "puts the matching daemon binary in place, and restarts the service.",
45124
+ "Hermes profiles already using failproofai are moved to the linked",
45125
+ "native plugin (legacy shell hooks never checked Hermes cron jobs).",
45126
+ "Exits non-zero when any half could not be brought current."
45035
45127
  ]
45036
45128
  },
45037
45129
  {
@@ -45097,17 +45189,36 @@ Run \`failproofai update --help\` for usage.`);
45097
45189
  daemonOk = result.ok;
45098
45190
  }
45099
45191
  }
45192
+ let hermesOk = true;
45193
+ let hermesMigrated = 0;
45194
+ try {
45195
+ await Promise.resolve().then(() => init_hermes_update());
45196
+ const svc = await Promise.resolve().then(() => (init_daemon_service2(), {}));
45197
+ const hermesResult = await runHermesUpdateMigration({
45198
+ daemonSupportsPolicyEvaluation: () => probeDaemonPolicyEvaluation2()
45199
+ });
45200
+ if (hermesResult.lines.length > 0)
45201
+ report.push("", ...hermesResult.lines);
45202
+ hermesOk = hermesResult.ok;
45203
+ hermesMigrated = hermesResult.profiles.filter((p) => p.status === "migrated").length;
45204
+ } catch (err) {
45205
+ report.push("", `Hermes migration failed: ${err instanceof Error ? err.message : String(err)}`);
45206
+ hermesOk = false;
45207
+ }
45208
+ const updateOk = daemonOk && !migrationFailed && hermesOk;
45100
45209
  await printReport("update", report, {
45101
- ok: daemonOk && !migrationFailed,
45210
+ ok: updateOk,
45102
45211
  meta: `v${version}`
45103
45212
  });
45104
45213
  await track("cli_update", {
45105
- ok: daemonOk && !migrationFailed,
45214
+ ok: updateOk,
45106
45215
  migrations: migrationsRan,
45107
- migration_failed: migrationFailed
45216
+ migration_failed: migrationFailed,
45217
+ hermes_ok: hermesOk,
45218
+ hermes_migrated: hermesMigrated
45108
45219
  });
45109
45220
  lastSubcommand = null;
45110
- await exitAfterFlush(daemonOk && !migrationFailed ? 0 : 1);
45221
+ await exitAfterFlush(updateOk ? 0 : 1);
45111
45222
  return;
45112
45223
  }
45113
45224
  if (args[0] === "uninstall") {