failproofai 0.0.2-beta.2 → 0.0.2-beta.4

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 (130) 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/server/app/_global-error/page/server-reference-manifest.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  18. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  19. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  20. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  21. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/index.html +1 -1
  26. package/.next/standalone/.next/server/app/index.rsc +16 -16
  27. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  29. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  30. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  31. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  33. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  36. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  39. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  42. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  46. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  49. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0103jwf._.js → [root-of-the-server]__05ib_c3._.js} +2 -2
  51. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ovwjau._.js → [root-of-the-server]__0od~yp1._.js} +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +5 -5
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  63. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  64. package/.next/standalone/.next/server/pages/404.html +2 -2
  65. package/.next/standalone/.next/server/pages/500.html +1 -1
  66. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  68. package/.next/standalone/.next/static/chunks/{0sm1iqi3m~xiz.js → 03qghe4e2_.ul.js} +1 -1
  69. package/.next/standalone/.next/static/chunks/{0jrzwsyo7wo26.js → 0_eej2~ju.yds.js} +1 -1
  70. package/.next/standalone/.next/static/chunks/{0tl2f-3yc.rqc.js → 0bc69j4t8njpq.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/0nnxt7uoz_cvj.css +1 -0
  72. package/.next/standalone/.next/static/chunks/{0uftmw5od9kdz.js → 0qaojcc.nvqd8.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{001k0zayn2o.s.js → 0xg4wy053mmhs.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0pdd7~yp8ytu6.js → 12mgwr8gh_kqo.js} +2 -2
  75. package/.next/standalone/.next/static/chunks/{0tbr0o7vwc~-s.js → 134~t05vpu75e.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{0wtcha31~i7rm.js → 17veghz_js0u3.js} +1 -1
  77. package/.next/standalone/Dockerfile.docs +12 -0
  78. package/.next/standalone/README.md +59 -46
  79. package/.next/standalone/app/components/session-hooks-panel.tsx +31 -6
  80. package/.next/standalone/app/policies/hooks-client.tsx +31 -6
  81. package/.next/standalone/dist/cli.mjs +234 -27
  82. package/.next/standalone/dist/index.js +2 -2
  83. package/.next/standalone/docs/{architecture.md → architecture.mdx} +40 -23
  84. package/.next/standalone/docs/{built-in-policies.md → built-in-policies.mdx} +134 -12
  85. package/.next/standalone/docs/cli/dashboard.mdx +28 -0
  86. package/.next/standalone/docs/cli/environment-variables.mdx +34 -0
  87. package/.next/standalone/docs/cli/hook.mdx +30 -0
  88. package/.next/standalone/docs/cli/install-policies.mdx +48 -0
  89. package/.next/standalone/docs/cli/list-policies.mdx +31 -0
  90. package/.next/standalone/docs/cli/remove-policies.mdx +44 -0
  91. package/.next/standalone/docs/cli/version.mdx +12 -0
  92. package/.next/standalone/docs/{configuration.md → configuration.mdx} +16 -16
  93. package/.next/standalone/docs/{custom-hooks.md → custom-policies.mdx} +80 -42
  94. package/.next/standalone/docs/{dashboard.md → dashboard.mdx} +26 -29
  95. package/.next/standalone/docs/docs.json +31 -4
  96. package/.next/standalone/docs/examples.mdx +253 -0
  97. package/.next/standalone/docs/for-agents.mdx +38 -0
  98. package/.next/standalone/docs/getting-started.mdx +134 -0
  99. package/.next/standalone/docs/introduction.mdx +57 -0
  100. package/.next/standalone/docs/logo/dark.svg +21 -0
  101. package/.next/standalone/docs/logo/light.svg +21 -0
  102. package/.next/standalone/docs/{package-aliases.md → package-aliases.mdx} +5 -5
  103. package/.next/standalone/docs/{testing.md → testing.mdx} +11 -11
  104. package/.next/standalone/package.json +6 -9
  105. package/.next/standalone/scripts/publish-aliases.mjs +4 -2
  106. package/.next/standalone/skills-lock.json +10 -0
  107. package/.next/standalone/src/hooks/builtin-policies.ts +271 -25
  108. package/.next/standalone/src/hooks/handler.ts +1 -0
  109. package/.next/standalone/src/hooks/hook-activity-store.ts +6 -1
  110. package/.next/standalone/src/hooks/policy-evaluator.ts +23 -2
  111. package/.next/standalone/src/hooks/policy-helpers.ts +2 -2
  112. package/.next/standalone/vitest.config.e2e.mts +3 -0
  113. package/.next/standalone/vitest.config.mts +3 -0
  114. package/README.md +59 -46
  115. package/dist/cli.mjs +234 -27
  116. package/dist/index.js +2 -2
  117. package/package.json +6 -9
  118. package/scripts/publish-aliases.mjs +4 -2
  119. package/src/hooks/builtin-policies.ts +271 -25
  120. package/src/hooks/handler.ts +1 -0
  121. package/src/hooks/hook-activity-store.ts +6 -1
  122. package/src/hooks/policy-evaluator.ts +23 -2
  123. package/src/hooks/policy-helpers.ts +2 -2
  124. package/.next/standalone/.next/static/chunks/15jpradyu_531.css +0 -1
  125. package/.next/standalone/docs/cli-reference.md +0 -175
  126. package/.next/standalone/docs/getting-started.md +0 -128
  127. package/.next/standalone/docs/introduction.md +0 -47
  128. /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → N7CmoNJD3b7hE1pCaP_Gs}/_buildManifest.js +0 -0
  129. /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → N7CmoNJD3b7hE1pCaP_Gs}/_clientMiddlewareManifest.js +0 -0
  130. /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → N7CmoNJD3b7hE1pCaP_Gs}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -179,8 +179,8 @@ var init_hooks_config = __esm(() => {
179
179
  });
180
180
 
181
181
  // src/hooks/policy-helpers.ts
182
- function allow() {
183
- return { decision: "allow" };
182
+ function allow(reason) {
183
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
184
184
  }
185
185
  function deny(reason) {
186
186
  return { decision: "deny", reason };
@@ -248,7 +248,7 @@ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILP
248
248
  // src/hooks/builtin-policies.ts
249
249
  import { resolve as resolve2, join as join2 } from "node:path";
250
250
  import { readFile, writeFile } from "node:fs/promises";
251
- import { execSync } from "node:child_process";
251
+ import { execSync, execFileSync } from "node:child_process";
252
252
  import { homedir as homedir3 } from "node:os";
253
253
  function isClaudeInternalPath(resolved2) {
254
254
  const claudeDir = join2(homedir3(), ".claude");
@@ -266,6 +266,22 @@ function getFilePath(ctx) {
266
266
  function parseArgvTokens(cmd) {
267
267
  return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
268
268
  }
269
+ function getCurrentBranch(cwd) {
270
+ try {
271
+ let branch = gitBranchCache.get(cwd);
272
+ if (branch === undefined) {
273
+ branch = execSync("git rev-parse --abbrev-ref HEAD", {
274
+ cwd,
275
+ encoding: "utf8",
276
+ timeout: 3000
277
+ }).trim();
278
+ gitBranchCache.set(cwd, branch);
279
+ }
280
+ return branch || null;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
269
285
  function matchesAllowedPattern(cmd, pattern) {
270
286
  const cmdTokens = parseArgvTokens(cmd);
271
287
  const patTokens = parseArgvTokens(pattern);
@@ -480,7 +496,8 @@ function rmTargetIsAllowed(cmd, allowPaths) {
480
496
  if (rmIdx < 0)
481
497
  continue;
482
498
  const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
483
- if (!/r/i.test(flagTokens.join("")))
499
+ const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
500
+ if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some((f) => /^--recursive$/i.test(f)))
484
501
  continue;
485
502
  const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
486
503
  for (const target of pathArgs) {
@@ -505,7 +522,10 @@ function blockRmRf(ctx) {
505
522
  if (ctx.toolName !== "Bash")
506
523
  return allow();
507
524
  const cmd = getCommand(ctx);
508
- const hasDestructivePath = /(?:\/\s*$|\/\*|~)/.test(cmd);
525
+ const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
526
+ const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
527
+ return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
528
+ });
509
529
  if (hasDestructivePath && (/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) || /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd))) {
510
530
  const allowPaths = ctx.params?.allowPaths ?? [];
511
531
  if (rmTargetIsAllowed(cmd, allowPaths))
@@ -515,7 +535,10 @@ function blockRmRf(ctx) {
515
535
  if (hasDestructivePath && /\brm\b/.test(cmd)) {
516
536
  const tokens = parseArgvTokens(cmd);
517
537
  const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
518
- if (/r/i.test(shortFlags) && /f/.test(shortFlags)) {
538
+ const longFlags = tokens.filter((t) => /^--/.test(t));
539
+ const hasRecursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
540
+ const hasForce = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
541
+ if (hasRecursive && hasForce) {
519
542
  const allowPaths = ctx.params?.allowPaths ?? [];
520
543
  if (rmTargetIsAllowed(cmd, allowPaths))
521
544
  return allow();
@@ -653,22 +676,12 @@ function blockWorkOnMain(ctx) {
653
676
  const cwd = ctx.session?.cwd;
654
677
  if (!cwd)
655
678
  return allow();
656
- try {
657
- let branch = gitBranchCache.get(cwd);
658
- if (branch === undefined) {
659
- branch = execSync("git rev-parse --abbrev-ref HEAD", {
660
- cwd,
661
- encoding: "utf8",
662
- timeout: 3000
663
- }).trim();
664
- gitBranchCache.set(cwd, branch);
665
- }
666
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
667
- if (protectedBranches.includes(branch)) {
668
- return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
669
- }
670
- } catch {
679
+ const branch = getCurrentBranch(cwd);
680
+ if (!branch)
671
681
  return allow();
682
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
683
+ if (protectedBranches.includes(branch)) {
684
+ return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
672
685
  }
673
686
  return allow();
674
687
  }
@@ -767,6 +780,137 @@ function warnBackgroundProcess(ctx) {
767
780
  }
768
781
  return allow();
769
782
  }
783
+ function requireCommitBeforeStop(ctx) {
784
+ const cwd = ctx.session?.cwd;
785
+ if (!cwd)
786
+ return allow("No working directory available, skipping commit check.");
787
+ try {
788
+ const status = execSync("git status --porcelain", {
789
+ cwd,
790
+ encoding: "utf8",
791
+ timeout: 5000
792
+ }).trim();
793
+ if (status.length > 0) {
794
+ return deny("You have uncommitted changes in the working directory. Commit all changes before stopping.");
795
+ }
796
+ return allow("All changes are committed.");
797
+ } catch {
798
+ return allow("Not a git repository, skipping commit check.");
799
+ }
800
+ }
801
+ function requirePushBeforeStop(ctx) {
802
+ const cwd = ctx.session?.cwd;
803
+ if (!cwd)
804
+ return allow("No working directory available, skipping push check.");
805
+ try {
806
+ const remotes = execSync("git remote", {
807
+ cwd,
808
+ encoding: "utf8",
809
+ timeout: 3000
810
+ }).trim();
811
+ if (!remotes)
812
+ return allow("No git remote configured, skipping push check.");
813
+ const remote = ctx.params?.remote ?? "origin";
814
+ const branch = getCurrentBranch(cwd);
815
+ if (!branch || branch === "HEAD")
816
+ return allow("Detached HEAD, skipping push check.");
817
+ let hasTracking = false;
818
+ try {
819
+ execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
820
+ cwd,
821
+ encoding: "utf8",
822
+ timeout: 3000
823
+ });
824
+ hasTracking = true;
825
+ } catch {}
826
+ if (!hasTracking) {
827
+ return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Push your branch with: git push -u ${remote} ${branch}`);
828
+ }
829
+ const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
830
+ cwd,
831
+ encoding: "utf8",
832
+ timeout: 5000
833
+ }).trim();
834
+ if (unpushed.length > 0) {
835
+ const commitCount = unpushed.split(`
836
+ `).length;
837
+ return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Push your changes with: git push`);
838
+ }
839
+ return allow(`All commits pushed to "${remote}".`);
840
+ } catch {
841
+ return allow("Could not check push status, skipping.");
842
+ }
843
+ }
844
+ function requirePrBeforeStop(ctx) {
845
+ const cwd = ctx.session?.cwd;
846
+ if (!cwd)
847
+ return allow("No working directory available, skipping PR check.");
848
+ try {
849
+ try {
850
+ execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
851
+ } catch {
852
+ return allow("GitHub CLI (gh) not installed, skipping PR check.");
853
+ }
854
+ const branch = getCurrentBranch(cwd);
855
+ if (!branch || branch === "HEAD")
856
+ return allow("Detached HEAD, skipping PR check.");
857
+ let prJson;
858
+ try {
859
+ prJson = execSync("gh pr view --json number,url,state", {
860
+ cwd,
861
+ encoding: "utf8",
862
+ timeout: 15000
863
+ }).trim();
864
+ } catch {
865
+ return deny(`No pull request found for branch "${branch}". ` + `Create one with: gh pr create`);
866
+ }
867
+ const pr = JSON.parse(prJson);
868
+ if (pr.state === "OPEN") {
869
+ return allow(`PR #${pr.number} exists: ${pr.url}`);
870
+ }
871
+ return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Create a new PR with: gh pr create`);
872
+ } catch {
873
+ return allow("Could not check PR status, skipping.");
874
+ }
875
+ }
876
+ function requireCiGreenBeforeStop(ctx) {
877
+ const cwd = ctx.session?.cwd;
878
+ if (!cwd)
879
+ return allow("No working directory available, skipping CI check.");
880
+ try {
881
+ try {
882
+ execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
883
+ } catch {
884
+ return allow("GitHub CLI (gh) not installed, skipping CI check.");
885
+ }
886
+ const branch = getCurrentBranch(cwd);
887
+ if (!branch || branch === "HEAD")
888
+ return allow("Detached HEAD, skipping CI check.");
889
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], {
890
+ cwd,
891
+ encoding: "utf8",
892
+ timeout: 15000
893
+ }).trim();
894
+ if (!runsJson || runsJson === "[]")
895
+ return allow(`No CI runs found for branch "${branch}".`);
896
+ const runs = JSON.parse(runsJson);
897
+ if (runs.length === 0)
898
+ return allow(`No CI runs found for branch "${branch}".`);
899
+ const failing = runs.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
900
+ if (failing.length > 0) {
901
+ const names = failing.map((r) => `"${r.name}"`).join(", ");
902
+ return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
903
+ }
904
+ const pending = runs.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
905
+ if (pending.length > 0) {
906
+ const names = pending.map((r) => `"${r.name}"`).join(", ");
907
+ return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
908
+ }
909
+ return allow(`All CI checks passed on branch "${branch}".`);
910
+ } catch {
911
+ return allow("Could not check CI status, skipping.");
912
+ }
913
+ }
770
914
  function registerBuiltinPolicies(enabledNames) {
771
915
  const enabledSet = new Set(enabledNames);
772
916
  for (const policy of BUILTIN_POLICIES) {
@@ -802,7 +946,7 @@ var init_builtin_policies = __esm(() => {
802
946
  SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
803
947
  PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
804
948
  ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
805
- ECHO_ENV_RE = /echo\s+.*\$[A-Za-z_]/;
949
+ ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
806
950
  EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
807
951
  PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
808
952
  PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
@@ -813,7 +957,7 @@ var init_builtin_policies = __esm(() => {
813
957
  SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
814
958
  PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
815
959
  RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
816
- CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh)/;
960
+ CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
817
961
  PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
818
962
  FORCE_PUSH_RE = /(?:--force|-f\b)/;
819
963
  SECRET_FILE_RE = /\.(?:pem|key)$/;
@@ -1102,6 +1246,49 @@ var init_builtin_policies = __esm(() => {
1102
1246
  match: { events: ["PreToolUse"] },
1103
1247
  defaultEnabled: false,
1104
1248
  category: "AI Behavior"
1249
+ },
1250
+ {
1251
+ name: "require-commit-before-stop",
1252
+ description: "Require all changes to be committed before Claude stops",
1253
+ fn: requireCommitBeforeStop,
1254
+ match: { events: ["Stop"] },
1255
+ defaultEnabled: false,
1256
+ category: "Workflow",
1257
+ beta: true
1258
+ },
1259
+ {
1260
+ name: "require-push-before-stop",
1261
+ description: "Require all commits to be pushed to remote before Claude stops",
1262
+ fn: requirePushBeforeStop,
1263
+ match: { events: ["Stop"] },
1264
+ defaultEnabled: false,
1265
+ category: "Workflow",
1266
+ beta: true,
1267
+ params: {
1268
+ remote: {
1269
+ type: "string",
1270
+ description: "Remote name to push to (default: origin)",
1271
+ default: "origin"
1272
+ }
1273
+ }
1274
+ },
1275
+ {
1276
+ name: "require-pr-before-stop",
1277
+ description: "Require a pull request to exist for the current branch before Claude stops",
1278
+ fn: requirePrBeforeStop,
1279
+ match: { events: ["Stop"] },
1280
+ defaultEnabled: false,
1281
+ category: "Workflow",
1282
+ beta: true
1283
+ },
1284
+ {
1285
+ name: "require-ci-green-before-stop",
1286
+ description: "Require CI checks to pass on the current branch before Claude stops",
1287
+ fn: requireCiGreenBeforeStop,
1288
+ match: { events: ["Stop"] },
1289
+ defaultEnabled: false,
1290
+ category: "Workflow",
1291
+ beta: true
1105
1292
  }
1106
1293
  ];
1107
1294
  });
@@ -1124,6 +1311,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1124
1311
  };
1125
1312
  let instructPolicyName = null;
1126
1313
  let instructReason = null;
1314
+ const allowEntries = [];
1127
1315
  for (const policy of policies) {
1128
1316
  const schema = POLICY_PARAMS_MAP.get(policy.name);
1129
1317
  let ctx;
@@ -1184,7 +1372,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1184
1372
  return {
1185
1373
  exitCode: 2,
1186
1374
  stdout: "",
1187
- stderr: "",
1375
+ stderr: reason,
1188
1376
  policyName: policy.name,
1189
1377
  reason,
1190
1378
  decision: "deny"
@@ -1195,6 +1383,9 @@ async function evaluatePolicies(eventType, payload, session, config) {
1195
1383
  instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1196
1384
  hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1197
1385
  }
1386
+ if (result.decision === "allow" && result.reason) {
1387
+ allowEntries.push({ policyName: policy.name, reason: result.reason });
1388
+ }
1198
1389
  }
1199
1390
  if (instructPolicyName && instructReason) {
1200
1391
  if (eventType === "Stop") {
@@ -1222,6 +1413,17 @@ async function evaluatePolicies(eventType, payload, session, config) {
1222
1413
  decision: "instruct"
1223
1414
  };
1224
1415
  }
1416
+ if (allowEntries.length > 0) {
1417
+ const combined = allowEntries.map((e) => e.reason).join(`
1418
+ `);
1419
+ const policyNames = allowEntries.map((e) => e.policyName);
1420
+ const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
1421
+ const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
1422
+ const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
1423
+ `);
1424
+ return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + `
1425
+ `, policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
1426
+ }
1225
1427
  return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
1226
1428
  }
1227
1429
  var POLICY_PARAMS_MAP;
@@ -1516,7 +1718,11 @@ function updateStats(entry) {
1516
1718
  s.totalEvents += 1;
1517
1719
  if (entry.decision === "deny")
1518
1720
  s.denyCount += 1;
1519
- if (entry.policyName) {
1721
+ if (entry.policyNames && entry.policyNames.length > 0) {
1722
+ for (const name of entry.policyNames) {
1723
+ s.policyMap[name] = (s.policyMap[name] ?? 0) + 1;
1724
+ }
1725
+ } else if (entry.policyName) {
1520
1726
  s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
1521
1727
  }
1522
1728
  const tmpPath = join3(storeDir, `stats.json.${process.pid}.tmp`);
@@ -1536,7 +1742,7 @@ var init_hook_activity_store = __esm(() => {
1536
1742
  });
1537
1743
 
1538
1744
  // package.json
1539
- var version2 = "0.0.2-beta.2";
1745
+ var version2 = "0.0.2-beta.4";
1540
1746
  var init_package = () => {};
1541
1747
 
1542
1748
  // src/posthog-key.ts
@@ -1745,6 +1951,7 @@ async function handleHookEvent(eventType) {
1745
1951
  eventType,
1746
1952
  toolName: parsed.tool_name ?? null,
1747
1953
  policyName: result.policyName,
1954
+ policyNames: result.policyNames,
1748
1955
  decision: result.decision,
1749
1956
  reason: result.reason,
1750
1957
  durationMs,
@@ -2762,7 +2969,7 @@ import { realpathSync as realpathSync2 } from "fs";
2762
2969
  import { dirname as dirname5, resolve as resolve8 } from "path";
2763
2970
  import { fileURLToPath as fileURLToPath2 } from "url";
2764
2971
  // package.json
2765
- var version = "0.0.2-beta.2";
2972
+ var version = "0.0.2-beta.4";
2766
2973
 
2767
2974
  // bin/failproofai.mjs
2768
2975
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
package/dist/index.js CHANGED
@@ -69,8 +69,8 @@ function clearCustomHooks() {
69
69
  g[REGISTRY_KEY] = [];
70
70
  }
71
71
  // src/hooks/policy-helpers.ts
72
- function allow() {
73
- return { decision: "allow" };
72
+ function allow(reason) {
73
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
74
74
  }
75
75
  function deny(reason) {
76
76
  return { decision: "deny", reason };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.2-beta.2",
4
- "description": "Open-source hooks, policies, and project visualization for Claude Code & Agents SDK",
3
+ "version": "0.0.2-beta.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"
7
7
  },
@@ -40,17 +40,14 @@
40
40
  "claude-agents-sdk",
41
41
  "anthropic",
42
42
  "ai-agent",
43
- "llm-observability",
44
- "agent-observability",
45
- "log-viewer",
46
- "session-replay",
43
+ "agent-reliability",
44
+ "agent-monitoring",
45
+ "autonomous-agent",
46
+ "failure-prevention",
47
47
  "developer-tools",
48
48
  "devtools",
49
49
  "cli",
50
50
  "local-first",
51
- "monitoring",
52
- "debugging",
53
- "tracing",
54
51
  "hooks",
55
52
  "policies"
56
53
  ],
@@ -8,6 +8,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const rootPkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
9
9
  const VERSION = rootPkg.version;
10
10
  const DRY_RUN = process.argv.includes('--dry-run');
11
+ const distTagIdx = process.argv.indexOf('--dist-tag');
12
+ const DIST_TAG = distTagIdx !== -1 ? process.argv[distTagIdx + 1] : (VERSION.includes('-') ? 'beta' : 'latest');
11
13
 
12
14
  const ALIASES = [
13
15
  // Formatting variants — no "ai", or hyphen/underscore separators
@@ -54,7 +56,7 @@ for (const name of ALIASES) {
54
56
  cpSync(join(__dirname, 'alias-proxy.js'), join(binDir, 'proxy.js'));
55
57
 
56
58
  if (DRY_RUN) {
57
- console.log(`[dry-run] Would publish ${name}@${VERSION}`);
59
+ console.log(`[dry-run] Would publish ${name}@${VERSION} (tag: ${DIST_TAG})`);
58
60
  console.log(JSON.stringify(pkg, null, 2));
59
61
  console.log('---');
60
62
  rmSync(tmpDir, { recursive: true, force: true });
@@ -63,7 +65,7 @@ for (const name of ALIASES) {
63
65
 
64
66
  console.log(`Publishing ${name}@${VERSION}...`);
65
67
  try {
66
- execSync('npm publish', { cwd: tmpDir, stdio: 'pipe' });
68
+ execSync(`npm publish --tag ${DIST_TAG}`, { cwd: tmpDir, stdio: 'pipe' });
67
69
  console.log(`Done: ${name}`);
68
70
  } catch (err) {
69
71
  const output = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');