failproofai 0.0.14-beta.1 → 0.0.14-beta.2

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 (164) 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 +2 -2
  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/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/index.html +1 -1
  38. package/.next/standalone/.next/server/app/index.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  40. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  42. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  43. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  83. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wp9w6i._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0brjczq._.js → [root-of-the-server]__0twi4fj._.js} +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  104. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_12rjfea.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  110. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  111. package/.next/standalone/.next/server/pages/404.html +1 -1
  112. package/.next/standalone/.next/server/pages/500.html +1 -1
  113. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  114. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  115. package/.next/standalone/.next/static/chunks/{0o9wpnze1ey8y.js → 0h-6ymo9v86b0.js} +2 -2
  116. package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
  117. package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
  118. package/.next/standalone/.next/static/chunks/{2vav_x0_336ds.js → 1bg_g3g359c_d.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{1359o5785rmve.js → 1gg-8e9q_m__9.js} +2 -2
  120. package/.next/standalone/.next/static/chunks/{0emg30_6svycu.js → 29zkom4s9de2d.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{0d5lypu1zs21r.js → 2llkdafnaejw_.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{20tuctw38jbgw.js → 388hgfqye67xa.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
  124. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
  125. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
  126. package/.next/standalone/app/components/copy-button.tsx +22 -6
  127. package/.next/standalone/{ci/cli-integration → integration-suite}/README.md +25 -7
  128. package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
  129. package/.next/standalone/{ci/cli-integration → integration-suite}/run.sh +4 -4
  130. package/.next/standalone/lib/format-duration.ts +23 -9
  131. package/.next/standalone/package.json +1 -1
  132. package/.next/standalone/server.js +1 -1
  133. package/dist/cli.mjs +342 -106
  134. package/lib/format-duration.ts +23 -9
  135. package/package.json +1 -1
  136. package/scripts/translate-docs/cli.ts +6 -0
  137. package/scripts/translate-docs/mdx-translator.ts +30 -19
  138. package/scripts/translate-docs/readme-translator.ts +31 -21
  139. package/scripts/translate-docs/translator.ts +142 -7
  140. package/scripts/translate-docs/types.ts +6 -0
  141. package/scripts/translate-docs/validate-translation.ts +132 -0
  142. package/scripts/validate-mdx.ts +80 -11
  143. package/src/audit/cli.ts +12 -6
  144. package/src/auth/cli.ts +14 -5
  145. package/src/hooks/builtin-policies.ts +168 -50
  146. package/src/hooks/configure-wizard.ts +238 -22
  147. package/src/hooks/custom-hooks-loader.ts +70 -4
  148. package/src/hooks/handler.ts +4 -1
  149. package/src/hooks/policy-types.ts +8 -0
  150. package/src/hooks/tui.ts +95 -28
  151. package/.next/standalone/.next/static/chunks/2ldhnaweu9ew3.js +0 -1
  152. package/.next/standalone/.next/static/chunks/3cg7x0joi2b98.js +0 -1
  153. package/.next/standalone/.next/static/chunks/3k87p-tijud-4.js +0 -1
  154. package/.next/standalone/.next/static/chunks/3kmaqcp7rldad.js +0 -1
  155. /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
  156. /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
  157. /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
  158. /package/.next/standalone/{ci/cli-integration → integration-suite}/Dockerfile +0 -0
  159. /package/.next/standalone/{ci/cli-integration → integration-suite}/canary-policies.mjs +0 -0
  160. /package/.next/standalone/{ci/cli-integration → integration-suite}/capture-tokens.sh +0 -0
  161. /package/.next/standalone/{ci/cli-integration → integration-suite}/inject-tokens.sh +0 -0
  162. /package/.next/standalone/{ci/cli-integration → integration-suite}/install-clis.sh +0 -0
  163. /package/.next/standalone/{ci/cli-integration → integration-suite}/probe-cli.sh +0 -0
  164. /package/.next/standalone/{ci/cli-integration → integration-suite}/report.js +0 -0
package/dist/cli.mjs CHANGED
@@ -19,7 +19,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // package.json
22
- var version2 = "0.0.14-beta.1";
22
+ var version2 = "0.0.14-beta.2";
23
23
  var init_package = () => {};
24
24
 
25
25
  // src/posthog-key.ts
@@ -727,6 +727,22 @@ function readMergedHooksConfig(cwd) {
727
727
  ...llm !== undefined ? { llm } : {}
728
728
  };
729
729
  }
730
+ function getConfigPath() {
731
+ return resolve(homedir2(), ".failproofai", "policies-config.json");
732
+ }
733
+ function readHooksConfig() {
734
+ const configPath = getConfigPath();
735
+ if (!existsSync2(configPath)) {
736
+ return { enabledPolicies: [] };
737
+ }
738
+ try {
739
+ const raw = readFileSync(configPath, "utf8");
740
+ return JSON.parse(raw);
741
+ } catch (err) {
742
+ hookLogWarn(`failed to parse config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
743
+ return { enabledPolicies: [] };
744
+ }
745
+ }
730
746
  function getConfigPathForScope(scope, cwd) {
731
747
  switch (scope) {
732
748
  case "user":
@@ -1178,28 +1194,73 @@ function blockPushMaster(ctx) {
1178
1194
  }
1179
1195
  return allow();
1180
1196
  }
1181
- function rmTargetIsAllowed(cmd, allowPaths) {
1182
- if (allowPaths.length === 0)
1197
+ function expandHomePrefix(path2) {
1198
+ const m = path2.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
1199
+ return m ? homedir3() + path2.slice(m[0].length) : path2;
1200
+ }
1201
+ function stripTrailingGlob(path2) {
1202
+ return path2.replace(/\/\*$/, "").replace(/\/+$/, "");
1203
+ }
1204
+ function isCatastrophicTarget(token) {
1205
+ const raw = token.replace(/^['"]|['"]$/g, "");
1206
+ if (raw === "")
1207
+ return false;
1208
+ const homePrefix = raw.match(HOME_PREFIX_RE);
1209
+ if (!homePrefix && /^[$`]/.test(raw))
1210
+ return true;
1211
+ const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
1212
+ if (belowRoot === null)
1183
1213
  return false;
1184
- const segments = cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /\brm\b/.test(s));
1185
- if (segments.length === 0)
1214
+ const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
1215
+ if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`)))
1186
1216
  return false;
1187
- for (const seg of segments) {
1188
- const tokens = parseArgvTokens(seg);
1189
- const rmIdx = tokens.findIndex((t) => t === "rm");
1190
- if (rmIdx < 0)
1191
- continue;
1192
- const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
1193
- const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
1194
- if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some((f) => /^--recursive$/i.test(f)))
1217
+ return segments.length <= CATASTROPHIC_DEPTH;
1218
+ }
1219
+ function recursiveDeletionTargets(seg) {
1220
+ const tokens = parseArgvTokens(seg);
1221
+ const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
1222
+ if (findIdx >= 0) {
1223
+ const expr = tokens.slice(findIdx + 1);
1224
+ const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
1225
+ const deletes = expr.includes("-delete") || execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? "");
1226
+ if (deletes) {
1227
+ let start = 0;
1228
+ while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
1229
+ start += expr[start] === "-D" ? 2 : 1;
1230
+ }
1231
+ const rest = expr.slice(start);
1232
+ const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
1233
+ return end < 0 ? rest : rest.slice(0, end);
1234
+ }
1235
+ }
1236
+ const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
1237
+ if (rmIdx >= 0) {
1238
+ const args = tokens.slice(rmIdx + 1);
1239
+ const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
1240
+ const longFlags = args.filter((t) => /^--/.test(t));
1241
+ const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
1242
+ const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
1243
+ if (recursive && force)
1244
+ return args.filter((t) => !t.startsWith("-"));
1245
+ }
1246
+ return null;
1247
+ }
1248
+ function shellSegments(cmd) {
1249
+ return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
1250
+ }
1251
+ function deletionTargetIsAllowed(cmd, allowPaths) {
1252
+ if (allowPaths.length === 0)
1253
+ return false;
1254
+ const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
1255
+ let sawRecursiveDelete = false;
1256
+ for (const seg of shellSegments(cmd)) {
1257
+ const targets = recursiveDeletionTargets(seg);
1258
+ if (targets === null)
1195
1259
  continue;
1196
- const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
1197
- for (const target of pathArgs) {
1198
- const normalized = target.replace(/\/\*$/, "").replace(/\/+$/, "") || "/";
1199
- const covered = allowPaths.some((p) => {
1200
- const np = p.replace(/\/+$/, "") || "/";
1201
- return normalized === np || normalized.startsWith(np + "/");
1202
- });
1260
+ sawRecursiveDelete = true;
1261
+ for (const target of targets) {
1262
+ const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
1263
+ const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
1203
1264
  if (!covered) {
1204
1265
  const segCovered = allowPaths.some((p) => {
1205
1266
  const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1210,35 +1271,22 @@ function rmTargetIsAllowed(cmd, allowPaths) {
1210
1271
  }
1211
1272
  }
1212
1273
  }
1213
- return true;
1274
+ return sawRecursiveDelete;
1214
1275
  }
1215
1276
  function blockRmRf(ctx) {
1216
1277
  if (ctx.toolName !== "Bash")
1217
1278
  return allow();
1218
1279
  const cmd = getCommand(ctx);
1219
- const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
1220
- const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
1221
- return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
1280
+ const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
1281
+ const targets = recursiveDeletionTargets(seg);
1282
+ return targets !== null && targets.some(isCatastrophicTarget);
1222
1283
  });
1223
- if (hasDestructivePath && (/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) || /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd))) {
1284
+ if (hasCatastrophicTarget) {
1224
1285
  const allowPaths = ctx.params?.allowPaths ?? [];
1225
- if (rmTargetIsAllowed(cmd, allowPaths))
1286
+ if (deletionTargetIsAllowed(cmd, allowPaths))
1226
1287
  return allow();
1227
1288
  return deny("Catastrophic deletion blocked");
1228
1289
  }
1229
- if (hasDestructivePath && /\brm\b/.test(cmd)) {
1230
- const tokens = parseArgvTokens(cmd);
1231
- const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
1232
- const longFlags = tokens.filter((t) => /^--/.test(t));
1233
- const hasRecursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
1234
- const hasForce = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
1235
- if (hasRecursive && hasForce) {
1236
- const allowPaths = ctx.params?.allowPaths ?? [];
1237
- if (rmTargetIsAllowed(cmd, allowPaths))
1238
- return allow();
1239
- return deny("Catastrophic deletion blocked");
1240
- }
1241
- }
1242
1290
  if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
1243
1291
  return deny("Catastrophic deletion blocked");
1244
1292
  }
@@ -1250,12 +1298,31 @@ function blockRmRf(ctx) {
1250
1298
  function blockForcePush(ctx) {
1251
1299
  if (ctx.toolName !== "Bash")
1252
1300
  return allow();
1253
- const args = extractGitPushArgs(getCommand(ctx));
1254
- if (args.some((a) => FORCE_PUSH_RE.test(a))) {
1255
- return deny("Force-pushing is blocked");
1301
+ for (const segment of extractGitPushArgs(getCommand(ctx))) {
1302
+ let sawEndOfOptions = false;
1303
+ for (const token of segment.split(/\s+/)) {
1304
+ if (token === "--") {
1305
+ sawEndOfOptions = true;
1306
+ continue;
1307
+ }
1308
+ if (sawEndOfOptions)
1309
+ continue;
1310
+ if (isForcePushFlag(token)) {
1311
+ return deny("Force-pushing is blocked");
1312
+ }
1313
+ }
1256
1314
  }
1257
1315
  return allow();
1258
1316
  }
1317
+ function isForcePushFlag(token) {
1318
+ if (token === "--force")
1319
+ return true;
1320
+ if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix)))
1321
+ return false;
1322
+ if (token.startsWith("--force"))
1323
+ return true;
1324
+ return SHORT_FLAG_BUNDLE_RE.test(token);
1325
+ }
1259
1326
  function blockSecretsWrite(ctx) {
1260
1327
  if (ctx.toolName !== "Write")
1261
1328
  return allow();
@@ -1836,7 +1903,7 @@ function registerBuiltinPolicies(enabledNames) {
1836
1903
  }
1837
1904
  }
1838
1905
  }
1839
- var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, FORCE_PUSH_RE, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1906
+ var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1840
1907
  var init_builtin_policies = __esm(() => {
1841
1908
  init_hook_logger();
1842
1909
  isClaudeInternalPath = isAgentInternalPath;
@@ -1878,7 +1945,8 @@ var init_builtin_policies = __esm(() => {
1878
1945
  RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
1879
1946
  CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
1880
1947
  PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
1881
- FORCE_PUSH_RE = /(?:--force|-f\b)/;
1948
+ SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
1949
+ SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
1882
1950
  SECRET_FILE_RE = /\.(?:pem|key)$/;
1883
1951
  SECRET_FILE_ID_RSA_RE = /id_rsa/;
1884
1952
  SECRET_FILE_CREDENTIALS_RE = /credentials/;
@@ -1919,6 +1987,13 @@ var init_builtin_policies = __esm(() => {
1919
1987
  HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
1920
1988
  GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
1921
1989
  gitBranchCache = new Map;
1990
+ HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
1991
+ RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
1992
+ FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
1993
+ FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
1994
+ FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
1995
+ FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
1996
+ SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
1922
1997
  READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
1923
1998
  SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
1924
1999
  BUILTIN_POLICIES = [
@@ -3435,6 +3510,15 @@ function discoverPolicyFiles(dir) {
3435
3510
  return [];
3436
3511
  }
3437
3512
  }
3513
+ function findSkippedPolicyFiles(dir) {
3514
+ if (!existsSync3(dir))
3515
+ return [];
3516
+ try {
3517
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && LOADABLE_EXT_RE.test(e.name) && !CONVENTION_FILE_RE.test(e.name) && !e.name.endsWith(".d.ts")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
3518
+ } catch {
3519
+ return [];
3520
+ }
3521
+ }
3438
3522
  async function loadSingleFile(absPath, opts) {
3439
3523
  const g = globalThis;
3440
3524
  g[LOADING_KEY] = true;
@@ -3477,10 +3561,18 @@ async function loadCustomHooks(customPoliciesPath, opts) {
3477
3561
  await loadSingleFile(absPath, opts);
3478
3562
  return getCustomHooks();
3479
3563
  }
3564
+ function warnSkippedPolicyFiles(dir, scope) {
3565
+ const skipped = findSkippedPolicyFiles(dir);
3566
+ if (skipped.length === 0)
3567
+ return;
3568
+ const suggest = (n) => n.replace(/\.(js|mjs|ts)$/, "-policies.$1");
3569
+ hookLogWarn(`${scope} policies: ${skipped.length} file(s) in ${dir} are NOT loaded — ` + `the convention only loads names ending in "policies.js|mjs|ts". ` + skipped.map((n) => `${n} → rename to ${suggest(n)}`).join("; "));
3570
+ }
3480
3571
  async function loadAllCustomHooks(customPoliciesPath, opts) {
3481
3572
  clearCustomHooks();
3482
3573
  const conventionSources = [];
3483
3574
  const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
3575
+ const conventionEnabled = opts?.customPoliciesEnabled !== false;
3484
3576
  if (customPoliciesPath) {
3485
3577
  const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(projectRoot, customPoliciesPath);
3486
3578
  if (existsSync3(absPath)) {
@@ -3491,7 +3583,9 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
3491
3583
  }
3492
3584
  const hooksBeforeConvention = getCustomHooks().length;
3493
3585
  const projectDir = resolve4(projectRoot, ".failproofai", "policies");
3494
- const projectFiles = discoverPolicyFiles(projectDir);
3586
+ if (conventionEnabled)
3587
+ warnSkippedPolicyFiles(projectDir, "project");
3588
+ const projectFiles = conventionEnabled ? discoverPolicyFiles(projectDir) : [];
3495
3589
  for (const file of projectFiles) {
3496
3590
  const hooksBefore = getCustomHooks().length;
3497
3591
  await loadSingleFile(file, { conventionScope: "project" });
@@ -3505,7 +3599,9 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
3505
3599
  }
3506
3600
  }
3507
3601
  const userDir = resolve4(homedir4(), ".failproofai", "policies");
3508
- const userFiles = discoverPolicyFiles(userDir);
3602
+ if (conventionEnabled)
3603
+ warnSkippedPolicyFiles(userDir, "user");
3604
+ const userFiles = conventionEnabled ? discoverPolicyFiles(userDir) : [];
3509
3605
  for (const file of userFiles) {
3510
3606
  const hooksBefore = getCustomHooks().length;
3511
3607
  await loadSingleFile(file, { conventionScope: "user" });
@@ -3520,7 +3616,9 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
3520
3616
  }
3521
3617
  const allHooks = getCustomHooks();
3522
3618
  const conventionCount = allHooks.length - hooksBeforeConvention;
3523
- if (projectFiles.length > 0 || userFiles.length > 0) {
3619
+ if (!conventionEnabled) {
3620
+ hookLogInfo("convention policies: DISABLED via customPoliciesEnabled:false — " + "no .failproofai/policies/ files loaded");
3621
+ } else if (projectFiles.length > 0 || userFiles.length > 0) {
3524
3622
  hookLogInfo(`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`);
3525
3623
  }
3526
3624
  const hookNameToScope = new Map;
@@ -3540,7 +3638,7 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
3540
3638
  }
3541
3639
  return { hooks: allHooks, conventionSources };
3542
3640
  }
3543
- var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE;
3641
+ var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE, LOADABLE_EXT_RE;
3544
3642
  var init_custom_hooks_loader = __esm(() => {
3545
3643
  init_hook_logger();
3546
3644
  init_custom_hooks_registry();
@@ -3549,6 +3647,7 @@ var init_custom_hooks_loader = __esm(() => {
3549
3647
  init_hook_telemetry2();
3550
3648
  init_telemetry_id2();
3551
3649
  CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
3650
+ LOADABLE_EXT_RE = /\.(js|mjs|ts)$/;
3552
3651
  });
3553
3652
 
3554
3653
  // src/hooks/hook-activity-store.ts
@@ -4761,17 +4860,20 @@ var init_sqlite_reader = () => {};
4761
4860
  function formatDuration(ms) {
4762
4861
  if (ms < 1000)
4763
4862
  return `${ms}ms`;
4764
- const seconds = ms / 1000;
4765
- if (seconds < 60)
4766
- return `${seconds.toFixed(1)}s`;
4767
- const totalMinutes = Math.floor(seconds / 60);
4768
- if (totalMinutes >= 60) {
4769
- const hours = Math.floor(totalMinutes / 60);
4770
- const remainingMinutes = totalMinutes % 60;
4771
- return `${hours}h ${remainingMinutes}m`;
4863
+ const deciseconds = Math.round(ms / 100);
4864
+ if (deciseconds < 600) {
4865
+ return `${(deciseconds / 10).toFixed(1)}s`;
4866
+ }
4867
+ const totalSeconds = Math.round(ms / 1000);
4868
+ if (totalSeconds < 3600) {
4869
+ const minutes2 = Math.floor(totalSeconds / 60);
4870
+ const seconds = totalSeconds % 60;
4871
+ return `${minutes2}m ${seconds}s`;
4772
4872
  }
4773
- const remainingSeconds = (seconds % 60).toFixed(0);
4774
- return `${totalMinutes}m ${remainingSeconds}s`;
4873
+ const totalMinutes = Math.round(ms / 60000);
4874
+ const hours = Math.floor(totalMinutes / 60);
4875
+ const minutes = totalMinutes % 60;
4876
+ return `${hours}h ${minutes}m`;
4775
4877
  }
4776
4878
 
4777
4879
  // lib/hermes-sessions.ts
@@ -8191,7 +8293,10 @@ async function handleHookEvent(eventType, cli = "claude") {
8191
8293
  const config = readMergedHooksConfig(session.cwd);
8192
8294
  clearPolicies();
8193
8295
  registerBuiltinPolicies(config.enabledPolicies);
8194
- const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
8296
+ const loadResult = await loadAllCustomHooks(config.customPoliciesPath, {
8297
+ sessionCwd: session.cwd,
8298
+ customPoliciesEnabled: config.customPoliciesEnabled
8299
+ });
8195
8300
  const customHooksList = loadResult.hooks;
8196
8301
  const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
8197
8302
  for (const hook of customHooksList) {
@@ -17162,6 +17267,11 @@ import * as readline from "node:readline";
17162
17267
  function colorsEnabled(out) {
17163
17268
  return !!out.isTTY && !process.env.NO_COLOR;
17164
17269
  }
17270
+ function brandAnsi(role) {
17271
+ const h = HUES[role];
17272
+ const code = truecolorEnabled() ? `38;2;${h.rgb.join(";")}` : h.basic;
17273
+ return `${ESC}[${code}m`;
17274
+ }
17165
17275
  function truecolorEnabled() {
17166
17276
  return /truecolor|24bit/i.test(process.env.COLORTERM || "");
17167
17277
  }
@@ -17458,7 +17568,7 @@ function multiSelect(opts) {
17458
17568
  const choices = opts.choices;
17459
17569
  const minSelected = opts.minSelected ?? 0;
17460
17570
  const noun = opts.summaryNoun ?? "selected";
17461
- const checked = choices.map((ch) => !!ch.checked);
17571
+ const checked = choices.map((ch) => ch.locked ? ch.checked ?? true : !!ch.checked);
17462
17572
  if (!stdin.isTTY || !stdout.isTTY) {
17463
17573
  return Promise.resolve(choices.filter((_, i) => checked[i]).map((ch) => ch.value));
17464
17574
  }
@@ -17474,7 +17584,7 @@ function multiSelect(opts) {
17474
17584
  renderRow: (index, active, budget) => {
17475
17585
  const choice = choices[index];
17476
17586
  const caret = active ? c.pink(CARET) : " ";
17477
- const box = checked[index] ? c.pink(CHECK_ON) : c.dim(CHECK_OFF);
17587
+ const box = choice.locked ? checked[index] ? c.guide(CHECK_ON) : c.dim(CHECK_OFF) : checked[index] ? c.pink(CHECK_ON) : c.dim(CHECK_OFF);
17478
17588
  const rawLabel = choice.label.padEnd(nameCol);
17479
17589
  const label = active ? c.pinkBold(rawLabel) : checked[index] ? rawLabel : c.dim(rawLabel);
17480
17590
  const hint = choice.hint ? ` ${c.dim(ellipsize(choice.hint, budget))}` : "";
@@ -17484,19 +17594,23 @@ function multiSelect(opts) {
17484
17594
  footer: opts.hint ?? "↑/↓ move · space select · ctrl+a all · enter confirm",
17485
17595
  onKey: (key, cursor2) => {
17486
17596
  if (key.name === "space") {
17597
+ if (choices[cursor2]?.locked)
17598
+ return "redraw";
17487
17599
  checked[cursor2] = !checked[cursor2];
17488
17600
  warn = false;
17489
17601
  return "redraw";
17490
17602
  }
17491
17603
  if (key.ctrl && key.name === "a") {
17492
- const allOn = checked.every(Boolean);
17493
- for (let i = 0;i < checked.length; i++)
17494
- checked[i] = !allOn;
17604
+ const allOn = choices.every((ch, i) => checked[i] || ch.locked);
17605
+ for (let i = 0;i < checked.length; i++) {
17606
+ checked[i] = choices[i]?.locked ? true : !allOn;
17607
+ }
17495
17608
  return "redraw";
17496
17609
  }
17497
17610
  if (key.name === "return") {
17498
17611
  const selected = choices.filter((_, i) => checked[i]).map((ch) => ch.value);
17499
- if (selected.length < minSelected) {
17612
+ const chosen = choices.filter((ch, i) => checked[i] && !ch.locked).length;
17613
+ if (chosen < minSelected) {
17500
17614
  warn = true;
17501
17615
  return "redraw";
17502
17616
  }
@@ -17504,10 +17618,10 @@ function multiSelect(opts) {
17504
17618
  }
17505
17619
  return;
17506
17620
  },
17507
- summaryFor: (values) => values === null ? "cancelled" : summarize(choices.filter((ch) => values.includes(ch.value)).map((ch) => ch.label), noun)
17621
+ summaryFor: (values) => values === null ? "cancelled" : summarize(choices.filter((ch) => values.includes(ch.value) && !ch.summaryExclude).map((ch) => ch.label), noun)
17508
17622
  });
17509
17623
  }
17510
- var ESC = "\x1B", BAR = "│", BAR_END = "└", STEP_ACTIVE = "◆", STEP_DONE = "◇", RADIO_ON = "●", RADIO_OFF = "○", CHECK_ON = "◼", CHECK_OFF = "◻", CARET = "❯", FLOWER = "❋", HUES, LOGO_MIN_COLS = 22, TAGLINE = "end-to-end failure layer for ai agents", LOGO_GRID, LOGO_TEAL, LOGO_PINK, WINDOW = 8;
17624
+ var ESC = "\x1B", BAR = "│", BAR_END = "└", STEP_ACTIVE = "◆", STEP_DONE = "◇", RADIO_ON = "●", RADIO_OFF = "○", CHECK_ON = "◼", CHECK_OFF = "◻", CARET = "❯", FLOWER = "❋", HUES, ANSI_RESET, ANSI_BOLD, ANSI_DIM, LOGO_MIN_COLS = 22, TAGLINE = "end-to-end failure layer for ai agents", LOGO_GRID, LOGO_TEAL, LOGO_PINK, WINDOW = 8;
17511
17625
  var init_tui = __esm(() => {
17512
17626
  HUES = {
17513
17627
  guide: { rgb: [102, 209, 181], basic: "36" },
@@ -17516,29 +17630,30 @@ var init_tui = __esm(() => {
17516
17630
  warn: { rgb: [227, 179, 65], basic: "33" },
17517
17631
  dim: { rgb: [107, 118, 132], basic: "2" }
17518
17632
  };
17633
+ ANSI_RESET = `${ESC}[0m`;
17634
+ ANSI_BOLD = `${ESC}[1m`;
17635
+ ANSI_DIM = `${ESC}[2m`;
17519
17636
  LOGO_GRID = [
17520
- "...tt...........",
17521
- "..tttt..........",
17522
- ".tttttt.........",
17523
- ".tttttt.....ppp.",
17524
- "..tttt......ppp.",
17525
- "...tt.......ppp.",
17526
- "............ppp.",
17527
- "............ppp.",
17528
- "..pppp......ppp.",
17529
- "..pppp......ppp.",
17530
- "..pppp......ppp.",
17531
- ".pppppp.....ppp.",
17532
- ".pppppp.....ppp.",
17533
- ".pppppp.....ppp.",
17534
- ".pppppp.....ppp.",
17535
- "..pppp......ppp.",
17536
- "..pppp......ppp.",
17537
- "..pppp......ppp.",
17538
- "..ppppppppppppp.",
17539
- "..ppppppppppppp.",
17540
- "..ppppppppppppp.",
17541
- "..ppppppppppppp."
17637
+ "...tt........",
17638
+ "..tttt.......",
17639
+ ".tttttt..pppp",
17640
+ ".tttttt..pppp",
17641
+ "..tttt...pppp",
17642
+ "...tt....pppp",
17643
+ ".........pppp",
17644
+ ".........pppp",
17645
+ "..pppp...pppp",
17646
+ "..pppp...pppp",
17647
+ "..pppp...pppp",
17648
+ ".pppppp..pppp",
17649
+ ".pppppp..pppp",
17650
+ ".pppppp..pppp",
17651
+ ".pppppp..pppp",
17652
+ "..pppp...pppp",
17653
+ "..ppppppppppp",
17654
+ "..ppppppppppp",
17655
+ "..ppppppppppp",
17656
+ "..ppppppppppp"
17542
17657
  ];
17543
17658
  LOGO_TEAL = HUES.guide.rgb;
17544
17659
  LOGO_PINK = HUES.logoPink.rgb;
@@ -19259,11 +19374,12 @@ async function runAuthCli(args) {
19259
19374
  return runLogout();
19260
19375
  return runWhoami();
19261
19376
  }
19262
- var HELP, LEGACY_FLAG_TO_SUB, SUBCOMMANDS, DIM = "\x1B[2m", RESET = "\x1B[0m", PINK = "\x1B[38;5;204m", GREEN = "\x1B[38;5;120m", RED = "\x1B[38;5;197m";
19377
+ var HELP, LEGACY_FLAG_TO_SUB, SUBCOMMANDS, COLOR, DIM, RESET, PINK, GREEN, RED;
19263
19378
  var init_cli = __esm(() => {
19264
19379
  init_api_server_client();
19265
19380
  init_auth_store();
19266
19381
  init_cli_error();
19382
+ init_tui();
19267
19383
  init_hook_telemetry2();
19268
19384
  init_telemetry_id2();
19269
19385
  HELP = `
@@ -19292,6 +19408,12 @@ EXAMPLES
19292
19408
  "--whoami": "whoami"
19293
19409
  };
19294
19410
  SUBCOMMANDS = new Set(["login", "logout", "whoami", "help"]);
19411
+ COLOR = colorsEnabled(process.stdout);
19412
+ DIM = COLOR ? ANSI_DIM : "";
19413
+ RESET = COLOR ? ANSI_RESET : "";
19414
+ PINK = COLOR ? brandAnsi("pink") : "";
19415
+ GREEN = COLOR ? brandAnsi("guide") : "";
19416
+ RED = COLOR ? brandAnsi("warn") : "";
19295
19417
  });
19296
19418
 
19297
19419
  // lib/claude-sessions.ts
@@ -21665,7 +21787,7 @@ async function runAuditCli(args) {
21665
21787
  const { launch: launch2 } = await Promise.resolve().then(() => (init_launch(), exports_launch));
21666
21788
  launch2("start");
21667
21789
  }
21668
- var DASHBOARD_PORT = 8020, AUDIT_STAGES, HELP2, RESET2 = "\x1B[0m", DIM2 = "\x1B[2m", BOLD = "\x1B[1m", PINK2 = "\x1B[38;5;204m", GREEN2 = "\x1B[38;5;120m", CYAN = "\x1B[38;5;81m", SPINNER;
21790
+ var DASHBOARD_PORT = 8020, AUDIT_STAGES, HELP2, RESET2, DIM2, BOLD, PINK2, GREEN2, CYAN, SPINNER;
21669
21791
  var init_cli2 = __esm(() => {
21670
21792
  init_audit();
21671
21793
  init_dashboard_cache();
@@ -21673,6 +21795,7 @@ var init_cli2 = __esm(() => {
21673
21795
  init_telemetry_id2();
21674
21796
  init_telemetry_sanitize();
21675
21797
  init_open_browser();
21798
+ init_tui();
21676
21799
  AUDIT_STAGES = [
21677
21800
  { label: "discovering transcripts", detail: "walking ~/.claude, ~/.codex, ~/.cursor, …" },
21678
21801
  { label: "parsing session logs", detail: "reading JSONL + sqlite session stores" },
@@ -21696,6 +21819,12 @@ WHAT IT DOES
21696
21819
  Runs fully offline — no account or network required. Press Ctrl+C to stop the
21697
21820
  dashboard server when you're done.
21698
21821
  `.trimStart();
21822
+ RESET2 = ANSI_RESET;
21823
+ DIM2 = ANSI_DIM;
21824
+ BOLD = ANSI_BOLD;
21825
+ PINK2 = brandAnsi("pink");
21826
+ GREEN2 = brandAnsi("guide");
21827
+ CYAN = brandAnsi("guide");
21699
21828
  SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
21700
21829
  });
21701
21830
 
@@ -22225,7 +22354,7 @@ async function runAuditCli2(args) {
22225
22354
  const { launch: launch2 } = await Promise.resolve().then(() => (init_launch(), exports_launch));
22226
22355
  launch2("start");
22227
22356
  }
22228
- var DASHBOARD_PORT2 = 8020, AUDIT_STAGES2, HELP3, RESET3 = "\x1B[0m", DIM3 = "\x1B[2m", BOLD2 = "\x1B[1m", PINK3 = "\x1B[38;5;204m", GREEN3 = "\x1B[38;5;120m", CYAN2 = "\x1B[38;5;81m", SPINNER2;
22357
+ var DASHBOARD_PORT2 = 8020, AUDIT_STAGES2, HELP3, RESET3, DIM3, BOLD2, PINK3, GREEN3, CYAN2, SPINNER2;
22229
22358
  var init_cli3 = __esm(() => {
22230
22359
  init_audit();
22231
22360
  init_dashboard_cache();
@@ -22233,6 +22362,7 @@ var init_cli3 = __esm(() => {
22233
22362
  init_telemetry_id2();
22234
22363
  init_telemetry_sanitize();
22235
22364
  init_open_browser();
22365
+ init_tui();
22236
22366
  AUDIT_STAGES2 = [
22237
22367
  { label: "discovering transcripts", detail: "walking ~/.claude, ~/.codex, ~/.cursor, …" },
22238
22368
  { label: "parsing session logs", detail: "reading JSONL + sqlite session stores" },
@@ -22256,23 +22386,32 @@ WHAT IT DOES
22256
22386
  Runs fully offline — no account or network required. Press Ctrl+C to stop the
22257
22387
  dashboard server when you're done.
22258
22388
  `.trimStart();
22389
+ RESET3 = ANSI_RESET;
22390
+ DIM3 = ANSI_DIM;
22391
+ BOLD2 = ANSI_BOLD;
22392
+ PINK3 = brandAnsi("pink");
22393
+ GREEN3 = brandAnsi("guide");
22394
+ CYAN2 = brandAnsi("guide");
22259
22395
  SPINNER2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22260
22396
  });
22261
22397
 
22262
22398
  // src/hooks/configure-wizard.ts
22263
22399
  var exports_configure_wizard = {};
22264
22400
  __export(exports_configure_wizard, {
22401
+ setCustomPoliciesEnabled: () => setCustomPoliciesEnabled,
22265
22402
  runConfigureWizard: () => runConfigureWizard,
22266
22403
  reviewLines: () => reviewLines,
22267
22404
  resolvePresetSelection: () => resolvePresetSelection,
22268
22405
  maybeFirstRunConfigure: () => maybeFirstRunConfigure,
22269
22406
  markLauncherSeen: () => markLauncherSeen,
22270
22407
  hasSeenLauncher: () => hasSeenLauncher,
22408
+ describeCustomPolicies: () => describeCustomPolicies,
22409
+ clisSupportingScope: () => clisSupportingScope,
22271
22410
  buildScopeChoices: () => buildScopeChoices,
22272
22411
  buildPresetChoices: () => buildPresetChoices,
22273
22412
  buildAgentChoices: () => buildAgentChoices
22274
22413
  });
22275
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "node:fs";
22414
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync16 } from "node:fs";
22276
22415
  import { homedir as homedir30 } from "node:os";
22277
22416
  import { resolve as resolve16, sep as sep5 } from "node:path";
22278
22417
  async function emit(event, props) {
@@ -22302,6 +22441,9 @@ function buildScopeChoices(cwd) {
22302
22441
  }
22303
22442
  ];
22304
22443
  }
22444
+ function clisSupportingScope(scope) {
22445
+ return INTEGRATION_TYPES.filter((id) => getIntegration(id).scopes.includes(scope));
22446
+ }
22305
22447
  function buildAgentChoices(scope, cwd) {
22306
22448
  const detected = new Set(detectInstalledClis());
22307
22449
  const ordered = [
@@ -22311,6 +22453,17 @@ function buildAgentChoices(scope, cwd) {
22311
22453
  return ordered.map((id) => {
22312
22454
  const integration = getIntegration(id);
22313
22455
  const isDetected = detected.has(id);
22456
+ const supported = integration.scopes.includes(scope);
22457
+ if (!supported) {
22458
+ return {
22459
+ label: integration.displayName,
22460
+ value: id,
22461
+ checked: false,
22462
+ locked: true,
22463
+ section: "Global only · not configurable per-project",
22464
+ hint: `supports ${integration.scopes.join(", ")} scope — rerun with "Everywhere I code"`
22465
+ };
22466
+ }
22314
22467
  let installedHere = false;
22315
22468
  try {
22316
22469
  installedHere = integration.hooksInstalledInSettings(scope, cwd);
@@ -22326,28 +22479,102 @@ function buildAgentChoices(scope, cwd) {
22326
22479
  };
22327
22480
  });
22328
22481
  }
22329
- function buildPresetChoices() {
22330
- const choices = POLICY_PRESETS.map((p) => ({ label: p.label, value: p.id, hint: p.description }));
22482
+ function buildPresetChoices(cwd = process.cwd(), enabled = true) {
22483
+ const choices = POLICY_PRESETS.map((p) => ({
22484
+ label: p.label,
22485
+ value: p.id,
22486
+ hint: p.description
22487
+ }));
22331
22488
  choices.push({
22332
22489
  label: "Everything",
22333
22490
  value: EVERYTHING,
22334
22491
  hint: `all ${resolveEverything().length} policies`
22335
22492
  });
22493
+ const custom = describeCustomPolicies(cwd);
22494
+ const skipped = custom.warnings.length;
22495
+ const plural = (n) => `${n} file${n === 1 ? "" : "s"}`;
22496
+ const skippedNote = skipped > 0 ? ` · ${skipped} skipped, see next screen` : "";
22497
+ if (custom.fileCount > 0) {
22498
+ choices.push({
22499
+ label: "Custom",
22500
+ value: CUSTOM,
22501
+ checked: enabled,
22502
+ hint: `${plural(custom.fileCount)} in ${custom.scopes.join(" + ")}${skippedNote}`
22503
+ });
22504
+ } else {
22505
+ choices.push({
22506
+ label: "Custom",
22507
+ value: CUSTOM,
22508
+ locked: true,
22509
+ checked: false,
22510
+ summaryExclude: true,
22511
+ hint: skipped > 0 ? `${plural(skipped)} found but NOT loaded — see next screen` : "none yet · drop *-policies.mjs in .failproofai/policies/"
22512
+ });
22513
+ }
22336
22514
  return choices;
22337
22515
  }
22338
22516
  function resolvePresetSelection(values) {
22339
- if (values.includes(EVERYTHING))
22517
+ const selected = values.filter((v) => v !== CUSTOM);
22518
+ if (selected.includes(EVERYTHING))
22340
22519
  return resolveEverything();
22341
- return [...new Set(values.flatMap((id) => resolvePreset(id)))];
22520
+ return [...new Set(selected.flatMap((id) => resolvePreset(id)))];
22521
+ }
22522
+ function setCustomPoliciesEnabled(scope, cwd, enabled) {
22523
+ if (enabled === undefined)
22524
+ return;
22525
+ const path3 = getConfigPathForScope(scope, cwd);
22526
+ let config = {};
22527
+ try {
22528
+ if (existsSync19(path3))
22529
+ config = JSON.parse(readFileSync16(path3, "utf8"));
22530
+ } catch {
22531
+ return;
22532
+ }
22533
+ if (enabled)
22534
+ delete config.customPoliciesEnabled;
22535
+ else
22536
+ config.customPoliciesEnabled = false;
22537
+ try {
22538
+ writeFileSync9(path3, JSON.stringify(config, null, 2) + `
22539
+ `, "utf8");
22540
+ } catch {}
22541
+ }
22542
+ function describeCustomPolicies(cwd) {
22543
+ const active = [];
22544
+ const warnings = [];
22545
+ const scopes = [];
22546
+ let fileCount = 0;
22547
+ const dirs = [
22548
+ { dir: resolve16(cwd, ".failproofai", "policies"), label: "project" },
22549
+ { dir: resolve16(homedir30(), ".failproofai", "policies"), label: "global" }
22550
+ ];
22551
+ for (const { dir, label } of dirs) {
22552
+ const found = discoverPolicyFiles(dir);
22553
+ if (found.length > 0) {
22554
+ active.push(`${found.length} file${found.length === 1 ? "" : "s"} (${label})`);
22555
+ scopes.push(label);
22556
+ fileCount += found.length;
22557
+ }
22558
+ for (const name of findSkippedPolicyFiles(dir)) {
22559
+ warnings.push(`! ${homeify(resolve16(dir, name))} is NOT loaded — rename to ` + `${name.replace(/\.(js|mjs|ts)$/, "-policies.$1")}`);
22560
+ }
22561
+ }
22562
+ return { active, warnings, fileCount, scopes };
22342
22563
  }
22343
22564
  function reviewLines(state) {
22344
- const { scope, clis, policies, cwd } = state;
22565
+ const { scope, clis, policies, cwd, customEnabled } = state;
22345
22566
  const where = scope === "project" ? `This project (${homeify(cwd)})` : "Everywhere (global)";
22346
22567
  const lines = [];
22347
22568
  const assistantNames = clis.map((c3) => getIntegration(c3).displayName);
22348
22569
  lines.push(` Where : ${where}`);
22349
22570
  lines.push(` Assistants : ${assistantNames.length ? summarize(assistantNames, "assistants") : "(none)"}`);
22350
22571
  lines.push(` Policies : ${policies.length} enabled`);
22572
+ const custom = describeCustomPolicies(cwd);
22573
+ if (custom.active.length > 0) {
22574
+ lines.push(` Custom : ${custom.active.join(" · ")} ${customEnabled === false ? "— DISABLED, will not load" : DIM_NOTE}`);
22575
+ }
22576
+ for (const warning of custom.warnings)
22577
+ lines.push(` ${warning}`);
22351
22578
  lines.push("");
22352
22579
  lines.push(" This will update:");
22353
22580
  for (const cli of clis) {
@@ -22437,7 +22664,8 @@ Use the flag form instead, e.g.:
22437
22664
  {
22438
22665
  label: "Everything available",
22439
22666
  value: ALL_CLIS,
22440
- hint: `protect all ${INTEGRATION_TYPES.length} supported CLIs`
22667
+ hint: `protect all ${clisSupportingScope(scope).length} CLIs configurable here`,
22668
+ summaryExclude: true
22441
22669
  },
22442
22670
  ...buildAgentChoices(scope, cwd)
22443
22671
  ],
@@ -22449,10 +22677,14 @@ Use the flag form instead, e.g.:
22449
22677
  });
22450
22678
  if (clisSel === null)
22451
22679
  return cancel();
22452
- const clis = clisSel.includes(ALL_CLIS) ? [...INTEGRATION_TYPES] : clisSel.filter((v) => v !== ALL_CLIS);
22680
+ const supported = new Set(clisSupportingScope(scope));
22681
+ const clis = (clisSel.includes(ALL_CLIS) ? [...INTEGRATION_TYPES] : clisSel.filter((v) => v !== ALL_CLIS)).filter((id) => supported.has(id));
22682
+ const customEnabledBefore = readHooksConfig().customPoliciesEnabled !== false;
22683
+ const presetChoices = buildPresetChoices(cwd, customEnabledBefore);
22684
+ const hasCustomFiles = describeCustomPolicies(cwd).fileCount > 0;
22453
22685
  const presets = await multiSelect({
22454
22686
  message: "What should we guard against?",
22455
- choices: buildPresetChoices(),
22687
+ choices: presetChoices,
22456
22688
  minSelected: 1,
22457
22689
  summaryNoun: "bundles",
22458
22690
  hint: "space toggles · combine presets · ↵ confirm",
@@ -22462,9 +22694,10 @@ Use the flag form instead, e.g.:
22462
22694
  if (presets === null)
22463
22695
  return cancel();
22464
22696
  const policies = resolvePresetSelection(presets);
22697
+ const customEnabled = hasCustomFiles ? presets.includes(CUSTOM) : undefined;
22465
22698
  const decision = await selectOne({
22466
22699
  message: "Ready to apply?",
22467
- body: reviewLines({ scope, clis, policies, cwd }),
22700
+ body: reviewLines({ scope, clis, policies, cwd, customEnabled }),
22468
22701
  choices: [
22469
22702
  { label: "Yes, apply now", value: "apply", hint: "write the config" },
22470
22703
  { label: "Cancel", value: "cancel", hint: "quit, no changes" }
@@ -22482,13 +22715,15 @@ Use the flag form instead, e.g.:
22482
22715
  source: presets.join("+")
22483
22716
  });
22484
22717
  await installHooks2(policies, scope, cwd, false, "configure-wizard", undefined, false, clis, { replace: true, quiet: true });
22718
+ setCustomPoliciesEnabled(scope, cwd, customEnabled);
22485
22719
  await applied;
22486
22720
  markLauncherSeen();
22487
- const agentNames = clis.map((c3) => getIntegration(c3).displayName).join(", ");
22488
- outro(`Setup complete ${policies.length} policies guarding ${agentNames}.`, { ok: true }, stdout);
22721
+ const customNote = customEnabled === true ? " + your custom policies" : customEnabled === false ? " · custom policies DISABLED" : "";
22722
+ const assistants = `${clis.length} assistant${clis.length === 1 ? "" : "s"}`;
22723
+ outro(`Setup complete — ${policies.length} policies${customNote} · ${assistants}`, { ok: true }, stdout);
22489
22724
  return { applied: true, scope, clis, policies };
22490
22725
  }
22491
- var EVERYTHING = "__everything__", ALL_CLIS = "__all_clis__";
22726
+ var EVERYTHING = "__everything__", ALL_CLIS = "__all_clis__", CUSTOM = "__custom__", DIM_NOTE = "(auto-loaded)";
22492
22727
  var init_configure_wizard = __esm(() => {
22493
22728
  init_tui();
22494
22729
  init_integrations();
@@ -22496,6 +22731,7 @@ var init_configure_wizard = __esm(() => {
22496
22731
  init_manager2();
22497
22732
  init_hooks_config();
22498
22733
  init_policy_presets();
22734
+ init_custom_hooks_loader();
22499
22735
  init_hook_telemetry2();
22500
22736
  init_telemetry_id2();
22501
22737
  });
@@ -22621,7 +22857,7 @@ import { realpathSync as realpathSync4 } from "fs";
22621
22857
  import { dirname as dirname9, resolve as resolve18 } from "path";
22622
22858
  import { fileURLToPath as fileURLToPath4 } from "url";
22623
22859
  // package.json
22624
- var version = "0.0.14-beta.1";
22860
+ var version = "0.0.14-beta.2";
22625
22861
 
22626
22862
  // bin/failproofai.mjs
22627
22863
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {