failproofai 0.0.2-beta.6 → 0.0.2-beta.8

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 (162) hide show
  1. package/.next/standalone/.claude/settings.json +316 -0
  2. package/.next/standalone/.failproofai/policies/workflow-policies.mjs +62 -0
  3. package/.next/standalone/.failproofai/policies-config.json +39 -0
  4. package/.next/standalone/.next/BUILD_ID +1 -1
  5. package/.next/standalone/.next/build-manifest.json +5 -5
  6. package/.next/standalone/.next/prerender-manifest.json +3 -3
  7. package/.next/standalone/.next/required-server-files.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  9. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  16. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  26. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  27. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  30. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  31. package/.next/standalone/.next/server/app/index.html +1 -1
  32. package/.next/standalone/.next/server/app/index.rsc +15 -15
  33. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  34. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  35. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  37. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0u_n1xe._.js → [root-of-the-server]__0.t2266._.js} +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0epc5zr._.js → [root-of-the-server]__0pjorff._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +8 -9
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0a_7sdg.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0j79~gv.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0pbja1x.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0r6o0i2.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11y81~_.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12or2kf.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
  83. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  84. package/.next/standalone/.next/server/pages/404.html +2 -2
  85. package/.next/standalone/.next/server/pages/500.html +1 -1
  86. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  87. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  88. package/.next/standalone/.next/static/chunks/{0efsuf1p-k4qe.js → 04xfyqyhdxbxz.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{17p200_z1ivz4.js → 07g0rbtaux_1t.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{031pa5~qfzt~_.js → 09e7drilkf1sn.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{0tood0~87-mm8.js → 0a_xh94bt.y0j.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/{0rvepm.~uvks4.js → 0j752uotyfvjh.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/{0wkzaq-8sxss7.js → 0qi0ubup__3pj.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/{0kbfx4p.g9wnr.js → 0xyvis4r_y.8o.js} +2 -2
  95. package/.next/standalone/.next/static/chunks/{0jqg886bw85_6.js → 0zfyfi1suoteq.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0_tx_~f8pi3d7.js → 121a-0zn-knuy.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{turbopack-0uc5y~g6h.n7-.js → turbopack-0r26pc8h0y_-e.js} +1 -1
  98. package/.next/standalone/CHANGELOG.md +88 -0
  99. package/.next/standalone/CLAUDE.md +14 -0
  100. package/.next/standalone/README.md +20 -3
  101. package/.next/standalone/bin/failproofai.mjs +5 -0
  102. package/.next/standalone/bun.lock +31 -63
  103. package/.next/standalone/dist/cli.mjs +268 -73
  104. package/.next/standalone/docs/built-in-policies.mdx +19 -3
  105. package/.next/standalone/docs/configuration.mdx +46 -0
  106. package/.next/standalone/docs/custom-policies.mdx +65 -7
  107. package/.next/standalone/docs/docs.json +3 -3
  108. package/.next/standalone/examples/convention-policies/security-policies.mjs +40 -0
  109. package/.next/standalone/examples/convention-policies/workflow-policies.mjs +41 -0
  110. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  111. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  112. package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/index.js +2 -2
  113. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  114. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  115. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
  116. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  117. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  118. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +7 -2
  119. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  120. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  121. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  122. package/.next/standalone/node_modules/next/dist/server/render.js +20 -19
  123. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  124. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  125. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  126. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  127. package/.next/standalone/node_modules/next/package.json +15 -15
  128. package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
  129. package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
  130. package/.next/standalone/node_modules/react/package.json +1 -1
  131. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
  132. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
  133. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
  134. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
  135. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
  136. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
  137. package/.next/standalone/node_modules/react-dom/package.json +2 -2
  138. package/.next/standalone/package.json +1 -1
  139. package/.next/standalone/server.js +1 -1
  140. package/.next/standalone/src/hooks/builtin-policies.ts +70 -18
  141. package/.next/standalone/src/hooks/custom-hooks-loader.ts +165 -21
  142. package/.next/standalone/src/hooks/handler.ts +32 -6
  143. package/.next/standalone/src/hooks/hooks-config.ts +47 -2
  144. package/.next/standalone/src/hooks/llm-client.ts +2 -2
  145. package/.next/standalone/src/hooks/loader-utils.ts +4 -4
  146. package/.next/standalone/src/hooks/manager.ts +57 -14
  147. package/.next/standalone/src/hooks/policy-evaluator.ts +35 -17
  148. package/README.md +20 -3
  149. package/bin/failproofai.mjs +5 -0
  150. package/dist/cli.mjs +268 -73
  151. package/package.json +1 -1
  152. package/src/hooks/builtin-policies.ts +70 -18
  153. package/src/hooks/custom-hooks-loader.ts +165 -21
  154. package/src/hooks/handler.ts +32 -6
  155. package/src/hooks/hooks-config.ts +47 -2
  156. package/src/hooks/llm-client.ts +2 -2
  157. package/src/hooks/loader-utils.ts +4 -4
  158. package/src/hooks/manager.ts +57 -14
  159. package/src/hooks/policy-evaluator.ts +35 -17
  160. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → itedhTSyIDln6TUf41j5X}/_buildManifest.js +0 -0
  161. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → itedhTSyIDln6TUf41j5X}/_clientMiddlewareManifest.js +0 -0
  162. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → itedhTSyIDln6TUf41j5X}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  [![npm](https://img.shields.io/npm/v/failproofai?style=flat-square&color=CB3837)](https://www.npmjs.com/package/failproofai)
14
14
  [![License](https://img.shields.io/badge/license-MIT%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSE)
15
15
  [![CI](https://img.shields.io/github/actions/workflow/status/exospherehost/failproofai/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/exospherehost/failproofai/actions)
16
- [![Discord](https://img.shields.io/discord/1234567890?style=flat-square&label=Discord&color=5865F2)](https://discord.com/invite/zT92CAgvkj)
16
+ [![Slack](https://img.shields.io/badge/Slack-join%20us-4A154B?style=flat-square&logo=slack)](https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ)
17
17
 
18
18
  The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously - for **Claude Code** & the **Agents SDK**.
19
19
 
@@ -111,10 +111,12 @@ Policy configuration lives in `~/.failproofai/policies-config.json` (global) or
111
111
  ],
112
112
  "policyParams": {
113
113
  "block-sudo": {
114
- "allowPatterns": ["sudo systemctl status", "sudo journalctl"]
114
+ "allowPatterns": ["sudo systemctl status", "sudo journalctl"],
115
+ "hint": "Use apt-get directly without sudo."
115
116
  },
116
117
  "block-push-master": {
117
- "protectedBranches": ["main", "release", "prod"]
118
+ "protectedBranches": ["main", "release", "prod"],
119
+ "hint": "Try creating a fresh branch instead."
118
120
  },
119
121
  "sanitize-api-keys": {
120
122
  "additionalPatterns": [
@@ -216,6 +218,21 @@ failproofai policies --install --custom ./my-policies.js
216
218
 
217
219
  Custom hooks support transitive local imports, async/await, and access to `process.env`. Errors are fail-open (logged to `~/.failproofai/hook.log`, built-in policies continue). See [docs/custom-hooks.mdx](docs/custom-hooks.mdx) for the full guide.
218
220
 
221
+ ### Convention-based policies (v0.0.2-beta.7+)
222
+
223
+ Drop `*policies.{js,mjs,ts}` files into `.failproofai/policies/` and they're automatically loaded — no `--custom` flag or config changes needed. Works like git hooks: drop a file, it just works.
224
+
225
+ ```text
226
+ # Project level — committed to git, shared with the team
227
+ .failproofai/policies/security-policies.mjs
228
+ .failproofai/policies/workflow-policies.mjs
229
+
230
+ # User level — personal, applies to all projects
231
+ ~/.failproofai/policies/my-policies.mjs
232
+ ```
233
+
234
+ Both levels load (union). Files are loaded alphabetically within each directory. Prefix with `01-`, `02-`, etc. to control order. See [examples/convention-policies/](examples/convention-policies/) for ready-to-use examples.
235
+
219
236
  ---
220
237
 
221
238
  ## Telemetry
@@ -97,6 +97,11 @@ COMMANDS
97
97
  --version, -v Print version and exit
98
98
  --help, -h Show this help message
99
99
 
100
+ CONVENTION POLICIES
101
+ Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
102
+ Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
103
+ No --custom flag or config changes needed — just drop files and they're picked up.
104
+
100
105
  EXAMPLES
101
106
  failproofai policies
102
107
  failproofai policies --install
package/dist/cli.mjs CHANGED
@@ -149,11 +149,19 @@ function readMergedHooksConfig(cwd) {
149
149
  ...llm !== undefined ? { llm } : {}
150
150
  };
151
151
  }
152
- function getConfigPath() {
153
- return resolve(homedir2(), ".failproofai", "policies-config.json");
152
+ function getConfigPathForScope(scope, cwd) {
153
+ const base = cwd ? resolve(cwd) : process.cwd();
154
+ switch (scope) {
155
+ case "user":
156
+ return resolve(homedir2(), ".failproofai", "policies-config.json");
157
+ case "project":
158
+ return resolve(base, ".failproofai", "policies-config.json");
159
+ case "local":
160
+ return resolve(base, ".failproofai", "policies-config.local.json");
161
+ }
154
162
  }
155
- function readHooksConfig() {
156
- const configPath = getConfigPath();
163
+ function readScopedHooksConfig(scope, cwd) {
164
+ const configPath = getConfigPathForScope(scope, cwd);
157
165
  if (!existsSync2(configPath)) {
158
166
  return { enabledPolicies: [] };
159
167
  }
@@ -165,8 +173,8 @@ function readHooksConfig() {
165
173
  return { enabledPolicies: [] };
166
174
  }
167
175
  }
168
- function writeHooksConfig(config) {
169
- const configPath = getConfigPath();
176
+ function writeScopedHooksConfig(config, scope, cwd) {
177
+ const configPath = getConfigPathForScope(scope, cwd);
170
178
  const dir = dirname(configPath);
171
179
  if (!existsSync2(dir)) {
172
180
  mkdirSync2(dir, { recursive: true });
@@ -282,6 +290,37 @@ function getCurrentBranch(cwd) {
282
290
  return null;
283
291
  }
284
292
  }
293
+ function getHeadSha(cwd) {
294
+ try {
295
+ const sha = execSync("git rev-parse HEAD", {
296
+ cwd,
297
+ encoding: "utf8",
298
+ timeout: 3000
299
+ }).trim();
300
+ return sha || null;
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+ function getThirdPartyCheckRuns(cwd, sha) {
306
+ try {
307
+ const json = execFileSync("gh", [
308
+ "api",
309
+ `repos/{owner}/{repo}/commits/${sha}/check-runs`,
310
+ "--jq",
311
+ '.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
312
+ ], {
313
+ cwd,
314
+ encoding: "utf8",
315
+ timeout: 15000
316
+ }).trim();
317
+ if (!json || json === "[]")
318
+ return [];
319
+ return JSON.parse(json);
320
+ } catch {
321
+ return [];
322
+ }
323
+ }
285
324
  function matchesAllowedPattern(cmd, pattern) {
286
325
  const cmdTokens = parseArgvTokens(cmd);
287
326
  const patTokens = parseArgvTokens(pattern);
@@ -914,22 +953,27 @@ function requireCiGreenBeforeStop(ctx) {
914
953
  const branch = getCurrentBranch(cwd);
915
954
  if (!branch || branch === "HEAD")
916
955
  return allow("Detached HEAD, skipping CI check.");
917
- const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], {
918
- cwd,
919
- encoding: "utf8",
920
- timeout: 15000
921
- }).trim();
922
- if (!runsJson || runsJson === "[]")
923
- return allow(`No CI runs found for branch "${branch}".`);
924
- const runs = JSON.parse(runsJson);
925
- if (runs.length === 0)
956
+ let workflowRuns = [];
957
+ try {
958
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], { cwd, encoding: "utf8", timeout: 15000 }).trim();
959
+ if (runsJson && runsJson !== "[]") {
960
+ workflowRuns = JSON.parse(runsJson);
961
+ }
962
+ } catch {}
963
+ let thirdPartyChecks = [];
964
+ const sha = getHeadSha(cwd);
965
+ if (sha) {
966
+ thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
967
+ }
968
+ const allChecks = [...workflowRuns, ...thirdPartyChecks];
969
+ if (allChecks.length === 0)
926
970
  return allow(`No CI runs found for branch "${branch}".`);
927
- const failing = runs.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
971
+ const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
928
972
  if (failing.length > 0) {
929
973
  const names = failing.map((r) => `"${r.name}"`).join(", ");
930
974
  return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
931
975
  }
932
- const pending = runs.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
976
+ const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
933
977
  if (pending.length > 0) {
934
978
  const names = pending.map((r) => `"${r.name}"`).join(", ");
935
979
  return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
@@ -1334,6 +1378,15 @@ var init_builtin_policies = __esm(() => {
1334
1378
  });
1335
1379
 
1336
1380
  // src/hooks/policy-evaluator.ts
1381
+ function appendHint(baseReason, hint) {
1382
+ const base = baseReason.trim();
1383
+ const normalizedHint = typeof hint === "string" ? hint.trim() : "";
1384
+ if (!normalizedHint)
1385
+ return base;
1386
+ if (!base)
1387
+ return normalizedHint;
1388
+ return `${base}. ${normalizedHint}`;
1389
+ }
1337
1390
  async function evaluatePolicies(eventType, payload, session, config) {
1338
1391
  const toolName = payload.tool_name;
1339
1392
  const toolInput = payload.tool_input;
@@ -1349,8 +1402,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1349
1402
  toolInput,
1350
1403
  session
1351
1404
  };
1352
- let instructPolicyName = null;
1353
- let instructReason = null;
1405
+ const instructEntries = [];
1354
1406
  const allowEntries = [];
1355
1407
  for (const policy of policies) {
1356
1408
  const schema = POLICY_PARAMS_MAP.get(policy.name);
@@ -1373,7 +1425,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1373
1425
  continue;
1374
1426
  }
1375
1427
  if (result.decision === "deny") {
1376
- const reason = result.reason ?? `Blocked by policy: ${policy.name}`;
1428
+ const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1377
1429
  hookLogInfo(`deny by "${policy.name}": ${reason}`);
1378
1430
  const displayTool = ctx.toolName ?? "unknown tool";
1379
1431
  if (eventType === "PreToolUse") {
@@ -1418,38 +1470,43 @@ async function evaluatePolicies(eventType, payload, session, config) {
1418
1470
  decision: "deny"
1419
1471
  };
1420
1472
  }
1421
- if (result.decision === "instruct" && !instructPolicyName) {
1422
- instructPolicyName = policy.name;
1423
- instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1424
- hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1473
+ if (result.decision === "instruct") {
1474
+ const reason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1475
+ instructEntries.push({ policyName: policy.name, reason });
1476
+ hookLogInfo(`instruct by "${policy.name}": ${reason}`);
1425
1477
  }
1426
1478
  if (result.decision === "allow" && result.reason) {
1427
1479
  allowEntries.push({ policyName: policy.name, reason: result.reason });
1428
1480
  }
1429
1481
  }
1430
- if (instructPolicyName && instructReason) {
1482
+ if (instructEntries.length > 0) {
1483
+ const combined = instructEntries.map((e) => e.reason).join(`
1484
+ `);
1485
+ const policyNames = instructEntries.map((e) => e.policyName);
1431
1486
  if (eventType === "Stop") {
1432
1487
  return {
1433
1488
  exitCode: 2,
1434
1489
  stdout: "",
1435
- stderr: instructReason,
1436
- policyName: instructPolicyName,
1437
- reason: instructReason,
1490
+ stderr: combined,
1491
+ policyName: policyNames[0],
1492
+ policyNames,
1493
+ reason: combined,
1438
1494
  decision: "instruct"
1439
1495
  };
1440
1496
  }
1441
1497
  const response = {
1442
1498
  hookSpecificOutput: {
1443
1499
  hookEventName: eventType,
1444
- additionalContext: `Instruction from failproofai: ${instructReason}`
1500
+ additionalContext: `Instruction from failproofai: ${combined}`
1445
1501
  }
1446
1502
  };
1447
1503
  return {
1448
1504
  exitCode: 0,
1449
1505
  stdout: JSON.stringify(response),
1450
1506
  stderr: "",
1451
- policyName: instructPolicyName,
1452
- reason: instructReason,
1507
+ policyName: policyNames[0],
1508
+ policyNames,
1509
+ reason: combined,
1453
1510
  decision: "instruct"
1454
1511
  };
1455
1512
  }
@@ -1533,10 +1590,9 @@ async function createEsmShim(distIndex, distUrl) {
1533
1590
  const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
1534
1591
  const shimCode = [
1535
1592
  `import _cjs from '${distUrl}';`,
1536
- `export const createApp = _cjs.createApp;`,
1537
- `export const getQueueCondition = _cjs.getQueueCondition;`,
1538
- `export const clearQueueCondition = _cjs.clearQueueCondition;`,
1539
1593
  `export const customPolicies = _cjs.customPolicies;`,
1594
+ `export const getCustomHooks = _cjs.getCustomHooks;`,
1595
+ `export const clearCustomHooks = _cjs.clearCustomHooks;`,
1540
1596
  `export const allow = _cjs.allow;`,
1541
1597
  `export const deny = _cjs.deny;`,
1542
1598
  `export const instruct = _cjs.instruct;`,
@@ -1616,20 +1672,21 @@ var init_loader_utils = __esm(() => {
1616
1672
  });
1617
1673
 
1618
1674
  // src/hooks/custom-hooks-loader.ts
1619
- import { resolve as resolve4, isAbsolute } from "node:path";
1620
- import { existsSync as existsSync3 } from "node:fs";
1675
+ import { resolve as resolve4, isAbsolute, basename } from "node:path";
1676
+ import { existsSync as existsSync3, readdirSync } from "node:fs";
1621
1677
  import { pathToFileURL as pathToFileURL2 } from "node:url";
1622
- async function loadCustomHooks(customPoliciesPath, opts) {
1623
- if (!customPoliciesPath)
1678
+ import { homedir as homedir4 } from "node:os";
1679
+ function discoverPolicyFiles(dir) {
1680
+ if (!existsSync3(dir))
1624
1681
  return [];
1625
- const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(process.cwd(), customPoliciesPath);
1626
- if (!existsSync3(absPath)) {
1627
- if (opts?.strict)
1628
- throw new Error(`Custom hooks file not found: ${absPath}`);
1629
- hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1682
+ try {
1683
+ const entries = readdirSync(dir, { withFileTypes: true });
1684
+ return entries.filter((e) => e.isFile() && CONVENTION_FILE_RE.test(e.name)).sort((a, b) => a.name.localeCompare(b.name)).map((e) => resolve4(dir, e.name));
1685
+ } catch {
1630
1686
  return [];
1631
1687
  }
1632
- clearCustomHooks();
1688
+ }
1689
+ async function loadSingleFile(absPath, opts) {
1633
1690
  const g = globalThis;
1634
1691
  g[LOADING_KEY] = true;
1635
1692
  let tmpFiles = [];
@@ -1645,18 +1702,93 @@ async function loadCustomHooks(customPoliciesPath, opts) {
1645
1702
  if (opts?.strict)
1646
1703
  throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
1647
1704
  hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
1648
- return [];
1649
1705
  } finally {
1650
1706
  g[LOADING_KEY] = false;
1651
1707
  await cleanupTmpFiles(tmpFiles);
1652
1708
  }
1709
+ }
1710
+ async function loadCustomHooks(customPoliciesPath, opts) {
1711
+ if (!customPoliciesPath)
1712
+ return [];
1713
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
1714
+ if (!existsSync3(absPath)) {
1715
+ if (opts?.strict)
1716
+ throw new Error(`Custom hooks file not found: ${absPath}`);
1717
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1718
+ return [];
1719
+ }
1720
+ clearCustomHooks();
1721
+ await loadSingleFile(absPath, opts);
1653
1722
  return getCustomHooks();
1654
1723
  }
1655
- var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
1724
+ async function loadAllCustomHooks(customPoliciesPath, opts) {
1725
+ clearCustomHooks();
1726
+ const conventionSources = [];
1727
+ if (customPoliciesPath) {
1728
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
1729
+ if (existsSync3(absPath)) {
1730
+ await loadSingleFile(absPath);
1731
+ } else {
1732
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1733
+ }
1734
+ }
1735
+ const hooksBeforeConvention = getCustomHooks().length;
1736
+ const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
1737
+ const projectFiles = discoverPolicyFiles(projectDir);
1738
+ for (const file of projectFiles) {
1739
+ const hooksBefore = getCustomHooks().length;
1740
+ await loadSingleFile(file);
1741
+ const newHooks = getCustomHooks().slice(hooksBefore);
1742
+ if (newHooks.length > 0) {
1743
+ conventionSources.push({
1744
+ scope: "project",
1745
+ file: basename(file),
1746
+ hookNames: newHooks.map((h) => h.name)
1747
+ });
1748
+ }
1749
+ }
1750
+ const userDir = resolve4(homedir4(), ".failproofai", "policies");
1751
+ const userFiles = discoverPolicyFiles(userDir);
1752
+ for (const file of userFiles) {
1753
+ const hooksBefore = getCustomHooks().length;
1754
+ await loadSingleFile(file);
1755
+ const newHooks = getCustomHooks().slice(hooksBefore);
1756
+ if (newHooks.length > 0) {
1757
+ conventionSources.push({
1758
+ scope: "user",
1759
+ file: basename(file),
1760
+ hookNames: newHooks.map((h) => h.name)
1761
+ });
1762
+ }
1763
+ }
1764
+ const allHooks = getCustomHooks();
1765
+ const conventionCount = allHooks.length - hooksBeforeConvention;
1766
+ if (projectFiles.length > 0 || userFiles.length > 0) {
1767
+ hookLogInfo(`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`);
1768
+ }
1769
+ const hookNameToScope = new Map;
1770
+ for (const source of conventionSources) {
1771
+ for (const name of source.hookNames) {
1772
+ hookNameToScope.set(name, source.scope);
1773
+ }
1774
+ }
1775
+ const conventionHookRefs = new Set;
1776
+ for (const hook of allHooks.slice(hooksBeforeConvention)) {
1777
+ conventionHookRefs.add(hook);
1778
+ }
1779
+ for (const hook of allHooks) {
1780
+ if (conventionHookRefs.has(hook)) {
1781
+ hook.__conventionScope = hookNameToScope.get(hook.name) ?? "project";
1782
+ }
1783
+ }
1784
+ return { hooks: allHooks, conventionSources };
1785
+ }
1786
+ var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE;
1656
1787
  var init_custom_hooks_loader = __esm(() => {
1657
1788
  init_hook_logger();
1658
1789
  init_custom_hooks_registry();
1659
1790
  init_loader_utils();
1791
+ CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
1660
1792
  });
1661
1793
 
1662
1794
  // src/hooks/hook-activity-store.ts
@@ -1665,14 +1797,14 @@ import {
1665
1797
  writeFileSync as writeFileSync2,
1666
1798
  appendFileSync as appendFileSync2,
1667
1799
  renameSync as renameSync2,
1668
- readdirSync,
1800
+ readdirSync as readdirSync2,
1669
1801
  mkdirSync as mkdirSync3,
1670
1802
  existsSync as existsSync4,
1671
1803
  statSync as statSync2,
1672
1804
  unlinkSync
1673
1805
  } from "node:fs";
1674
1806
  import { join as join3 } from "node:path";
1675
- import { homedir as homedir4 } from "node:os";
1807
+ import { homedir as homedir5 } from "node:os";
1676
1808
  function ensureDir() {
1677
1809
  if (!existsSync4(storeDir)) {
1678
1810
  mkdirSync3(storeDir, { recursive: true });
@@ -1777,12 +1909,12 @@ function updateStats(entry) {
1777
1909
  }
1778
1910
  var PAGE_SIZE = 25, DEFAULT_STORE_DIR, CURRENT_FILE = "current.jsonl", COUNT_FILE = "current.count", STATS_FILE = "stats.json", LOCK_FILE = "current.lock", LOCK_STALE_MS = 2000, storeDir, rotateSeq = 0;
1779
1911
  var init_hook_activity_store = __esm(() => {
1780
- DEFAULT_STORE_DIR = join3(homedir4(), ".failproofai", "cache", "hook-activity");
1912
+ DEFAULT_STORE_DIR = join3(homedir5(), ".failproofai", "cache", "hook-activity");
1781
1913
  storeDir = DEFAULT_STORE_DIR;
1782
1914
  });
1783
1915
 
1784
1916
  // package.json
1785
- var version2 = "0.0.2-beta.6";
1917
+ var version2 = "0.0.2-beta.8";
1786
1918
  var init_package = () => {};
1787
1919
 
1788
1920
  // src/posthog-key.ts
@@ -1944,9 +2076,14 @@ async function handleHookEvent(eventType) {
1944
2076
  const config = readMergedHooksConfig(session.cwd);
1945
2077
  clearPolicies();
1946
2078
  registerBuiltinPolicies(config.enabledPolicies);
1947
- const customHooksList = await loadCustomHooks(config.customPoliciesPath);
2079
+ const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
2080
+ const customHooksList = loadResult.hooks;
2081
+ const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
1948
2082
  for (const hook of customHooksList) {
1949
2083
  const hookName = hook.name;
2084
+ const conventionScope = hook.__conventionScope;
2085
+ const isConvention = !!conventionScope;
2086
+ const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
1950
2087
  const fn = async (ctx) => {
1951
2088
  try {
1952
2089
  const result2 = await Promise.race([
@@ -1957,16 +2094,18 @@ async function handleHookEvent(eventType) {
1957
2094
  } catch (err) {
1958
2095
  const msg = err instanceof Error ? err.message : String(err);
1959
2096
  const isTimeout = msg === "timeout";
1960
- hookLogWarn(`custom hook "${hookName}" failed: ${msg}`);
2097
+ hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
1961
2098
  trackHookEvent(getInstanceId(), "custom_hook_error", {
1962
2099
  hook_name: hookName,
1963
2100
  error_type: isTimeout ? "timeout" : "exception",
1964
- event_type: eventType
2101
+ event_type: eventType,
2102
+ is_convention_policy: isConvention,
2103
+ convention_scope: conventionScope ?? null
1965
2104
  });
1966
2105
  return { decision: "allow" };
1967
2106
  }
1968
2107
  };
1969
- registerPolicy(`custom/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
2108
+ registerPolicy(`${prefix}/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
1970
2109
  }
1971
2110
  if (customHooksList.length > 0) {
1972
2111
  trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
@@ -1975,7 +2114,16 @@ async function handleHookEvent(eventType) {
1975
2114
  event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
1976
2115
  });
1977
2116
  }
1978
- hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length}`);
2117
+ if (loadResult.conventionSources.length > 0) {
2118
+ trackHookEvent(getInstanceId(), "convention_policies_loaded", {
2119
+ event_type: eventType,
2120
+ project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
2121
+ user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
2122
+ convention_hook_count: conventionHookNames.size,
2123
+ convention_hook_names: [...conventionHookNames]
2124
+ });
2125
+ }
2126
+ hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
1979
2127
  const result = await evaluatePolicies(eventType, parsed, session, config);
1980
2128
  const durationMs = Math.round(performance.now() - startTime);
1981
2129
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
@@ -2007,7 +2155,9 @@ async function handleHookEvent(eventType) {
2007
2155
  if (result.decision === "deny" || result.decision === "instruct") {
2008
2156
  try {
2009
2157
  const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
2010
- const hasCustomParams = !isCustomHook && !!(result.policyName && config.policyParams?.[result.policyName]);
2158
+ const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
2159
+ const conventionScope = isConventionPolicy ? result.policyName.match(/^\.failproofai-(project|user)\//)?.[1] ?? null : null;
2160
+ const hasCustomParams = !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
2011
2161
  const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
2012
2162
  const distinctId = getInstanceId();
2013
2163
  await trackHookEvent(distinctId, "hook_policy_triggered", {
@@ -2016,6 +2166,8 @@ async function handleHookEvent(eventType) {
2016
2166
  policy_name: result.policyName,
2017
2167
  decision: result.decision,
2018
2168
  is_custom_hook: isCustomHook,
2169
+ is_convention_policy: isConventionPolicy,
2170
+ convention_scope: conventionScope,
2019
2171
  has_custom_params: hasCustomParams,
2020
2172
  param_keys_overridden: paramKeysOverridden
2021
2173
  });
@@ -2348,13 +2500,13 @@ __export(exports_manager, {
2348
2500
  });
2349
2501
  import { execSync as execSync3 } from "node:child_process";
2350
2502
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
2351
- import { resolve as resolve5, dirname as dirname3 } from "node:path";
2352
- import { homedir as homedir5, platform, arch, release, hostname } from "node:os";
2503
+ import { resolve as resolve5, dirname as dirname3, basename as basename2 } from "node:path";
2504
+ import { homedir as homedir6, platform, arch, release, hostname } from "node:os";
2353
2505
  function getSettingsPath(scope, cwd) {
2354
2506
  const base = cwd ? resolve5(cwd) : process.cwd();
2355
2507
  switch (scope) {
2356
2508
  case "user":
2357
- return resolve5(homedir5(), ".claude", "settings.json");
2509
+ return resolve5(homedir6(), ".claude", "settings.json");
2358
2510
  case "project":
2359
2511
  return resolve5(base, ".claude", "settings.json");
2360
2512
  case "local":
@@ -2481,7 +2633,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2481
2633
  }
2482
2634
  }
2483
2635
  const binaryPath = resolveFailproofaiBinary();
2484
- const previousConfig = readHooksConfig();
2636
+ const previousConfig = readScopedHooksConfig(scope, cwd);
2485
2637
  const previousEnabled = new Set(previousConfig.enabledPolicies);
2486
2638
  let selectedPolicies;
2487
2639
  if (policyNames !== undefined) {
@@ -2515,7 +2667,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2515
2667
  console.log(`
2516
2668
  Validated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`);
2517
2669
  }
2518
- writeHooksConfig(configToWrite);
2670
+ writeScopedHooksConfig(configToWrite, scope, cwd);
2519
2671
  console.log(`
2520
2672
  Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`);
2521
2673
  if (removeCustomHooks) {
@@ -2592,15 +2744,16 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
2592
2744
  }
2593
2745
  }
2594
2746
  async function removeHooks(policyNames, scope = "user", cwd, opts) {
2747
+ const configScope = scope === "all" ? "user" : scope;
2595
2748
  if (opts?.removeCustomHooks) {
2596
- const config = readHooksConfig();
2749
+ const config = readScopedHooksConfig(configScope, cwd);
2597
2750
  delete config.customPoliciesPath;
2598
- writeHooksConfig(config);
2751
+ writeScopedHooksConfig(config, configScope, cwd);
2599
2752
  console.log("Custom hooks path cleared.");
2600
2753
  }
2601
2754
  if (policyNames && policyNames.length > 0 && !(policyNames.length === 1 && policyNames[0] === "all")) {
2602
2755
  validatePolicyNames(policyNames);
2603
- const config = readHooksConfig();
2756
+ const config = readScopedHooksConfig(configScope, cwd);
2604
2757
  const removeSet = new Set(policyNames);
2605
2758
  const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
2606
2759
  const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
@@ -2614,7 +2767,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2614
2767
  enabledPolicies: remaining,
2615
2768
  ...filteredParams && Object.keys(filteredParams).length > 0 ? { policyParams: filteredParams } : {}
2616
2769
  };
2617
- writeHooksConfig(updatedConfig);
2770
+ writeScopedHooksConfig(updatedConfig, configScope, cwd);
2618
2771
  try {
2619
2772
  const distinctId = getInstanceId();
2620
2773
  const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
@@ -2635,7 +2788,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2635
2788
  console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
2636
2789
  return;
2637
2790
  }
2638
- const configBeforeRemoval = readHooksConfig();
2791
+ const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
2639
2792
  const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
2640
2793
  let totalRemoved = 0;
2641
2794
  for (const s of scopesToRemove) {
@@ -2682,10 +2835,18 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2682
2835
  hostname_hash: hashToId(hostname())
2683
2836
  });
2684
2837
  } catch {}
2685
- if (scope === "all" || !HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
2686
- const existingForClear = readHooksConfig();
2687
- const { customPoliciesPath: _drop, policyParams: _dropParams, ...restClear } = existingForClear;
2688
- writeHooksConfig({ ...restClear, enabledPolicies: [] });
2838
+ if (scope === "all") {
2839
+ for (const s of HOOK_SCOPES) {
2840
+ const existing = readScopedHooksConfig(s, cwd);
2841
+ if (existing.enabledPolicies.length > 0 || existing.customPoliciesPath || existing.policyParams) {
2842
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
2843
+ writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, s, cwd);
2844
+ }
2845
+ }
2846
+ } else if (!HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
2847
+ const existing = readScopedHooksConfig(configScope, cwd);
2848
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
2849
+ writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
2689
2850
  }
2690
2851
  }
2691
2852
  async function listHooks(cwd) {
@@ -2822,6 +2983,35 @@ Failproof AI Hook Policies
2822
2983
  }
2823
2984
  console.log();
2824
2985
  }
2986
+ const base = cwd ? resolve5(cwd) : process.cwd();
2987
+ const conventionDirs = [
2988
+ { label: "Project", dir: resolve5(base, ".failproofai", "policies") },
2989
+ { label: "User", dir: resolve5(homedir6(), ".failproofai", "policies") }
2990
+ ];
2991
+ for (const { label, dir } of conventionDirs) {
2992
+ const files = discoverPolicyFiles(dir);
2993
+ if (files.length === 0)
2994
+ continue;
2995
+ console.log(`
2996
+ ── Convention Policies — ${label} (${dir}) ──────────`);
2997
+ for (const file of files) {
2998
+ try {
2999
+ const hooks = await loadCustomHooks(file);
3000
+ if (hooks.length === 0) {
3001
+ const filename = basename2(file);
3002
+ console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
3003
+ } else {
3004
+ const filename = basename2(file);
3005
+ const hookSummary = hooks.map((h) => h.name).join(", ");
3006
+ console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
3007
+ }
3008
+ } catch {
3009
+ const filename = basename2(file);
3010
+ console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
3011
+ }
3012
+ }
3013
+ console.log();
3014
+ }
2825
3015
  }
2826
3016
  var VALID_POLICY_NAMES;
2827
3017
  var init_manager = __esm(() => {
@@ -2837,10 +3027,10 @@ var init_manager = __esm(() => {
2837
3027
  });
2838
3028
 
2839
3029
  // lib/paths.ts
2840
- import { homedir as homedir6 } from "os";
3030
+ import { homedir as homedir7 } from "os";
2841
3031
  import { join as join4 } from "path";
2842
3032
  function getDefaultClaudeProjectsPath() {
2843
- return join4(homedir6(), ".claude", "projects");
3033
+ return join4(homedir7(), ".claude", "projects");
2844
3034
  }
2845
3035
  var init_paths = () => {};
2846
3036
 
@@ -3009,7 +3199,7 @@ import { realpathSync as realpathSync2 } from "fs";
3009
3199
  import { dirname as dirname5, resolve as resolve8 } from "path";
3010
3200
  import { fileURLToPath as fileURLToPath2 } from "url";
3011
3201
  // package.json
3012
- var version = "0.0.2-beta.6";
3202
+ var version = "0.0.2-beta.8";
3013
3203
 
3014
3204
  // bin/failproofai.mjs
3015
3205
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -3073,6 +3263,11 @@ COMMANDS
3073
3263
  --version, -v Print version and exit
3074
3264
  --help, -h Show this help message
3075
3265
 
3266
+ CONVENTION POLICIES
3267
+ Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
3268
+ Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
3269
+ No --custom flag or config changes needed \u2014 just drop files and they're picked up.
3270
+
3076
3271
  EXAMPLES
3077
3272
  failproofai policies
3078
3273
  failproofai policies --install