failproofai 0.0.2-beta.5 → 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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00_.atk._.js → [root-of-the-server]__05zi2mt._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0gw4qdj._.js → [root-of-the-server]__0kkt_9z._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +8 -9
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0a_7sdg.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0j79~gv.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0pbja1x.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0r6o0i2.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11y81~_.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12or2kf.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{0issdwvmb81z_.js → 02u4v.k5amfah.js} +1 -1
- package/.next/standalone/.next/static/chunks/{031pa5~qfzt~_.js → 09e7drilkf1sn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{14ee68i9dy9b3.js → 0bkizbynk9via.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0gleuaabeolm~.js → 0e76l4~hq_sei.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0odv81fzkn6u~.js → 0ltx5i0xv85_s.js} +1 -1
- package/.next/standalone/.next/static/chunks/{040il49xqyq~j.js → 0q7atesxo-36k.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ezymnwrt2x6i.js → 0suauczjqzn07.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d-hv1uc827s6.js → 0w.rtg9.m8dk-.js} +2 -2
- package/.next/standalone/.next/static/chunks/{10uhv8kh~ad6m.js → 13jdpvk~s2da8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-0uc5y~g6h.n7-.js → turbopack-0r26pc8h0y_-e.js} +1 -1
- package/.next/standalone/CHANGELOG.md +74 -0
- package/.next/standalone/CLAUDE.md +14 -0
- package/.next/standalone/README.md +20 -3
- package/.next/standalone/bin/failproofai.mjs +5 -0
- package/.next/standalone/bun.lock +31 -63
- package/.next/standalone/dist/cli.mjs +261 -61
- package/.next/standalone/docs/built-in-policies.mdx +2 -2
- package/.next/standalone/docs/configuration.mdx +46 -0
- package/.next/standalone/docs/custom-policies.mdx +63 -5
- package/.next/standalone/docs/docs.json +3 -3
- package/.next/standalone/examples/convention-policies/security-policies.mjs +40 -0
- package/.next/standalone/examples/convention-policies/workflow-policies.mjs +41 -0
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/index.js +2 -2
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +7 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/render.js +20 -19
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +15 -15
- package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
- package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
- package/.next/standalone/node_modules/react/package.json +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/package.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/.next/standalone/src/hooks/builtin-policies.ts +110 -18
- package/.next/standalone/src/hooks/custom-hooks-loader.ts +158 -21
- package/.next/standalone/src/hooks/handler.ts +26 -6
- package/.next/standalone/src/hooks/hooks-config.ts +47 -2
- package/.next/standalone/src/hooks/llm-client.ts +2 -2
- package/.next/standalone/src/hooks/loader-utils.ts +4 -4
- package/.next/standalone/src/hooks/manager.ts +57 -14
- package/.next/standalone/src/hooks/policy-evaluator.ts +16 -2
- package/README.md +20 -3
- package/bin/failproofai.mjs +5 -0
- package/dist/cli.mjs +261 -61
- package/package.json +1 -1
- package/src/hooks/builtin-policies.ts +110 -18
- package/src/hooks/custom-hooks-loader.ts +158 -21
- package/src/hooks/handler.ts +26 -6
- package/src/hooks/hooks-config.ts +47 -2
- package/src/hooks/llm-client.ts +2 -2
- package/src/hooks/loader-utils.ts +4 -4
- package/src/hooks/manager.ts +57 -14
- package/src/hooks/policy-evaluator.ts +16 -2
- /package/.next/standalone/.next/static/{p7b7Yk0VOBDjbtr1aHDyV → Opbai6exOQP2W488FWmr6}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{p7b7Yk0VOBDjbtr1aHDyV → Opbai6exOQP2W488FWmr6}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{p7b7Yk0VOBDjbtr1aHDyV → Opbai6exOQP2W488FWmr6}/_ssgManifest.js +0 -0
|
@@ -149,11 +149,19 @@ function readMergedHooksConfig(cwd) {
|
|
|
149
149
|
...llm !== undefined ? { llm } : {}
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
-
function
|
|
153
|
-
|
|
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
|
|
156
|
-
const configPath =
|
|
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
|
|
169
|
-
const configPath =
|
|
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);
|
|
@@ -814,6 +853,20 @@ function requirePushBeforeStop(ctx) {
|
|
|
814
853
|
const branch = getCurrentBranch(cwd);
|
|
815
854
|
if (!branch || branch === "HEAD")
|
|
816
855
|
return allow("Detached HEAD, skipping push check.");
|
|
856
|
+
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
857
|
+
if (branch === baseBranch) {
|
|
858
|
+
return allow(`On base branch "${baseBranch}", skipping push check.`);
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
const ahead = execFileSync("git", ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
862
|
+
if (!ahead) {
|
|
863
|
+
return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
|
|
864
|
+
}
|
|
865
|
+
const diff = execFileSync("git", ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
866
|
+
if (!diff) {
|
|
867
|
+
return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
|
|
868
|
+
}
|
|
869
|
+
} catch {}
|
|
817
870
|
let hasTracking = false;
|
|
818
871
|
try {
|
|
819
872
|
execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
@@ -900,22 +953,27 @@ function requireCiGreenBeforeStop(ctx) {
|
|
|
900
953
|
const branch = getCurrentBranch(cwd);
|
|
901
954
|
if (!branch || branch === "HEAD")
|
|
902
955
|
return allow("Detached HEAD, skipping CI check.");
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
encoding: "utf8",
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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)
|
|
912
970
|
return allow(`No CI runs found for branch "${branch}".`);
|
|
913
|
-
const failing =
|
|
971
|
+
const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
|
|
914
972
|
if (failing.length > 0) {
|
|
915
973
|
const names = failing.map((r) => `"${r.name}"`).join(", ");
|
|
916
974
|
return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
|
|
917
975
|
}
|
|
918
|
-
const pending =
|
|
976
|
+
const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
|
|
919
977
|
if (pending.length > 0) {
|
|
920
978
|
const names = pending.map((r) => `"${r.name}"`).join(", ");
|
|
921
979
|
return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
|
|
@@ -1283,6 +1341,11 @@ var init_builtin_policies = __esm(() => {
|
|
|
1283
1341
|
type: "string",
|
|
1284
1342
|
description: "Remote name to push to (default: origin)",
|
|
1285
1343
|
default: "origin"
|
|
1344
|
+
},
|
|
1345
|
+
baseBranch: {
|
|
1346
|
+
type: "string",
|
|
1347
|
+
description: "Base branch to compare against (default: main)",
|
|
1348
|
+
default: "main"
|
|
1286
1349
|
}
|
|
1287
1350
|
}
|
|
1288
1351
|
},
|
|
@@ -1315,6 +1378,15 @@ var init_builtin_policies = __esm(() => {
|
|
|
1315
1378
|
});
|
|
1316
1379
|
|
|
1317
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
|
+
}
|
|
1318
1390
|
async function evaluatePolicies(eventType, payload, session, config) {
|
|
1319
1391
|
const toolName = payload.tool_name;
|
|
1320
1392
|
const toolInput = payload.tool_input;
|
|
@@ -1354,7 +1426,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1354
1426
|
continue;
|
|
1355
1427
|
}
|
|
1356
1428
|
if (result.decision === "deny") {
|
|
1357
|
-
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);
|
|
1358
1430
|
hookLogInfo(`deny by "${policy.name}": ${reason}`);
|
|
1359
1431
|
const displayTool = ctx.toolName ?? "unknown tool";
|
|
1360
1432
|
if (eventType === "PreToolUse") {
|
|
@@ -1401,7 +1473,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1401
1473
|
}
|
|
1402
1474
|
if (result.decision === "instruct" && !instructPolicyName) {
|
|
1403
1475
|
instructPolicyName = policy.name;
|
|
1404
|
-
instructReason = result.reason ?? `Instruction from policy: ${policy.name}
|
|
1476
|
+
instructReason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
|
|
1405
1477
|
hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
|
|
1406
1478
|
}
|
|
1407
1479
|
if (result.decision === "allow" && result.reason) {
|
|
@@ -1514,10 +1586,9 @@ async function createEsmShim(distIndex, distUrl) {
|
|
|
1514
1586
|
const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
|
|
1515
1587
|
const shimCode = [
|
|
1516
1588
|
`import _cjs from '${distUrl}';`,
|
|
1517
|
-
`export const createApp = _cjs.createApp;`,
|
|
1518
|
-
`export const getQueueCondition = _cjs.getQueueCondition;`,
|
|
1519
|
-
`export const clearQueueCondition = _cjs.clearQueueCondition;`,
|
|
1520
1589
|
`export const customPolicies = _cjs.customPolicies;`,
|
|
1590
|
+
`export const getCustomHooks = _cjs.getCustomHooks;`,
|
|
1591
|
+
`export const clearCustomHooks = _cjs.clearCustomHooks;`,
|
|
1521
1592
|
`export const allow = _cjs.allow;`,
|
|
1522
1593
|
`export const deny = _cjs.deny;`,
|
|
1523
1594
|
`export const instruct = _cjs.instruct;`,
|
|
@@ -1597,20 +1668,21 @@ var init_loader_utils = __esm(() => {
|
|
|
1597
1668
|
});
|
|
1598
1669
|
|
|
1599
1670
|
// src/hooks/custom-hooks-loader.ts
|
|
1600
|
-
import { resolve as resolve4, isAbsolute } from "node:path";
|
|
1601
|
-
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";
|
|
1602
1673
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1603
|
-
|
|
1604
|
-
|
|
1674
|
+
import { homedir as homedir4 } from "node:os";
|
|
1675
|
+
function discoverPolicyFiles(dir) {
|
|
1676
|
+
if (!existsSync3(dir))
|
|
1605
1677
|
return [];
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
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 {
|
|
1611
1682
|
return [];
|
|
1612
1683
|
}
|
|
1613
|
-
|
|
1684
|
+
}
|
|
1685
|
+
async function loadSingleFile(absPath, opts) {
|
|
1614
1686
|
const g = globalThis;
|
|
1615
1687
|
g[LOADING_KEY] = true;
|
|
1616
1688
|
let tmpFiles = [];
|
|
@@ -1626,18 +1698,87 @@ async function loadCustomHooks(customPoliciesPath, opts) {
|
|
|
1626
1698
|
if (opts?.strict)
|
|
1627
1699
|
throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
|
|
1628
1700
|
hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
|
|
1629
|
-
return [];
|
|
1630
1701
|
} finally {
|
|
1631
1702
|
g[LOADING_KEY] = false;
|
|
1632
1703
|
await cleanupTmpFiles(tmpFiles);
|
|
1633
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);
|
|
1634
1718
|
return getCustomHooks();
|
|
1635
1719
|
}
|
|
1636
|
-
|
|
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;
|
|
1637
1777
|
var init_custom_hooks_loader = __esm(() => {
|
|
1638
1778
|
init_hook_logger();
|
|
1639
1779
|
init_custom_hooks_registry();
|
|
1640
1780
|
init_loader_utils();
|
|
1781
|
+
CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
|
|
1641
1782
|
});
|
|
1642
1783
|
|
|
1643
1784
|
// src/hooks/hook-activity-store.ts
|
|
@@ -1646,14 +1787,14 @@ import {
|
|
|
1646
1787
|
writeFileSync as writeFileSync2,
|
|
1647
1788
|
appendFileSync as appendFileSync2,
|
|
1648
1789
|
renameSync as renameSync2,
|
|
1649
|
-
readdirSync,
|
|
1790
|
+
readdirSync as readdirSync2,
|
|
1650
1791
|
mkdirSync as mkdirSync3,
|
|
1651
1792
|
existsSync as existsSync4,
|
|
1652
1793
|
statSync as statSync2,
|
|
1653
1794
|
unlinkSync
|
|
1654
1795
|
} from "node:fs";
|
|
1655
1796
|
import { join as join3 } from "node:path";
|
|
1656
|
-
import { homedir as
|
|
1797
|
+
import { homedir as homedir5 } from "node:os";
|
|
1657
1798
|
function ensureDir() {
|
|
1658
1799
|
if (!existsSync4(storeDir)) {
|
|
1659
1800
|
mkdirSync3(storeDir, { recursive: true });
|
|
@@ -1758,12 +1899,12 @@ function updateStats(entry) {
|
|
|
1758
1899
|
}
|
|
1759
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;
|
|
1760
1901
|
var init_hook_activity_store = __esm(() => {
|
|
1761
|
-
DEFAULT_STORE_DIR = join3(
|
|
1902
|
+
DEFAULT_STORE_DIR = join3(homedir5(), ".failproofai", "cache", "hook-activity");
|
|
1762
1903
|
storeDir = DEFAULT_STORE_DIR;
|
|
1763
1904
|
});
|
|
1764
1905
|
|
|
1765
1906
|
// package.json
|
|
1766
|
-
var version2 = "0.0.2-beta.
|
|
1907
|
+
var version2 = "0.0.2-beta.7";
|
|
1767
1908
|
var init_package = () => {};
|
|
1768
1909
|
|
|
1769
1910
|
// src/posthog-key.ts
|
|
@@ -1925,9 +2066,13 @@ async function handleHookEvent(eventType) {
|
|
|
1925
2066
|
const config = readMergedHooksConfig(session.cwd);
|
|
1926
2067
|
clearPolicies();
|
|
1927
2068
|
registerBuiltinPolicies(config.enabledPolicies);
|
|
1928
|
-
const
|
|
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));
|
|
1929
2072
|
for (const hook of customHooksList) {
|
|
1930
2073
|
const hookName = hook.name;
|
|
2074
|
+
const isConvention = hook.__conventionSource === true;
|
|
2075
|
+
const prefix = isConvention ? "convention" : "custom";
|
|
1931
2076
|
const fn = async (ctx) => {
|
|
1932
2077
|
try {
|
|
1933
2078
|
const result2 = await Promise.race([
|
|
@@ -1938,16 +2083,17 @@ async function handleHookEvent(eventType) {
|
|
|
1938
2083
|
} catch (err) {
|
|
1939
2084
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1940
2085
|
const isTimeout = msg === "timeout";
|
|
1941
|
-
hookLogWarn(
|
|
2086
|
+
hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
|
|
1942
2087
|
trackHookEvent(getInstanceId(), "custom_hook_error", {
|
|
1943
2088
|
hook_name: hookName,
|
|
1944
2089
|
error_type: isTimeout ? "timeout" : "exception",
|
|
1945
|
-
event_type: eventType
|
|
2090
|
+
event_type: eventType,
|
|
2091
|
+
is_convention_policy: isConvention
|
|
1946
2092
|
});
|
|
1947
2093
|
return { decision: "allow" };
|
|
1948
2094
|
}
|
|
1949
2095
|
};
|
|
1950
|
-
registerPolicy(
|
|
2096
|
+
registerPolicy(`${prefix}/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
|
|
1951
2097
|
}
|
|
1952
2098
|
if (customHooksList.length > 0) {
|
|
1953
2099
|
trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
|
|
@@ -1956,7 +2102,16 @@ async function handleHookEvent(eventType) {
|
|
|
1956
2102
|
event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
|
|
1957
2103
|
});
|
|
1958
2104
|
}
|
|
1959
|
-
|
|
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}`);
|
|
1960
2115
|
const result = await evaluatePolicies(eventType, parsed, session, config);
|
|
1961
2116
|
const durationMs = Math.round(performance.now() - startTime);
|
|
1962
2117
|
hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
|
|
@@ -1988,7 +2143,8 @@ async function handleHookEvent(eventType) {
|
|
|
1988
2143
|
if (result.decision === "deny" || result.decision === "instruct") {
|
|
1989
2144
|
try {
|
|
1990
2145
|
const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
|
|
1991
|
-
const
|
|
2146
|
+
const isConventionPolicy = result.policyName?.startsWith("convention/") ?? false;
|
|
2147
|
+
const hasCustomParams = !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
|
|
1992
2148
|
const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
|
|
1993
2149
|
const distinctId = getInstanceId();
|
|
1994
2150
|
await trackHookEvent(distinctId, "hook_policy_triggered", {
|
|
@@ -1997,6 +2153,7 @@ async function handleHookEvent(eventType) {
|
|
|
1997
2153
|
policy_name: result.policyName,
|
|
1998
2154
|
decision: result.decision,
|
|
1999
2155
|
is_custom_hook: isCustomHook,
|
|
2156
|
+
is_convention_policy: isConventionPolicy,
|
|
2000
2157
|
has_custom_params: hasCustomParams,
|
|
2001
2158
|
param_keys_overridden: paramKeysOverridden
|
|
2002
2159
|
});
|
|
@@ -2329,13 +2486,13 @@ __export(exports_manager, {
|
|
|
2329
2486
|
});
|
|
2330
2487
|
import { execSync as execSync3 } from "node:child_process";
|
|
2331
2488
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
|
|
2332
|
-
import { resolve as resolve5, dirname as dirname3 } from "node:path";
|
|
2333
|
-
import { homedir as
|
|
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";
|
|
2334
2491
|
function getSettingsPath(scope, cwd) {
|
|
2335
2492
|
const base = cwd ? resolve5(cwd) : process.cwd();
|
|
2336
2493
|
switch (scope) {
|
|
2337
2494
|
case "user":
|
|
2338
|
-
return resolve5(
|
|
2495
|
+
return resolve5(homedir6(), ".claude", "settings.json");
|
|
2339
2496
|
case "project":
|
|
2340
2497
|
return resolve5(base, ".claude", "settings.json");
|
|
2341
2498
|
case "local":
|
|
@@ -2462,7 +2619,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
|
|
|
2462
2619
|
}
|
|
2463
2620
|
}
|
|
2464
2621
|
const binaryPath = resolveFailproofaiBinary();
|
|
2465
|
-
const previousConfig =
|
|
2622
|
+
const previousConfig = readScopedHooksConfig(scope, cwd);
|
|
2466
2623
|
const previousEnabled = new Set(previousConfig.enabledPolicies);
|
|
2467
2624
|
let selectedPolicies;
|
|
2468
2625
|
if (policyNames !== undefined) {
|
|
@@ -2496,7 +2653,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
|
|
|
2496
2653
|
console.log(`
|
|
2497
2654
|
Validated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`);
|
|
2498
2655
|
}
|
|
2499
|
-
|
|
2656
|
+
writeScopedHooksConfig(configToWrite, scope, cwd);
|
|
2500
2657
|
console.log(`
|
|
2501
2658
|
Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`);
|
|
2502
2659
|
if (removeCustomHooks) {
|
|
@@ -2573,15 +2730,16 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
|
|
|
2573
2730
|
}
|
|
2574
2731
|
}
|
|
2575
2732
|
async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
2733
|
+
const configScope = scope === "all" ? "user" : scope;
|
|
2576
2734
|
if (opts?.removeCustomHooks) {
|
|
2577
|
-
const config =
|
|
2735
|
+
const config = readScopedHooksConfig(configScope, cwd);
|
|
2578
2736
|
delete config.customPoliciesPath;
|
|
2579
|
-
|
|
2737
|
+
writeScopedHooksConfig(config, configScope, cwd);
|
|
2580
2738
|
console.log("Custom hooks path cleared.");
|
|
2581
2739
|
}
|
|
2582
2740
|
if (policyNames && policyNames.length > 0 && !(policyNames.length === 1 && policyNames[0] === "all")) {
|
|
2583
2741
|
validatePolicyNames(policyNames);
|
|
2584
|
-
const config =
|
|
2742
|
+
const config = readScopedHooksConfig(configScope, cwd);
|
|
2585
2743
|
const removeSet = new Set(policyNames);
|
|
2586
2744
|
const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
|
|
2587
2745
|
const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
|
|
@@ -2595,7 +2753,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
|
2595
2753
|
enabledPolicies: remaining,
|
|
2596
2754
|
...filteredParams && Object.keys(filteredParams).length > 0 ? { policyParams: filteredParams } : {}
|
|
2597
2755
|
};
|
|
2598
|
-
|
|
2756
|
+
writeScopedHooksConfig(updatedConfig, configScope, cwd);
|
|
2599
2757
|
try {
|
|
2600
2758
|
const distinctId = getInstanceId();
|
|
2601
2759
|
const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
|
|
@@ -2616,7 +2774,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
|
2616
2774
|
console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
|
|
2617
2775
|
return;
|
|
2618
2776
|
}
|
|
2619
|
-
const configBeforeRemoval =
|
|
2777
|
+
const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
|
|
2620
2778
|
const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
|
|
2621
2779
|
let totalRemoved = 0;
|
|
2622
2780
|
for (const s of scopesToRemove) {
|
|
@@ -2663,10 +2821,18 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
|
2663
2821
|
hostname_hash: hashToId(hostname())
|
|
2664
2822
|
});
|
|
2665
2823
|
} catch {}
|
|
2666
|
-
if (scope === "all"
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
|
|
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);
|
|
2670
2836
|
}
|
|
2671
2837
|
}
|
|
2672
2838
|
async function listHooks(cwd) {
|
|
@@ -2803,6 +2969,35 @@ Failproof AI Hook Policies
|
|
|
2803
2969
|
}
|
|
2804
2970
|
console.log();
|
|
2805
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
|
+
}
|
|
2806
3001
|
}
|
|
2807
3002
|
var VALID_POLICY_NAMES;
|
|
2808
3003
|
var init_manager = __esm(() => {
|
|
@@ -2818,10 +3013,10 @@ var init_manager = __esm(() => {
|
|
|
2818
3013
|
});
|
|
2819
3014
|
|
|
2820
3015
|
// lib/paths.ts
|
|
2821
|
-
import { homedir as
|
|
3016
|
+
import { homedir as homedir7 } from "os";
|
|
2822
3017
|
import { join as join4 } from "path";
|
|
2823
3018
|
function getDefaultClaudeProjectsPath() {
|
|
2824
|
-
return join4(
|
|
3019
|
+
return join4(homedir7(), ".claude", "projects");
|
|
2825
3020
|
}
|
|
2826
3021
|
var init_paths = () => {};
|
|
2827
3022
|
|
|
@@ -2990,7 +3185,7 @@ import { realpathSync as realpathSync2 } from "fs";
|
|
|
2990
3185
|
import { dirname as dirname5, resolve as resolve8 } from "path";
|
|
2991
3186
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2992
3187
|
// package.json
|
|
2993
|
-
var version = "0.0.2-beta.
|
|
3188
|
+
var version = "0.0.2-beta.7";
|
|
2994
3189
|
|
|
2995
3190
|
// bin/failproofai.mjs
|
|
2996
3191
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
|
@@ -3054,6 +3249,11 @@ COMMANDS
|
|
|
3054
3249
|
--version, -v Print version and exit
|
|
3055
3250
|
--help, -h Show this help message
|
|
3056
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
|
+
|
|
3057
3257
|
EXAMPLES
|
|
3058
3258
|
failproofai policies
|
|
3059
3259
|
failproofai policies --install
|
|
@@ -491,14 +491,14 @@ pull requests. If `gh` is not installed or not authenticated, the policy fails o
|
|
|
491
491
|
### `require-ci-green-before-stop`
|
|
492
492
|
|
|
493
493
|
**Event:** Stop
|
|
494
|
-
**Default:** Denies stopping when CI checks are failing or still running on the current branch. Treats `skipped` conclusions as success. Returns an informational message when all checks pass.
|
|
494
|
+
**Default:** Denies stopping when CI checks are failing or still running on the current branch. Checks both GitHub Actions workflow runs and third-party bot checks (e.g. CodeRabbit, SonarCloud, Codecov). Treats `skipped` conclusions as success. Returns an informational message when all checks pass.
|
|
495
495
|
|
|
496
496
|
No parameters.
|
|
497
497
|
|
|
498
498
|
<Note>
|
|
499
499
|
This policy requires [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated.
|
|
500
500
|
Run `gh auth login` with a personal access token that has `repo` scope for read access to
|
|
501
|
-
Actions workflow runs. If `gh` is not installed or not authenticated, the policy fails open and reports the reason to Claude.
|
|
501
|
+
Actions workflow runs and the Checks API. If `gh` is not installed or not authenticated, the policy fails open and reports the reason to Claude.
|
|
502
502
|
</Note>
|
|
503
503
|
|
|
504
504
|
---
|
|
@@ -116,6 +116,35 @@ If a policy has parameters but you don't specify them, the policy's built-in def
|
|
|
116
116
|
|
|
117
117
|
Unknown keys inside a policy's params block are silently ignored at hook-fire time but flagged as warnings when you run `failproofai policies`.
|
|
118
118
|
|
|
119
|
+
#### `hint` (cross-cutting)
|
|
120
|
+
|
|
121
|
+
Type: `string` (optional)
|
|
122
|
+
|
|
123
|
+
A message appended to the reason when a policy returns `deny` or `instruct`. Use it to give Claude actionable guidance without modifying the policy itself.
|
|
124
|
+
|
|
125
|
+
Works with any policy type — built-in, custom (`custom/`), or convention (`convention/`).
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"policyParams": {
|
|
130
|
+
"block-force-push": {
|
|
131
|
+
"hint": "Try creating a fresh branch instead."
|
|
132
|
+
},
|
|
133
|
+
"block-sudo": {
|
|
134
|
+
"allowPatterns": ["sudo apt-get"],
|
|
135
|
+
"hint": "Use apt-get directly without sudo."
|
|
136
|
+
},
|
|
137
|
+
"custom/my-policy": {
|
|
138
|
+
"hint": "Ask the user for approval first."
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When `block-force-push` denies, Claude sees: *"Force-pushing is blocked. Try creating a fresh branch instead."*
|
|
145
|
+
|
|
146
|
+
Non-string values and empty strings are silently ignored. If `hint` is not set, behavior is unchanged (backward-compatible).
|
|
147
|
+
|
|
119
148
|
### `customPoliciesPath`
|
|
120
149
|
|
|
121
150
|
Type: `string` (absolute path)
|
|
@@ -124,6 +153,23 @@ Path to a JavaScript file containing custom hook policies. This is set automatic
|
|
|
124
153
|
|
|
125
154
|
The file is loaded fresh on every hook event - there is no caching. See [Custom Policies](/custom-policies) for authoring details.
|
|
126
155
|
|
|
156
|
+
### Convention-based policies (v0.0.2-beta.7+)
|
|
157
|
+
|
|
158
|
+
In addition to the explicit `customPoliciesPath`, failproofai automatically discovers and loads policy files from `.failproofai/policies/` directories:
|
|
159
|
+
|
|
160
|
+
| Level | Directory | Scope |
|
|
161
|
+
|-------|-----------|-------|
|
|
162
|
+
| Project | `.failproofai/policies/` | Shared with team via version control |
|
|
163
|
+
| User | `~/.failproofai/policies/` | Personal, applies to all projects |
|
|
164
|
+
|
|
165
|
+
**File matching:** Only files matching `*policies.{js,mjs,ts}` are loaded (e.g. `security-policies.mjs`, `workflow-policies.js`). Other files in the directory are ignored.
|
|
166
|
+
|
|
167
|
+
**No config needed:** Convention policies require no entries in `policies-config.json`. Just drop files into the directory and they're picked up on the next hook event.
|
|
168
|
+
|
|
169
|
+
**Union loading:** Both project and user convention directories are scanned. All matching files from both levels are loaded (unlike `customPoliciesPath` which uses first-scope-wins).
|
|
170
|
+
|
|
171
|
+
See [Custom Policies](/custom-policies) for more details and examples.
|
|
172
|
+
|
|
127
173
|
### `llm`
|
|
128
174
|
|
|
129
175
|
Type: `object` (optional)
|