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.
- 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]__0u_n1xe._.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]__0epc5zr._.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/{0tood0~87-mm8.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/{0jqg886bw85_6.js → 0bkizbynk9via.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17p200_z1ivz4.js → 0e76l4~hq_sei.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0wkzaq-8sxss7.js → 0ltx5i0xv85_s.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0efsuf1p-k4qe.js → 0q7atesxo-36k.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0rvepm.~uvks4.js → 0suauczjqzn07.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0kbfx4p.g9wnr.js → 0w.rtg9.m8dk-.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0_tx_~f8pi3d7.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 +242 -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 +70 -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 +242 -61
- package/package.json +1 -1
- package/src/hooks/builtin-policies.ts +70 -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/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → 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);
|
|
@@ -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
|
-
|
|
918
|
-
|
|
919
|
-
encoding: "utf8",
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1623
|
-
|
|
1674
|
+
import { homedir as homedir4 } from "node:os";
|
|
1675
|
+
function discoverPolicyFiles(dir) {
|
|
1676
|
+
if (!existsSync3(dir))
|
|
1624
1677
|
return [];
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2735
|
+
const config = readScopedHooksConfig(configScope, cwd);
|
|
2597
2736
|
delete config.customPoliciesPath;
|
|
2598
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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"
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
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
|
|
3016
|
+
import { homedir as homedir7 } from "os";
|
|
2841
3017
|
import { join as join4 } from "path";
|
|
2842
3018
|
function getDefaultClaudeProjectsPath() {
|
|
2843
|
-
return join4(
|
|
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.
|
|
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
|
|
@@ -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)
|