failproofai 0.0.2-beta.6 → 0.0.2-beta.7

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 (159) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  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/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/.next/standalone/.next/server/app/index.html +1 -1
  29. package/.next/standalone/.next/server/app/index.rsc +15 -15
  30. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  31. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  32. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  33. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  34. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  37. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0u_n1xe._.js → [root-of-the-server]__05zi2mt._.js} +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0epc5zr._.js → [root-of-the-server]__0kkt_9z._.js} +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +8 -9
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0a_7sdg.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0j79~gv.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0pbja1x.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0r6o0i2.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11y81~_.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12or2kf.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
  80. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  81. package/.next/standalone/.next/server/pages/404.html +2 -2
  82. package/.next/standalone/.next/server/pages/500.html +1 -1
  83. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  85. package/.next/standalone/.next/static/chunks/{0tood0~87-mm8.js → 02u4v.k5amfah.js} +1 -1
  86. package/.next/standalone/.next/static/chunks/{031pa5~qfzt~_.js → 09e7drilkf1sn.js} +1 -1
  87. package/.next/standalone/.next/static/chunks/{0jqg886bw85_6.js → 0bkizbynk9via.js} +1 -1
  88. package/.next/standalone/.next/static/chunks/{17p200_z1ivz4.js → 0e76l4~hq_sei.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{0wkzaq-8sxss7.js → 0ltx5i0xv85_s.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{0efsuf1p-k4qe.js → 0q7atesxo-36k.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{0rvepm.~uvks4.js → 0suauczjqzn07.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/{0kbfx4p.g9wnr.js → 0w.rtg9.m8dk-.js} +2 -2
  93. package/.next/standalone/.next/static/chunks/{0_tx_~f8pi3d7.js → 13jdpvk~s2da8.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/{turbopack-0uc5y~g6h.n7-.js → turbopack-0r26pc8h0y_-e.js} +1 -1
  95. package/.next/standalone/CHANGELOG.md +74 -0
  96. package/.next/standalone/CLAUDE.md +14 -0
  97. package/.next/standalone/README.md +20 -3
  98. package/.next/standalone/bin/failproofai.mjs +5 -0
  99. package/.next/standalone/bun.lock +31 -63
  100. package/.next/standalone/dist/cli.mjs +242 -61
  101. package/.next/standalone/docs/built-in-policies.mdx +2 -2
  102. package/.next/standalone/docs/configuration.mdx +46 -0
  103. package/.next/standalone/docs/custom-policies.mdx +63 -5
  104. package/.next/standalone/docs/docs.json +3 -3
  105. package/.next/standalone/examples/convention-policies/security-policies.mjs +40 -0
  106. package/.next/standalone/examples/convention-policies/workflow-policies.mjs +41 -0
  107. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  108. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  109. package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/index.js +2 -2
  110. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  111. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  112. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
  113. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  114. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  115. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +7 -2
  116. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  117. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  118. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  119. package/.next/standalone/node_modules/next/dist/server/render.js +20 -19
  120. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  121. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  122. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  123. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  124. package/.next/standalone/node_modules/next/package.json +15 -15
  125. package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
  126. package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
  127. package/.next/standalone/node_modules/react/package.json +1 -1
  128. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
  129. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
  130. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
  131. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
  132. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
  133. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
  134. package/.next/standalone/node_modules/react-dom/package.json +2 -2
  135. package/.next/standalone/package.json +1 -1
  136. package/.next/standalone/server.js +1 -1
  137. package/.next/standalone/src/hooks/builtin-policies.ts +70 -18
  138. package/.next/standalone/src/hooks/custom-hooks-loader.ts +158 -21
  139. package/.next/standalone/src/hooks/handler.ts +26 -6
  140. package/.next/standalone/src/hooks/hooks-config.ts +47 -2
  141. package/.next/standalone/src/hooks/llm-client.ts +2 -2
  142. package/.next/standalone/src/hooks/loader-utils.ts +4 -4
  143. package/.next/standalone/src/hooks/manager.ts +57 -14
  144. package/.next/standalone/src/hooks/policy-evaluator.ts +16 -2
  145. package/README.md +20 -3
  146. package/bin/failproofai.mjs +5 -0
  147. package/dist/cli.mjs +242 -61
  148. package/package.json +1 -1
  149. package/src/hooks/builtin-policies.ts +70 -18
  150. package/src/hooks/custom-hooks-loader.ts +158 -21
  151. package/src/hooks/handler.ts +26 -6
  152. package/src/hooks/hooks-config.ts +47 -2
  153. package/src/hooks/llm-client.ts +2 -2
  154. package/src/hooks/loader-utils.ts +4 -4
  155. package/src/hooks/manager.ts +57 -14
  156. package/src/hooks/policy-evaluator.ts +16 -2
  157. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_buildManifest.js +0 -0
  158. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_clientMiddlewareManifest.js +0 -0
  159. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_ssgManifest.js +0 -0
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;
@@ -1373,7 +1426,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1373
1426
  continue;
1374
1427
  }
1375
1428
  if (result.decision === "deny") {
1376
- const reason = result.reason ?? `Blocked by policy: ${policy.name}`;
1429
+ const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1377
1430
  hookLogInfo(`deny by "${policy.name}": ${reason}`);
1378
1431
  const displayTool = ctx.toolName ?? "unknown tool";
1379
1432
  if (eventType === "PreToolUse") {
@@ -1420,7 +1473,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1420
1473
  }
1421
1474
  if (result.decision === "instruct" && !instructPolicyName) {
1422
1475
  instructPolicyName = policy.name;
1423
- instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1476
+ instructReason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1424
1477
  hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1425
1478
  }
1426
1479
  if (result.decision === "allow" && result.reason) {
@@ -1533,10 +1586,9 @@ async function createEsmShim(distIndex, distUrl) {
1533
1586
  const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
1534
1587
  const shimCode = [
1535
1588
  `import _cjs from '${distUrl}';`,
1536
- `export const createApp = _cjs.createApp;`,
1537
- `export const getQueueCondition = _cjs.getQueueCondition;`,
1538
- `export const clearQueueCondition = _cjs.clearQueueCondition;`,
1539
1589
  `export const customPolicies = _cjs.customPolicies;`,
1590
+ `export const getCustomHooks = _cjs.getCustomHooks;`,
1591
+ `export const clearCustomHooks = _cjs.clearCustomHooks;`,
1540
1592
  `export const allow = _cjs.allow;`,
1541
1593
  `export const deny = _cjs.deny;`,
1542
1594
  `export const instruct = _cjs.instruct;`,
@@ -1616,20 +1668,21 @@ var init_loader_utils = __esm(() => {
1616
1668
  });
1617
1669
 
1618
1670
  // src/hooks/custom-hooks-loader.ts
1619
- import { resolve as resolve4, isAbsolute } from "node:path";
1620
- import { existsSync as existsSync3 } from "node:fs";
1671
+ import { resolve as resolve4, isAbsolute, basename } from "node:path";
1672
+ import { existsSync as existsSync3, readdirSync } from "node:fs";
1621
1673
  import { pathToFileURL as pathToFileURL2 } from "node:url";
1622
- async function loadCustomHooks(customPoliciesPath, opts) {
1623
- if (!customPoliciesPath)
1674
+ import { homedir as homedir4 } from "node:os";
1675
+ function discoverPolicyFiles(dir) {
1676
+ if (!existsSync3(dir))
1624
1677
  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}`);
1678
+ try {
1679
+ const entries = readdirSync(dir, { withFileTypes: true });
1680
+ 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));
1681
+ } catch {
1630
1682
  return [];
1631
1683
  }
1632
- clearCustomHooks();
1684
+ }
1685
+ async function loadSingleFile(absPath, opts) {
1633
1686
  const g = globalThis;
1634
1687
  g[LOADING_KEY] = true;
1635
1688
  let tmpFiles = [];
@@ -1645,18 +1698,87 @@ async function loadCustomHooks(customPoliciesPath, opts) {
1645
1698
  if (opts?.strict)
1646
1699
  throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
1647
1700
  hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
1648
- return [];
1649
1701
  } finally {
1650
1702
  g[LOADING_KEY] = false;
1651
1703
  await cleanupTmpFiles(tmpFiles);
1652
1704
  }
1705
+ }
1706
+ async function loadCustomHooks(customPoliciesPath, opts) {
1707
+ if (!customPoliciesPath)
1708
+ return [];
1709
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
1710
+ if (!existsSync3(absPath)) {
1711
+ if (opts?.strict)
1712
+ throw new Error(`Custom hooks file not found: ${absPath}`);
1713
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1714
+ return [];
1715
+ }
1716
+ clearCustomHooks();
1717
+ await loadSingleFile(absPath, opts);
1653
1718
  return getCustomHooks();
1654
1719
  }
1655
- var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
1720
+ async function loadAllCustomHooks(customPoliciesPath, opts) {
1721
+ clearCustomHooks();
1722
+ const conventionSources = [];
1723
+ if (customPoliciesPath) {
1724
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
1725
+ if (existsSync3(absPath)) {
1726
+ await loadSingleFile(absPath);
1727
+ } else {
1728
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1729
+ }
1730
+ }
1731
+ const hooksBeforeConvention = getCustomHooks().length;
1732
+ const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
1733
+ const projectFiles = discoverPolicyFiles(projectDir);
1734
+ for (const file of projectFiles) {
1735
+ const hooksBefore = getCustomHooks().length;
1736
+ await loadSingleFile(file);
1737
+ const newHooks = getCustomHooks().slice(hooksBefore);
1738
+ if (newHooks.length > 0) {
1739
+ conventionSources.push({
1740
+ scope: "project",
1741
+ file: basename(file),
1742
+ hookNames: newHooks.map((h) => h.name)
1743
+ });
1744
+ }
1745
+ }
1746
+ const userDir = resolve4(homedir4(), ".failproofai", "policies");
1747
+ const userFiles = discoverPolicyFiles(userDir);
1748
+ for (const file of userFiles) {
1749
+ const hooksBefore = getCustomHooks().length;
1750
+ await loadSingleFile(file);
1751
+ const newHooks = getCustomHooks().slice(hooksBefore);
1752
+ if (newHooks.length > 0) {
1753
+ conventionSources.push({
1754
+ scope: "user",
1755
+ file: basename(file),
1756
+ hookNames: newHooks.map((h) => h.name)
1757
+ });
1758
+ }
1759
+ }
1760
+ const allHooks = getCustomHooks();
1761
+ const conventionCount = allHooks.length - hooksBeforeConvention;
1762
+ if (projectFiles.length > 0 || userFiles.length > 0) {
1763
+ hookLogInfo(`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`);
1764
+ }
1765
+ const conventionHookRefs = new Set;
1766
+ for (const hook of allHooks.slice(hooksBeforeConvention)) {
1767
+ conventionHookRefs.add(hook);
1768
+ }
1769
+ for (const hook of allHooks) {
1770
+ if (conventionHookRefs.has(hook)) {
1771
+ hook.__conventionSource = true;
1772
+ }
1773
+ }
1774
+ return { hooks: allHooks, conventionSources };
1775
+ }
1776
+ var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE;
1656
1777
  var init_custom_hooks_loader = __esm(() => {
1657
1778
  init_hook_logger();
1658
1779
  init_custom_hooks_registry();
1659
1780
  init_loader_utils();
1781
+ CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
1660
1782
  });
1661
1783
 
1662
1784
  // src/hooks/hook-activity-store.ts
@@ -1665,14 +1787,14 @@ import {
1665
1787
  writeFileSync as writeFileSync2,
1666
1788
  appendFileSync as appendFileSync2,
1667
1789
  renameSync as renameSync2,
1668
- readdirSync,
1790
+ readdirSync as readdirSync2,
1669
1791
  mkdirSync as mkdirSync3,
1670
1792
  existsSync as existsSync4,
1671
1793
  statSync as statSync2,
1672
1794
  unlinkSync
1673
1795
  } from "node:fs";
1674
1796
  import { join as join3 } from "node:path";
1675
- import { homedir as homedir4 } from "node:os";
1797
+ import { homedir as homedir5 } from "node:os";
1676
1798
  function ensureDir() {
1677
1799
  if (!existsSync4(storeDir)) {
1678
1800
  mkdirSync3(storeDir, { recursive: true });
@@ -1777,12 +1899,12 @@ function updateStats(entry) {
1777
1899
  }
1778
1900
  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
1901
  var init_hook_activity_store = __esm(() => {
1780
- DEFAULT_STORE_DIR = join3(homedir4(), ".failproofai", "cache", "hook-activity");
1902
+ DEFAULT_STORE_DIR = join3(homedir5(), ".failproofai", "cache", "hook-activity");
1781
1903
  storeDir = DEFAULT_STORE_DIR;
1782
1904
  });
1783
1905
 
1784
1906
  // package.json
1785
- var version2 = "0.0.2-beta.6";
1907
+ var version2 = "0.0.2-beta.7";
1786
1908
  var init_package = () => {};
1787
1909
 
1788
1910
  // src/posthog-key.ts
@@ -1944,9 +2066,13 @@ async function handleHookEvent(eventType) {
1944
2066
  const config = readMergedHooksConfig(session.cwd);
1945
2067
  clearPolicies();
1946
2068
  registerBuiltinPolicies(config.enabledPolicies);
1947
- const customHooksList = await loadCustomHooks(config.customPoliciesPath);
2069
+ const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
2070
+ const customHooksList = loadResult.hooks;
2071
+ const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
1948
2072
  for (const hook of customHooksList) {
1949
2073
  const hookName = hook.name;
2074
+ const isConvention = hook.__conventionSource === true;
2075
+ const prefix = isConvention ? "convention" : "custom";
1950
2076
  const fn = async (ctx) => {
1951
2077
  try {
1952
2078
  const result2 = await Promise.race([
@@ -1957,16 +2083,17 @@ async function handleHookEvent(eventType) {
1957
2083
  } catch (err) {
1958
2084
  const msg = err instanceof Error ? err.message : String(err);
1959
2085
  const isTimeout = msg === "timeout";
1960
- hookLogWarn(`custom hook "${hookName}" failed: ${msg}`);
2086
+ hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
1961
2087
  trackHookEvent(getInstanceId(), "custom_hook_error", {
1962
2088
  hook_name: hookName,
1963
2089
  error_type: isTimeout ? "timeout" : "exception",
1964
- event_type: eventType
2090
+ event_type: eventType,
2091
+ is_convention_policy: isConvention
1965
2092
  });
1966
2093
  return { decision: "allow" };
1967
2094
  }
1968
2095
  };
1969
- registerPolicy(`custom/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
2096
+ registerPolicy(`${prefix}/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
1970
2097
  }
1971
2098
  if (customHooksList.length > 0) {
1972
2099
  trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
@@ -1975,7 +2102,16 @@ async function handleHookEvent(eventType) {
1975
2102
  event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
1976
2103
  });
1977
2104
  }
1978
- hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length}`);
2105
+ if (loadResult.conventionSources.length > 0) {
2106
+ trackHookEvent(getInstanceId(), "convention_policies_loaded", {
2107
+ event_type: eventType,
2108
+ project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
2109
+ user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
2110
+ convention_hook_count: conventionHookNames.size,
2111
+ convention_hook_names: [...conventionHookNames]
2112
+ });
2113
+ }
2114
+ hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
1979
2115
  const result = await evaluatePolicies(eventType, parsed, session, config);
1980
2116
  const durationMs = Math.round(performance.now() - startTime);
1981
2117
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
@@ -2007,7 +2143,8 @@ async function handleHookEvent(eventType) {
2007
2143
  if (result.decision === "deny" || result.decision === "instruct") {
2008
2144
  try {
2009
2145
  const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
2010
- const hasCustomParams = !isCustomHook && !!(result.policyName && config.policyParams?.[result.policyName]);
2146
+ const isConventionPolicy = result.policyName?.startsWith("convention/") ?? false;
2147
+ const hasCustomParams = !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
2011
2148
  const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
2012
2149
  const distinctId = getInstanceId();
2013
2150
  await trackHookEvent(distinctId, "hook_policy_triggered", {
@@ -2016,6 +2153,7 @@ async function handleHookEvent(eventType) {
2016
2153
  policy_name: result.policyName,
2017
2154
  decision: result.decision,
2018
2155
  is_custom_hook: isCustomHook,
2156
+ is_convention_policy: isConventionPolicy,
2019
2157
  has_custom_params: hasCustomParams,
2020
2158
  param_keys_overridden: paramKeysOverridden
2021
2159
  });
@@ -2348,13 +2486,13 @@ __export(exports_manager, {
2348
2486
  });
2349
2487
  import { execSync as execSync3 } from "node:child_process";
2350
2488
  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";
2489
+ import { resolve as resolve5, dirname as dirname3, basename as basename2 } from "node:path";
2490
+ import { homedir as homedir6, platform, arch, release, hostname } from "node:os";
2353
2491
  function getSettingsPath(scope, cwd) {
2354
2492
  const base = cwd ? resolve5(cwd) : process.cwd();
2355
2493
  switch (scope) {
2356
2494
  case "user":
2357
- return resolve5(homedir5(), ".claude", "settings.json");
2495
+ return resolve5(homedir6(), ".claude", "settings.json");
2358
2496
  case "project":
2359
2497
  return resolve5(base, ".claude", "settings.json");
2360
2498
  case "local":
@@ -2481,7 +2619,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2481
2619
  }
2482
2620
  }
2483
2621
  const binaryPath = resolveFailproofaiBinary();
2484
- const previousConfig = readHooksConfig();
2622
+ const previousConfig = readScopedHooksConfig(scope, cwd);
2485
2623
  const previousEnabled = new Set(previousConfig.enabledPolicies);
2486
2624
  let selectedPolicies;
2487
2625
  if (policyNames !== undefined) {
@@ -2515,7 +2653,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2515
2653
  console.log(`
2516
2654
  Validated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`);
2517
2655
  }
2518
- writeHooksConfig(configToWrite);
2656
+ writeScopedHooksConfig(configToWrite, scope, cwd);
2519
2657
  console.log(`
2520
2658
  Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`);
2521
2659
  if (removeCustomHooks) {
@@ -2592,15 +2730,16 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
2592
2730
  }
2593
2731
  }
2594
2732
  async function removeHooks(policyNames, scope = "user", cwd, opts) {
2733
+ const configScope = scope === "all" ? "user" : scope;
2595
2734
  if (opts?.removeCustomHooks) {
2596
- const config = readHooksConfig();
2735
+ const config = readScopedHooksConfig(configScope, cwd);
2597
2736
  delete config.customPoliciesPath;
2598
- writeHooksConfig(config);
2737
+ writeScopedHooksConfig(config, configScope, cwd);
2599
2738
  console.log("Custom hooks path cleared.");
2600
2739
  }
2601
2740
  if (policyNames && policyNames.length > 0 && !(policyNames.length === 1 && policyNames[0] === "all")) {
2602
2741
  validatePolicyNames(policyNames);
2603
- const config = readHooksConfig();
2742
+ const config = readScopedHooksConfig(configScope, cwd);
2604
2743
  const removeSet = new Set(policyNames);
2605
2744
  const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
2606
2745
  const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
@@ -2614,7 +2753,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2614
2753
  enabledPolicies: remaining,
2615
2754
  ...filteredParams && Object.keys(filteredParams).length > 0 ? { policyParams: filteredParams } : {}
2616
2755
  };
2617
- writeHooksConfig(updatedConfig);
2756
+ writeScopedHooksConfig(updatedConfig, configScope, cwd);
2618
2757
  try {
2619
2758
  const distinctId = getInstanceId();
2620
2759
  const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
@@ -2635,7 +2774,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2635
2774
  console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
2636
2775
  return;
2637
2776
  }
2638
- const configBeforeRemoval = readHooksConfig();
2777
+ const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
2639
2778
  const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
2640
2779
  let totalRemoved = 0;
2641
2780
  for (const s of scopesToRemove) {
@@ -2682,10 +2821,18 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2682
2821
  hostname_hash: hashToId(hostname())
2683
2822
  });
2684
2823
  } 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: [] });
2824
+ if (scope === "all") {
2825
+ for (const s of HOOK_SCOPES) {
2826
+ const existing = readScopedHooksConfig(s, cwd);
2827
+ if (existing.enabledPolicies.length > 0 || existing.customPoliciesPath || existing.policyParams) {
2828
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
2829
+ writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, s, cwd);
2830
+ }
2831
+ }
2832
+ } else if (!HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
2833
+ const existing = readScopedHooksConfig(configScope, cwd);
2834
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
2835
+ writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
2689
2836
  }
2690
2837
  }
2691
2838
  async function listHooks(cwd) {
@@ -2822,6 +2969,35 @@ Failproof AI Hook Policies
2822
2969
  }
2823
2970
  console.log();
2824
2971
  }
2972
+ const base = cwd ? resolve5(cwd) : process.cwd();
2973
+ const conventionDirs = [
2974
+ { label: "Project", dir: resolve5(base, ".failproofai", "policies") },
2975
+ { label: "User", dir: resolve5(homedir6(), ".failproofai", "policies") }
2976
+ ];
2977
+ for (const { label, dir } of conventionDirs) {
2978
+ const files = discoverPolicyFiles(dir);
2979
+ if (files.length === 0)
2980
+ continue;
2981
+ console.log(`
2982
+ ── Convention Policies — ${label} (${dir}) ──────────`);
2983
+ for (const file of files) {
2984
+ try {
2985
+ const hooks = await loadCustomHooks(file);
2986
+ if (hooks.length === 0) {
2987
+ const filename = basename2(file);
2988
+ console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
2989
+ } else {
2990
+ const filename = basename2(file);
2991
+ const hookSummary = hooks.map((h) => h.name).join(", ");
2992
+ console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
2993
+ }
2994
+ } catch {
2995
+ const filename = basename2(file);
2996
+ console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
2997
+ }
2998
+ }
2999
+ console.log();
3000
+ }
2825
3001
  }
2826
3002
  var VALID_POLICY_NAMES;
2827
3003
  var init_manager = __esm(() => {
@@ -2837,10 +3013,10 @@ var init_manager = __esm(() => {
2837
3013
  });
2838
3014
 
2839
3015
  // lib/paths.ts
2840
- import { homedir as homedir6 } from "os";
3016
+ import { homedir as homedir7 } from "os";
2841
3017
  import { join as join4 } from "path";
2842
3018
  function getDefaultClaudeProjectsPath() {
2843
- return join4(homedir6(), ".claude", "projects");
3019
+ return join4(homedir7(), ".claude", "projects");
2844
3020
  }
2845
3021
  var init_paths = () => {};
2846
3022
 
@@ -3009,7 +3185,7 @@ import { realpathSync as realpathSync2 } from "fs";
3009
3185
  import { dirname as dirname5, resolve as resolve8 } from "path";
3010
3186
  import { fileURLToPath as fileURLToPath2 } from "url";
3011
3187
  // package.json
3012
- var version = "0.0.2-beta.6";
3188
+ var version = "0.0.2-beta.7";
3013
3189
 
3014
3190
  // bin/failproofai.mjs
3015
3191
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -3073,6 +3249,11 @@ COMMANDS
3073
3249
  --version, -v Print version and exit
3074
3250
  --help, -h Show this help message
3075
3251
 
3252
+ CONVENTION POLICIES
3253
+ Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
3254
+ Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
3255
+ No --custom flag or config changes needed \u2014 just drop files and they're picked up.
3256
+
3076
3257
  EXAMPLES
3077
3258
  failproofai policies
3078
3259
  failproofai policies --install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.2-beta.6",
3
+ "version": "0.0.2-beta.7",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -168,6 +168,50 @@ function getCurrentBranch(cwd: string): string | null {
168
168
  }
169
169
  }
170
170
 
171
+ function getHeadSha(cwd: string): string | null {
172
+ try {
173
+ const sha = execSync("git rev-parse HEAD", {
174
+ cwd,
175
+ encoding: "utf8",
176
+ timeout: 3000,
177
+ }).trim();
178
+ return sha || null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ interface CiCheck {
185
+ name: string;
186
+ status: string;
187
+ conclusion: string;
188
+ }
189
+
190
+ /** Fetch third-party check runs (non-GitHub-Actions) for a commit via the Checks API. */
191
+ function getThirdPartyCheckRuns(cwd: string, sha: string): CiCheck[] {
192
+ try {
193
+ const json = execFileSync(
194
+ "gh",
195
+ [
196
+ "api",
197
+ `repos/{owner}/{repo}/commits/${sha}/check-runs`,
198
+ "--jq",
199
+ '.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})',
200
+ ],
201
+ {
202
+ cwd,
203
+ encoding: "utf8",
204
+ timeout: 15000,
205
+ },
206
+ ).trim();
207
+
208
+ if (!json || json === "[]") return [];
209
+ return JSON.parse(json) as CiCheck[];
210
+ } catch {
211
+ return [];
212
+ }
213
+ }
214
+
171
215
  /**
172
216
  * Check if a command matches an allow pattern using token-by-token comparison.
173
217
  * The "*" token is a wildcard. Extra command tokens beyond the pattern are allowed,
@@ -1013,27 +1057,35 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1013
1057
  const branch = getCurrentBranch(cwd);
1014
1058
  if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping CI check.");
1015
1059
 
1016
- const runsJson = execFileSync(
1017
- "gh",
1018
- ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
1019
- {
1020
- cwd,
1021
- encoding: "utf8",
1022
- timeout: 15000,
1023
- },
1024
- ).trim();
1060
+ // 1. GitHub Actions workflow runs
1061
+ let workflowRuns: CiCheck[] = [];
1062
+ try {
1063
+ const runsJson = execFileSync(
1064
+ "gh",
1065
+ ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
1066
+ { cwd, encoding: "utf8", timeout: 15000 },
1067
+ ).trim();
1025
1068
 
1026
- if (!runsJson || runsJson === "[]") return allow(`No CI runs found for branch "${branch}".`);
1069
+ if (runsJson && runsJson !== "[]") {
1070
+ workflowRuns = JSON.parse(runsJson) as CiCheck[];
1071
+ }
1072
+ } catch {
1073
+ // fail-open for workflow runs; continue to check third-party checks
1074
+ }
1075
+
1076
+ // 2. Third-party check runs (CodeRabbit, SonarCloud, Codecov, etc.)
1077
+ let thirdPartyChecks: CiCheck[] = [];
1078
+ const sha = getHeadSha(cwd);
1079
+ if (sha) {
1080
+ thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
1081
+ }
1027
1082
 
1028
- const runs = JSON.parse(runsJson) as Array<{
1029
- status: string;
1030
- conclusion: string;
1031
- name: string;
1032
- }>;
1083
+ // 3. Merge all checks
1084
+ const allChecks = [...workflowRuns, ...thirdPartyChecks];
1033
1085
 
1034
- if (runs.length === 0) return allow(`No CI runs found for branch "${branch}".`);
1086
+ if (allChecks.length === 0) return allow(`No CI runs found for branch "${branch}".`);
1035
1087
 
1036
- const failing = runs.filter(
1088
+ const failing = allChecks.filter(
1037
1089
  (r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped",
1038
1090
  );
1039
1091
  if (failing.length > 0) {
@@ -1043,7 +1095,7 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1043
1095
  );
1044
1096
  }
1045
1097
 
1046
- const pending = runs.filter(
1098
+ const pending = allChecks.filter(
1047
1099
  (r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting",
1048
1100
  );
1049
1101
  if (pending.length > 0) {