failproofai 0.0.2-beta.6 → 0.0.2-beta.8
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/.claude/settings.json +316 -0
- package/.next/standalone/.failproofai/policies/workflow-policies.mjs +62 -0
- package/.next/standalone/.failproofai/policies-config.json +39 -0
- 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]__0.t2266._.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]__0okos0k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0epc5zr._.js → [root-of-the-server]__0pjorff._.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/{0efsuf1p-k4qe.js → 04xfyqyhdxbxz.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17p200_z1ivz4.js → 07g0rbtaux_1t.js} +1 -1
- package/.next/standalone/.next/static/chunks/{031pa5~qfzt~_.js → 09e7drilkf1sn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tood0~87-mm8.js → 0a_xh94bt.y0j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0rvepm.~uvks4.js → 0j752uotyfvjh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0wkzaq-8sxss7.js → 0qi0ubup__3pj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0kbfx4p.g9wnr.js → 0xyvis4r_y.8o.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0jqg886bw85_6.js → 0zfyfi1suoteq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_tx_~f8pi3d7.js → 121a-0zn-knuy.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 +88 -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 +268 -73
- package/.next/standalone/docs/built-in-policies.mdx +19 -3
- package/.next/standalone/docs/configuration.mdx +46 -0
- package/.next/standalone/docs/custom-policies.mdx +65 -7
- 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 +165 -21
- package/.next/standalone/src/hooks/handler.ts +32 -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 +35 -17
- package/README.md +20 -3
- package/bin/failproofai.mjs +5 -0
- package/dist/cli.mjs +268 -73
- package/package.json +1 -1
- package/src/hooks/builtin-policies.ts +70 -18
- package/src/hooks/custom-hooks-loader.ts +165 -21
- package/src/hooks/handler.ts +32 -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 +35 -17
- /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → itedhTSyIDln6TUf41j5X}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → itedhTSyIDln6TUf41j5X}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → itedhTSyIDln6TUf41j5X}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
[](https://www.npmjs.com/package/failproofai)
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
[](https://github.com/exospherehost/failproofai/actions)
|
|
16
|
-
[](https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ)
|
|
17
17
|
|
|
18
18
|
The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously - for **Claude Code** & the **Agents SDK**.
|
|
19
19
|
|
|
@@ -111,10 +111,12 @@ Policy configuration lives in `~/.failproofai/policies-config.json` (global) or
|
|
|
111
111
|
],
|
|
112
112
|
"policyParams": {
|
|
113
113
|
"block-sudo": {
|
|
114
|
-
"allowPatterns": ["sudo systemctl status", "sudo journalctl"]
|
|
114
|
+
"allowPatterns": ["sudo systemctl status", "sudo journalctl"],
|
|
115
|
+
"hint": "Use apt-get directly without sudo."
|
|
115
116
|
},
|
|
116
117
|
"block-push-master": {
|
|
117
|
-
"protectedBranches": ["main", "release", "prod"]
|
|
118
|
+
"protectedBranches": ["main", "release", "prod"],
|
|
119
|
+
"hint": "Try creating a fresh branch instead."
|
|
118
120
|
},
|
|
119
121
|
"sanitize-api-keys": {
|
|
120
122
|
"additionalPatterns": [
|
|
@@ -216,6 +218,21 @@ failproofai policies --install --custom ./my-policies.js
|
|
|
216
218
|
|
|
217
219
|
Custom hooks support transitive local imports, async/await, and access to `process.env`. Errors are fail-open (logged to `~/.failproofai/hook.log`, built-in policies continue). See [docs/custom-hooks.mdx](docs/custom-hooks.mdx) for the full guide.
|
|
218
220
|
|
|
221
|
+
### Convention-based policies (v0.0.2-beta.7+)
|
|
222
|
+
|
|
223
|
+
Drop `*policies.{js,mjs,ts}` files into `.failproofai/policies/` and they're automatically loaded — no `--custom` flag or config changes needed. Works like git hooks: drop a file, it just works.
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
# Project level — committed to git, shared with the team
|
|
227
|
+
.failproofai/policies/security-policies.mjs
|
|
228
|
+
.failproofai/policies/workflow-policies.mjs
|
|
229
|
+
|
|
230
|
+
# User level — personal, applies to all projects
|
|
231
|
+
~/.failproofai/policies/my-policies.mjs
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Both levels load (union). Files are loaded alphabetically within each directory. Prefix with `01-`, `02-`, etc. to control order. See [examples/convention-policies/](examples/convention-policies/) for ready-to-use examples.
|
|
235
|
+
|
|
219
236
|
---
|
|
220
237
|
|
|
221
238
|
## Telemetry
|
package/bin/failproofai.mjs
CHANGED
|
@@ -97,6 +97,11 @@ COMMANDS
|
|
|
97
97
|
--version, -v Print version and exit
|
|
98
98
|
--help, -h Show this help message
|
|
99
99
|
|
|
100
|
+
CONVENTION POLICIES
|
|
101
|
+
Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
|
|
102
|
+
Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
|
|
103
|
+
No --custom flag or config changes needed — just drop files and they're picked up.
|
|
104
|
+
|
|
100
105
|
EXAMPLES
|
|
101
106
|
failproofai policies
|
|
102
107
|
failproofai policies --install
|
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;
|
|
@@ -1349,8 +1402,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1349
1402
|
toolInput,
|
|
1350
1403
|
session
|
|
1351
1404
|
};
|
|
1352
|
-
|
|
1353
|
-
let instructReason = null;
|
|
1405
|
+
const instructEntries = [];
|
|
1354
1406
|
const allowEntries = [];
|
|
1355
1407
|
for (const policy of policies) {
|
|
1356
1408
|
const schema = POLICY_PARAMS_MAP.get(policy.name);
|
|
@@ -1373,7 +1425,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1373
1425
|
continue;
|
|
1374
1426
|
}
|
|
1375
1427
|
if (result.decision === "deny") {
|
|
1376
|
-
const reason = result.reason ?? `Blocked by policy: ${policy.name}
|
|
1428
|
+
const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
|
|
1377
1429
|
hookLogInfo(`deny by "${policy.name}": ${reason}`);
|
|
1378
1430
|
const displayTool = ctx.toolName ?? "unknown tool";
|
|
1379
1431
|
if (eventType === "PreToolUse") {
|
|
@@ -1418,38 +1470,43 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1418
1470
|
decision: "deny"
|
|
1419
1471
|
};
|
|
1420
1472
|
}
|
|
1421
|
-
if (result.decision === "instruct"
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
hookLogInfo(`instruct by "${policy.name}": ${
|
|
1473
|
+
if (result.decision === "instruct") {
|
|
1474
|
+
const reason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
|
|
1475
|
+
instructEntries.push({ policyName: policy.name, reason });
|
|
1476
|
+
hookLogInfo(`instruct by "${policy.name}": ${reason}`);
|
|
1425
1477
|
}
|
|
1426
1478
|
if (result.decision === "allow" && result.reason) {
|
|
1427
1479
|
allowEntries.push({ policyName: policy.name, reason: result.reason });
|
|
1428
1480
|
}
|
|
1429
1481
|
}
|
|
1430
|
-
if (
|
|
1482
|
+
if (instructEntries.length > 0) {
|
|
1483
|
+
const combined = instructEntries.map((e) => e.reason).join(`
|
|
1484
|
+
`);
|
|
1485
|
+
const policyNames = instructEntries.map((e) => e.policyName);
|
|
1431
1486
|
if (eventType === "Stop") {
|
|
1432
1487
|
return {
|
|
1433
1488
|
exitCode: 2,
|
|
1434
1489
|
stdout: "",
|
|
1435
|
-
stderr:
|
|
1436
|
-
policyName:
|
|
1437
|
-
|
|
1490
|
+
stderr: combined,
|
|
1491
|
+
policyName: policyNames[0],
|
|
1492
|
+
policyNames,
|
|
1493
|
+
reason: combined,
|
|
1438
1494
|
decision: "instruct"
|
|
1439
1495
|
};
|
|
1440
1496
|
}
|
|
1441
1497
|
const response = {
|
|
1442
1498
|
hookSpecificOutput: {
|
|
1443
1499
|
hookEventName: eventType,
|
|
1444
|
-
additionalContext: `Instruction from failproofai: ${
|
|
1500
|
+
additionalContext: `Instruction from failproofai: ${combined}`
|
|
1445
1501
|
}
|
|
1446
1502
|
};
|
|
1447
1503
|
return {
|
|
1448
1504
|
exitCode: 0,
|
|
1449
1505
|
stdout: JSON.stringify(response),
|
|
1450
1506
|
stderr: "",
|
|
1451
|
-
policyName:
|
|
1452
|
-
|
|
1507
|
+
policyName: policyNames[0],
|
|
1508
|
+
policyNames,
|
|
1509
|
+
reason: combined,
|
|
1453
1510
|
decision: "instruct"
|
|
1454
1511
|
};
|
|
1455
1512
|
}
|
|
@@ -1533,10 +1590,9 @@ async function createEsmShim(distIndex, distUrl) {
|
|
|
1533
1590
|
const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
|
|
1534
1591
|
const shimCode = [
|
|
1535
1592
|
`import _cjs from '${distUrl}';`,
|
|
1536
|
-
`export const createApp = _cjs.createApp;`,
|
|
1537
|
-
`export const getQueueCondition = _cjs.getQueueCondition;`,
|
|
1538
|
-
`export const clearQueueCondition = _cjs.clearQueueCondition;`,
|
|
1539
1593
|
`export const customPolicies = _cjs.customPolicies;`,
|
|
1594
|
+
`export const getCustomHooks = _cjs.getCustomHooks;`,
|
|
1595
|
+
`export const clearCustomHooks = _cjs.clearCustomHooks;`,
|
|
1540
1596
|
`export const allow = _cjs.allow;`,
|
|
1541
1597
|
`export const deny = _cjs.deny;`,
|
|
1542
1598
|
`export const instruct = _cjs.instruct;`,
|
|
@@ -1616,20 +1672,21 @@ var init_loader_utils = __esm(() => {
|
|
|
1616
1672
|
});
|
|
1617
1673
|
|
|
1618
1674
|
// src/hooks/custom-hooks-loader.ts
|
|
1619
|
-
import { resolve as resolve4, isAbsolute } from "node:path";
|
|
1620
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
1675
|
+
import { resolve as resolve4, isAbsolute, basename } from "node:path";
|
|
1676
|
+
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
1621
1677
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1622
|
-
|
|
1623
|
-
|
|
1678
|
+
import { homedir as homedir4 } from "node:os";
|
|
1679
|
+
function discoverPolicyFiles(dir) {
|
|
1680
|
+
if (!existsSync3(dir))
|
|
1624
1681
|
return [];
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
hookLogWarn(`customPoliciesPath not found: ${absPath}`);
|
|
1682
|
+
try {
|
|
1683
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1684
|
+
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));
|
|
1685
|
+
} catch {
|
|
1630
1686
|
return [];
|
|
1631
1687
|
}
|
|
1632
|
-
|
|
1688
|
+
}
|
|
1689
|
+
async function loadSingleFile(absPath, opts) {
|
|
1633
1690
|
const g = globalThis;
|
|
1634
1691
|
g[LOADING_KEY] = true;
|
|
1635
1692
|
let tmpFiles = [];
|
|
@@ -1645,18 +1702,93 @@ async function loadCustomHooks(customPoliciesPath, opts) {
|
|
|
1645
1702
|
if (opts?.strict)
|
|
1646
1703
|
throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
|
|
1647
1704
|
hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
|
|
1648
|
-
return [];
|
|
1649
1705
|
} finally {
|
|
1650
1706
|
g[LOADING_KEY] = false;
|
|
1651
1707
|
await cleanupTmpFiles(tmpFiles);
|
|
1652
1708
|
}
|
|
1709
|
+
}
|
|
1710
|
+
async function loadCustomHooks(customPoliciesPath, opts) {
|
|
1711
|
+
if (!customPoliciesPath)
|
|
1712
|
+
return [];
|
|
1713
|
+
const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
|
|
1714
|
+
if (!existsSync3(absPath)) {
|
|
1715
|
+
if (opts?.strict)
|
|
1716
|
+
throw new Error(`Custom hooks file not found: ${absPath}`);
|
|
1717
|
+
hookLogWarn(`customPoliciesPath not found: ${absPath}`);
|
|
1718
|
+
return [];
|
|
1719
|
+
}
|
|
1720
|
+
clearCustomHooks();
|
|
1721
|
+
await loadSingleFile(absPath, opts);
|
|
1653
1722
|
return getCustomHooks();
|
|
1654
1723
|
}
|
|
1655
|
-
|
|
1724
|
+
async function loadAllCustomHooks(customPoliciesPath, opts) {
|
|
1725
|
+
clearCustomHooks();
|
|
1726
|
+
const conventionSources = [];
|
|
1727
|
+
if (customPoliciesPath) {
|
|
1728
|
+
const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
|
|
1729
|
+
if (existsSync3(absPath)) {
|
|
1730
|
+
await loadSingleFile(absPath);
|
|
1731
|
+
} else {
|
|
1732
|
+
hookLogWarn(`customPoliciesPath not found: ${absPath}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const hooksBeforeConvention = getCustomHooks().length;
|
|
1736
|
+
const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
|
|
1737
|
+
const projectFiles = discoverPolicyFiles(projectDir);
|
|
1738
|
+
for (const file of projectFiles) {
|
|
1739
|
+
const hooksBefore = getCustomHooks().length;
|
|
1740
|
+
await loadSingleFile(file);
|
|
1741
|
+
const newHooks = getCustomHooks().slice(hooksBefore);
|
|
1742
|
+
if (newHooks.length > 0) {
|
|
1743
|
+
conventionSources.push({
|
|
1744
|
+
scope: "project",
|
|
1745
|
+
file: basename(file),
|
|
1746
|
+
hookNames: newHooks.map((h) => h.name)
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const userDir = resolve4(homedir4(), ".failproofai", "policies");
|
|
1751
|
+
const userFiles = discoverPolicyFiles(userDir);
|
|
1752
|
+
for (const file of userFiles) {
|
|
1753
|
+
const hooksBefore = getCustomHooks().length;
|
|
1754
|
+
await loadSingleFile(file);
|
|
1755
|
+
const newHooks = getCustomHooks().slice(hooksBefore);
|
|
1756
|
+
if (newHooks.length > 0) {
|
|
1757
|
+
conventionSources.push({
|
|
1758
|
+
scope: "user",
|
|
1759
|
+
file: basename(file),
|
|
1760
|
+
hookNames: newHooks.map((h) => h.name)
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const allHooks = getCustomHooks();
|
|
1765
|
+
const conventionCount = allHooks.length - hooksBeforeConvention;
|
|
1766
|
+
if (projectFiles.length > 0 || userFiles.length > 0) {
|
|
1767
|
+
hookLogInfo(`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`);
|
|
1768
|
+
}
|
|
1769
|
+
const hookNameToScope = new Map;
|
|
1770
|
+
for (const source of conventionSources) {
|
|
1771
|
+
for (const name of source.hookNames) {
|
|
1772
|
+
hookNameToScope.set(name, source.scope);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const conventionHookRefs = new Set;
|
|
1776
|
+
for (const hook of allHooks.slice(hooksBeforeConvention)) {
|
|
1777
|
+
conventionHookRefs.add(hook);
|
|
1778
|
+
}
|
|
1779
|
+
for (const hook of allHooks) {
|
|
1780
|
+
if (conventionHookRefs.has(hook)) {
|
|
1781
|
+
hook.__conventionScope = hookNameToScope.get(hook.name) ?? "project";
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return { hooks: allHooks, conventionSources };
|
|
1785
|
+
}
|
|
1786
|
+
var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE;
|
|
1656
1787
|
var init_custom_hooks_loader = __esm(() => {
|
|
1657
1788
|
init_hook_logger();
|
|
1658
1789
|
init_custom_hooks_registry();
|
|
1659
1790
|
init_loader_utils();
|
|
1791
|
+
CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
|
|
1660
1792
|
});
|
|
1661
1793
|
|
|
1662
1794
|
// src/hooks/hook-activity-store.ts
|
|
@@ -1665,14 +1797,14 @@ import {
|
|
|
1665
1797
|
writeFileSync as writeFileSync2,
|
|
1666
1798
|
appendFileSync as appendFileSync2,
|
|
1667
1799
|
renameSync as renameSync2,
|
|
1668
|
-
readdirSync,
|
|
1800
|
+
readdirSync as readdirSync2,
|
|
1669
1801
|
mkdirSync as mkdirSync3,
|
|
1670
1802
|
existsSync as existsSync4,
|
|
1671
1803
|
statSync as statSync2,
|
|
1672
1804
|
unlinkSync
|
|
1673
1805
|
} from "node:fs";
|
|
1674
1806
|
import { join as join3 } from "node:path";
|
|
1675
|
-
import { homedir as
|
|
1807
|
+
import { homedir as homedir5 } from "node:os";
|
|
1676
1808
|
function ensureDir() {
|
|
1677
1809
|
if (!existsSync4(storeDir)) {
|
|
1678
1810
|
mkdirSync3(storeDir, { recursive: true });
|
|
@@ -1777,12 +1909,12 @@ function updateStats(entry) {
|
|
|
1777
1909
|
}
|
|
1778
1910
|
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
1911
|
var init_hook_activity_store = __esm(() => {
|
|
1780
|
-
DEFAULT_STORE_DIR = join3(
|
|
1912
|
+
DEFAULT_STORE_DIR = join3(homedir5(), ".failproofai", "cache", "hook-activity");
|
|
1781
1913
|
storeDir = DEFAULT_STORE_DIR;
|
|
1782
1914
|
});
|
|
1783
1915
|
|
|
1784
1916
|
// package.json
|
|
1785
|
-
var version2 = "0.0.2-beta.
|
|
1917
|
+
var version2 = "0.0.2-beta.8";
|
|
1786
1918
|
var init_package = () => {};
|
|
1787
1919
|
|
|
1788
1920
|
// src/posthog-key.ts
|
|
@@ -1944,9 +2076,14 @@ async function handleHookEvent(eventType) {
|
|
|
1944
2076
|
const config = readMergedHooksConfig(session.cwd);
|
|
1945
2077
|
clearPolicies();
|
|
1946
2078
|
registerBuiltinPolicies(config.enabledPolicies);
|
|
1947
|
-
const
|
|
2079
|
+
const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
|
|
2080
|
+
const customHooksList = loadResult.hooks;
|
|
2081
|
+
const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
|
|
1948
2082
|
for (const hook of customHooksList) {
|
|
1949
2083
|
const hookName = hook.name;
|
|
2084
|
+
const conventionScope = hook.__conventionScope;
|
|
2085
|
+
const isConvention = !!conventionScope;
|
|
2086
|
+
const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
|
|
1950
2087
|
const fn = async (ctx) => {
|
|
1951
2088
|
try {
|
|
1952
2089
|
const result2 = await Promise.race([
|
|
@@ -1957,16 +2094,18 @@ async function handleHookEvent(eventType) {
|
|
|
1957
2094
|
} catch (err) {
|
|
1958
2095
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1959
2096
|
const isTimeout = msg === "timeout";
|
|
1960
|
-
hookLogWarn(
|
|
2097
|
+
hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
|
|
1961
2098
|
trackHookEvent(getInstanceId(), "custom_hook_error", {
|
|
1962
2099
|
hook_name: hookName,
|
|
1963
2100
|
error_type: isTimeout ? "timeout" : "exception",
|
|
1964
|
-
event_type: eventType
|
|
2101
|
+
event_type: eventType,
|
|
2102
|
+
is_convention_policy: isConvention,
|
|
2103
|
+
convention_scope: conventionScope ?? null
|
|
1965
2104
|
});
|
|
1966
2105
|
return { decision: "allow" };
|
|
1967
2106
|
}
|
|
1968
2107
|
};
|
|
1969
|
-
registerPolicy(
|
|
2108
|
+
registerPolicy(`${prefix}/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
|
|
1970
2109
|
}
|
|
1971
2110
|
if (customHooksList.length > 0) {
|
|
1972
2111
|
trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
|
|
@@ -1975,7 +2114,16 @@ async function handleHookEvent(eventType) {
|
|
|
1975
2114
|
event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
|
|
1976
2115
|
});
|
|
1977
2116
|
}
|
|
1978
|
-
|
|
2117
|
+
if (loadResult.conventionSources.length > 0) {
|
|
2118
|
+
trackHookEvent(getInstanceId(), "convention_policies_loaded", {
|
|
2119
|
+
event_type: eventType,
|
|
2120
|
+
project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
|
|
2121
|
+
user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
|
|
2122
|
+
convention_hook_count: conventionHookNames.size,
|
|
2123
|
+
convention_hook_names: [...conventionHookNames]
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
|
|
1979
2127
|
const result = await evaluatePolicies(eventType, parsed, session, config);
|
|
1980
2128
|
const durationMs = Math.round(performance.now() - startTime);
|
|
1981
2129
|
hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
|
|
@@ -2007,7 +2155,9 @@ async function handleHookEvent(eventType) {
|
|
|
2007
2155
|
if (result.decision === "deny" || result.decision === "instruct") {
|
|
2008
2156
|
try {
|
|
2009
2157
|
const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
|
|
2010
|
-
const
|
|
2158
|
+
const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
|
|
2159
|
+
const conventionScope = isConventionPolicy ? result.policyName.match(/^\.failproofai-(project|user)\//)?.[1] ?? null : null;
|
|
2160
|
+
const hasCustomParams = !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
|
|
2011
2161
|
const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
|
|
2012
2162
|
const distinctId = getInstanceId();
|
|
2013
2163
|
await trackHookEvent(distinctId, "hook_policy_triggered", {
|
|
@@ -2016,6 +2166,8 @@ async function handleHookEvent(eventType) {
|
|
|
2016
2166
|
policy_name: result.policyName,
|
|
2017
2167
|
decision: result.decision,
|
|
2018
2168
|
is_custom_hook: isCustomHook,
|
|
2169
|
+
is_convention_policy: isConventionPolicy,
|
|
2170
|
+
convention_scope: conventionScope,
|
|
2019
2171
|
has_custom_params: hasCustomParams,
|
|
2020
2172
|
param_keys_overridden: paramKeysOverridden
|
|
2021
2173
|
});
|
|
@@ -2348,13 +2500,13 @@ __export(exports_manager, {
|
|
|
2348
2500
|
});
|
|
2349
2501
|
import { execSync as execSync3 } from "node:child_process";
|
|
2350
2502
|
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
|
|
2503
|
+
import { resolve as resolve5, dirname as dirname3, basename as basename2 } from "node:path";
|
|
2504
|
+
import { homedir as homedir6, platform, arch, release, hostname } from "node:os";
|
|
2353
2505
|
function getSettingsPath(scope, cwd) {
|
|
2354
2506
|
const base = cwd ? resolve5(cwd) : process.cwd();
|
|
2355
2507
|
switch (scope) {
|
|
2356
2508
|
case "user":
|
|
2357
|
-
return resolve5(
|
|
2509
|
+
return resolve5(homedir6(), ".claude", "settings.json");
|
|
2358
2510
|
case "project":
|
|
2359
2511
|
return resolve5(base, ".claude", "settings.json");
|
|
2360
2512
|
case "local":
|
|
@@ -2481,7 +2633,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
|
|
|
2481
2633
|
}
|
|
2482
2634
|
}
|
|
2483
2635
|
const binaryPath = resolveFailproofaiBinary();
|
|
2484
|
-
const previousConfig =
|
|
2636
|
+
const previousConfig = readScopedHooksConfig(scope, cwd);
|
|
2485
2637
|
const previousEnabled = new Set(previousConfig.enabledPolicies);
|
|
2486
2638
|
let selectedPolicies;
|
|
2487
2639
|
if (policyNames !== undefined) {
|
|
@@ -2515,7 +2667,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
|
|
|
2515
2667
|
console.log(`
|
|
2516
2668
|
Validated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`);
|
|
2517
2669
|
}
|
|
2518
|
-
|
|
2670
|
+
writeScopedHooksConfig(configToWrite, scope, cwd);
|
|
2519
2671
|
console.log(`
|
|
2520
2672
|
Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`);
|
|
2521
2673
|
if (removeCustomHooks) {
|
|
@@ -2592,15 +2744,16 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
|
|
|
2592
2744
|
}
|
|
2593
2745
|
}
|
|
2594
2746
|
async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
2747
|
+
const configScope = scope === "all" ? "user" : scope;
|
|
2595
2748
|
if (opts?.removeCustomHooks) {
|
|
2596
|
-
const config =
|
|
2749
|
+
const config = readScopedHooksConfig(configScope, cwd);
|
|
2597
2750
|
delete config.customPoliciesPath;
|
|
2598
|
-
|
|
2751
|
+
writeScopedHooksConfig(config, configScope, cwd);
|
|
2599
2752
|
console.log("Custom hooks path cleared.");
|
|
2600
2753
|
}
|
|
2601
2754
|
if (policyNames && policyNames.length > 0 && !(policyNames.length === 1 && policyNames[0] === "all")) {
|
|
2602
2755
|
validatePolicyNames(policyNames);
|
|
2603
|
-
const config =
|
|
2756
|
+
const config = readScopedHooksConfig(configScope, cwd);
|
|
2604
2757
|
const removeSet = new Set(policyNames);
|
|
2605
2758
|
const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
|
|
2606
2759
|
const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
|
|
@@ -2614,7 +2767,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
|
2614
2767
|
enabledPolicies: remaining,
|
|
2615
2768
|
...filteredParams && Object.keys(filteredParams).length > 0 ? { policyParams: filteredParams } : {}
|
|
2616
2769
|
};
|
|
2617
|
-
|
|
2770
|
+
writeScopedHooksConfig(updatedConfig, configScope, cwd);
|
|
2618
2771
|
try {
|
|
2619
2772
|
const distinctId = getInstanceId();
|
|
2620
2773
|
const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
|
|
@@ -2635,7 +2788,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
|
2635
2788
|
console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
|
|
2636
2789
|
return;
|
|
2637
2790
|
}
|
|
2638
|
-
const configBeforeRemoval =
|
|
2791
|
+
const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
|
|
2639
2792
|
const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
|
|
2640
2793
|
let totalRemoved = 0;
|
|
2641
2794
|
for (const s of scopesToRemove) {
|
|
@@ -2682,10 +2835,18 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
|
|
|
2682
2835
|
hostname_hash: hashToId(hostname())
|
|
2683
2836
|
});
|
|
2684
2837
|
} catch {}
|
|
2685
|
-
if (scope === "all"
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
2838
|
+
if (scope === "all") {
|
|
2839
|
+
for (const s of HOOK_SCOPES) {
|
|
2840
|
+
const existing = readScopedHooksConfig(s, cwd);
|
|
2841
|
+
if (existing.enabledPolicies.length > 0 || existing.customPoliciesPath || existing.policyParams) {
|
|
2842
|
+
const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
|
|
2843
|
+
writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, s, cwd);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
} else if (!HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
|
|
2847
|
+
const existing = readScopedHooksConfig(configScope, cwd);
|
|
2848
|
+
const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
|
|
2849
|
+
writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
|
|
2689
2850
|
}
|
|
2690
2851
|
}
|
|
2691
2852
|
async function listHooks(cwd) {
|
|
@@ -2822,6 +2983,35 @@ Failproof AI Hook Policies
|
|
|
2822
2983
|
}
|
|
2823
2984
|
console.log();
|
|
2824
2985
|
}
|
|
2986
|
+
const base = cwd ? resolve5(cwd) : process.cwd();
|
|
2987
|
+
const conventionDirs = [
|
|
2988
|
+
{ label: "Project", dir: resolve5(base, ".failproofai", "policies") },
|
|
2989
|
+
{ label: "User", dir: resolve5(homedir6(), ".failproofai", "policies") }
|
|
2990
|
+
];
|
|
2991
|
+
for (const { label, dir } of conventionDirs) {
|
|
2992
|
+
const files = discoverPolicyFiles(dir);
|
|
2993
|
+
if (files.length === 0)
|
|
2994
|
+
continue;
|
|
2995
|
+
console.log(`
|
|
2996
|
+
── Convention Policies — ${label} (${dir}) ──────────`);
|
|
2997
|
+
for (const file of files) {
|
|
2998
|
+
try {
|
|
2999
|
+
const hooks = await loadCustomHooks(file);
|
|
3000
|
+
if (hooks.length === 0) {
|
|
3001
|
+
const filename = basename2(file);
|
|
3002
|
+
console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
|
|
3003
|
+
} else {
|
|
3004
|
+
const filename = basename2(file);
|
|
3005
|
+
const hookSummary = hooks.map((h) => h.name).join(", ");
|
|
3006
|
+
console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
|
|
3007
|
+
}
|
|
3008
|
+
} catch {
|
|
3009
|
+
const filename = basename2(file);
|
|
3010
|
+
console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
console.log();
|
|
3014
|
+
}
|
|
2825
3015
|
}
|
|
2826
3016
|
var VALID_POLICY_NAMES;
|
|
2827
3017
|
var init_manager = __esm(() => {
|
|
@@ -2837,10 +3027,10 @@ var init_manager = __esm(() => {
|
|
|
2837
3027
|
});
|
|
2838
3028
|
|
|
2839
3029
|
// lib/paths.ts
|
|
2840
|
-
import { homedir as
|
|
3030
|
+
import { homedir as homedir7 } from "os";
|
|
2841
3031
|
import { join as join4 } from "path";
|
|
2842
3032
|
function getDefaultClaudeProjectsPath() {
|
|
2843
|
-
return join4(
|
|
3033
|
+
return join4(homedir7(), ".claude", "projects");
|
|
2844
3034
|
}
|
|
2845
3035
|
var init_paths = () => {};
|
|
2846
3036
|
|
|
@@ -3009,7 +3199,7 @@ import { realpathSync as realpathSync2 } from "fs";
|
|
|
3009
3199
|
import { dirname as dirname5, resolve as resolve8 } from "path";
|
|
3010
3200
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3011
3201
|
// package.json
|
|
3012
|
-
var version = "0.0.2-beta.
|
|
3202
|
+
var version = "0.0.2-beta.8";
|
|
3013
3203
|
|
|
3014
3204
|
// bin/failproofai.mjs
|
|
3015
3205
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
|
@@ -3073,6 +3263,11 @@ COMMANDS
|
|
|
3073
3263
|
--version, -v Print version and exit
|
|
3074
3264
|
--help, -h Show this help message
|
|
3075
3265
|
|
|
3266
|
+
CONVENTION POLICIES
|
|
3267
|
+
Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
|
|
3268
|
+
Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
|
|
3269
|
+
No --custom flag or config changes needed \u2014 just drop files and they're picked up.
|
|
3270
|
+
|
|
3076
3271
|
EXAMPLES
|
|
3077
3272
|
failproofai policies
|
|
3078
3273
|
failproofai policies --install
|