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
package/dist/cli.mjs
CHANGED
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
3
|
+
"version": "0.0.2-beta.7",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -168,6 +168,50 @@ function getCurrentBranch(cwd: string): string | null {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function getHeadSha(cwd: string): string | null {
|
|
172
|
+
try {
|
|
173
|
+
const sha = execSync("git rev-parse HEAD", {
|
|
174
|
+
cwd,
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
timeout: 3000,
|
|
177
|
+
}).trim();
|
|
178
|
+
return sha || null;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface CiCheck {
|
|
185
|
+
name: string;
|
|
186
|
+
status: string;
|
|
187
|
+
conclusion: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Fetch third-party check runs (non-GitHub-Actions) for a commit via the Checks API. */
|
|
191
|
+
function getThirdPartyCheckRuns(cwd: string, sha: string): CiCheck[] {
|
|
192
|
+
try {
|
|
193
|
+
const json = execFileSync(
|
|
194
|
+
"gh",
|
|
195
|
+
[
|
|
196
|
+
"api",
|
|
197
|
+
`repos/{owner}/{repo}/commits/${sha}/check-runs`,
|
|
198
|
+
"--jq",
|
|
199
|
+
'.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})',
|
|
200
|
+
],
|
|
201
|
+
{
|
|
202
|
+
cwd,
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
timeout: 15000,
|
|
205
|
+
},
|
|
206
|
+
).trim();
|
|
207
|
+
|
|
208
|
+
if (!json || json === "[]") return [];
|
|
209
|
+
return JSON.parse(json) as CiCheck[];
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
171
215
|
/**
|
|
172
216
|
* Check if a command matches an allow pattern using token-by-token comparison.
|
|
173
217
|
* The "*" token is a wildcard. Extra command tokens beyond the pattern are allowed,
|
|
@@ -1013,27 +1057,35 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1013
1057
|
const branch = getCurrentBranch(cwd);
|
|
1014
1058
|
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping CI check.");
|
|
1015
1059
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
timeout: 15000,
|
|
1023
|
-
|
|
1024
|
-
).trim();
|
|
1060
|
+
// 1. GitHub Actions workflow runs
|
|
1061
|
+
let workflowRuns: CiCheck[] = [];
|
|
1062
|
+
try {
|
|
1063
|
+
const runsJson = execFileSync(
|
|
1064
|
+
"gh",
|
|
1065
|
+
["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
|
|
1066
|
+
{ cwd, encoding: "utf8", timeout: 15000 },
|
|
1067
|
+
).trim();
|
|
1025
1068
|
|
|
1026
|
-
|
|
1069
|
+
if (runsJson && runsJson !== "[]") {
|
|
1070
|
+
workflowRuns = JSON.parse(runsJson) as CiCheck[];
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
// fail-open for workflow runs; continue to check third-party checks
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// 2. Third-party check runs (CodeRabbit, SonarCloud, Codecov, etc.)
|
|
1077
|
+
let thirdPartyChecks: CiCheck[] = [];
|
|
1078
|
+
const sha = getHeadSha(cwd);
|
|
1079
|
+
if (sha) {
|
|
1080
|
+
thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
|
|
1081
|
+
}
|
|
1027
1082
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
conclusion: string;
|
|
1031
|
-
name: string;
|
|
1032
|
-
}>;
|
|
1083
|
+
// 3. Merge all checks
|
|
1084
|
+
const allChecks = [...workflowRuns, ...thirdPartyChecks];
|
|
1033
1085
|
|
|
1034
|
-
if (
|
|
1086
|
+
if (allChecks.length === 0) return allow(`No CI runs found for branch "${branch}".`);
|
|
1035
1087
|
|
|
1036
|
-
const failing =
|
|
1088
|
+
const failing = allChecks.filter(
|
|
1037
1089
|
(r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped",
|
|
1038
1090
|
);
|
|
1039
1091
|
if (failing.length > 0) {
|
|
@@ -1043,7 +1095,7 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1043
1095
|
);
|
|
1044
1096
|
}
|
|
1045
1097
|
|
|
1046
|
-
const pending =
|
|
1098
|
+
const pending = allChecks.filter(
|
|
1047
1099
|
(r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting",
|
|
1048
1100
|
);
|
|
1049
1101
|
if (pending.length > 0) {
|