failproofai 0.0.14-beta.1 → 0.0.14
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 +3 -3
- 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/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/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 +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- 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 +9 -9
- 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/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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/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/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/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/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]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0brjczq._.js → [root-of-the-server]__0zf8xlq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wp9w6i._.js → [root-of-the-server]__1s2b6h0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_12rjfea.js → node_modules_html-to-image_es_index_0gkzjaa.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- 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 +10 -10
- package/.next/standalone/.next/static/chunks/06z_ex1ts3n4s.js +1 -0
- package/.next/standalone/.next/static/chunks/0o0ut0ugd0_-s.js +1 -0
- package/.next/standalone/.next/static/chunks/0xl97x-874k-u.js +1 -0
- package/.next/standalone/.next/static/chunks/19d7hqx8udopz.js +1 -0
- package/.next/standalone/.next/static/chunks/{0emg30_6svycu.js → 29o4j32z643_a.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1359o5785rmve.js → 2uh_4k4m296z5.js} +2 -2
- package/.next/standalone/.next/static/chunks/370sj2lgkw5ey.js +1 -0
- package/.next/standalone/.next/static/chunks/{0d5lypu1zs21r.js → 3s8aaig91937v.js} +1 -1
- package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
- package/.next/standalone/.next/static/chunks/{0o9wpnze1ey8y.js → 42oeva1-w0z2d.js} +1 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
- package/.next/standalone/app/components/copy-button.tsx +22 -6
- package/.next/standalone/app/components/log-viewer/entry-row.tsx +8 -3
- package/.next/standalone/app/components/raw-log-viewer.tsx +33 -15
- package/.next/standalone/app/policies/hooks-client.tsx +1 -1
- package/.next/standalone/{ci/cli-integration → integration-suite}/README.md +25 -7
- package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
- package/.next/standalone/{ci/cli-integration → integration-suite}/run.sh +4 -4
- package/.next/standalone/lib/entry-keys.ts +36 -0
- package/.next/standalone/lib/format-duration.ts +23 -9
- package/.next/standalone/package.json +5 -4
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +342 -106
- package/lib/entry-keys.ts +36 -0
- package/lib/format-duration.ts +23 -9
- package/package.json +5 -4
- package/scripts/translate-docs/cli.ts +6 -0
- package/scripts/translate-docs/mdx-translator.ts +30 -19
- package/scripts/translate-docs/readme-translator.ts +31 -21
- package/scripts/translate-docs/translator.ts +142 -7
- package/scripts/translate-docs/types.ts +6 -0
- package/scripts/translate-docs/validate-translation.ts +132 -0
- package/scripts/validate-mdx.ts +80 -11
- package/src/audit/cli.ts +12 -6
- package/src/auth/cli.ts +14 -5
- package/src/hooks/builtin-policies.ts +168 -50
- package/src/hooks/configure-wizard.ts +238 -22
- package/src/hooks/custom-hooks-loader.ts +70 -4
- package/src/hooks/handler.ts +4 -1
- package/src/hooks/policy-types.ts +8 -0
- package/src/hooks/tui.ts +95 -28
- package/.next/standalone/.next/static/chunks/20tuctw38jbgw.js +0 -1
- package/.next/standalone/.next/static/chunks/2ldhnaweu9ew3.js +0 -1
- package/.next/standalone/.next/static/chunks/2vav_x0_336ds.js +0 -1
- package/.next/standalone/.next/static/chunks/3cg7x0joi2b98.js +0 -1
- package/.next/standalone/.next/static/chunks/3k87p-tijud-4.js +0 -1
- package/.next/standalone/.next/static/chunks/3kmaqcp7rldad.js +0 -1
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → WQRKgSfFekLcC_pvyzp4S}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → WQRKgSfFekLcC_pvyzp4S}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → WQRKgSfFekLcC_pvyzp4S}/_ssgManifest.js +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/Dockerfile +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/canary-policies.mjs +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/capture-tokens.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/inject-tokens.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/install-clis.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/probe-cli.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/report.js +0 -0
|
@@ -12,19 +12,29 @@
|
|
|
12
12
|
* whatever is already enabled, so unticking removes). Reuses the tested
|
|
13
13
|
* install/uninstall manager and the existing searchable policy picker.
|
|
14
14
|
*/
|
|
15
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { resolve, sep } from "node:path";
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
selectOne,
|
|
21
|
+
multiSelect,
|
|
22
|
+
intro,
|
|
23
|
+
outro,
|
|
24
|
+
summarize,
|
|
25
|
+
type MultiChoice,
|
|
26
|
+
type TTYIn,
|
|
27
|
+
type TTYOut,
|
|
28
|
+
} from "./tui";
|
|
20
29
|
import {
|
|
21
30
|
detectInstalledClis,
|
|
22
31
|
getIntegration,
|
|
23
32
|
} from "./integrations";
|
|
24
33
|
import { INTEGRATION_TYPES, type IntegrationType, type HookScope } from "./types";
|
|
25
34
|
import { installHooks } from "./manager";
|
|
26
|
-
import { getConfigPathForScope } from "./hooks-config";
|
|
35
|
+
import { getConfigPathForScope, readHooksConfig } from "./hooks-config";
|
|
27
36
|
import { POLICY_PRESETS, resolvePreset, resolveEverything } from "./policy-presets";
|
|
37
|
+
import { discoverPolicyFiles, findSkippedPolicyFiles } from "./custom-hooks-loader";
|
|
28
38
|
import { trackHookEvent } from "./hook-telemetry";
|
|
29
39
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
30
40
|
|
|
@@ -75,7 +85,13 @@ export function buildScopeChoices(cwd: string) {
|
|
|
75
85
|
];
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
/** The CLIs that can actually be configured at `scope`. Hermes and OpenClaw
|
|
89
|
+
* are gateways with no project-level config, so they are user-scope only. */
|
|
90
|
+
export function clisSupportingScope(scope: HookScope): IntegrationType[] {
|
|
91
|
+
return INTEGRATION_TYPES.filter((id) => getIntegration(id).scopes.includes(scope));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildAgentChoices(scope: HookScope, cwd: string): MultiChoice<IntegrationType>[] {
|
|
79
95
|
const detected = new Set(detectInstalledClis());
|
|
80
96
|
// Detected first, then the rest as "install ahead of time".
|
|
81
97
|
const ordered = [
|
|
@@ -85,6 +101,25 @@ export function buildAgentChoices(scope: HookScope, cwd: string) {
|
|
|
85
101
|
return ordered.map((id) => {
|
|
86
102
|
const integration = getIntegration(id);
|
|
87
103
|
const isDetected = detected.has(id);
|
|
104
|
+
|
|
105
|
+
// Not every CLI can be configured at every scope — Hermes and OpenClaw
|
|
106
|
+
// have no project config at all. Offering them anyway meant picking "Just
|
|
107
|
+
// this project" and applying died with `Scope "project" is not supported
|
|
108
|
+
// by Hermes`, after the user had answered every question. Show them as
|
|
109
|
+
// locked and unchecked with the reason, so the constraint is visible
|
|
110
|
+
// instead of being discovered as a crash.
|
|
111
|
+
const supported = integration.scopes.includes(scope);
|
|
112
|
+
if (!supported) {
|
|
113
|
+
return {
|
|
114
|
+
label: integration.displayName,
|
|
115
|
+
value: id,
|
|
116
|
+
checked: false,
|
|
117
|
+
locked: true,
|
|
118
|
+
section: "Global only · not configurable per-project",
|
|
119
|
+
hint: `supports ${integration.scopes.join(", ")} scope — rerun with "Everywhere I code"`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
88
123
|
let installedHere = false;
|
|
89
124
|
try {
|
|
90
125
|
installedHere = integration.hooksInstalledInSettings(scope, cwd);
|
|
@@ -103,18 +138,62 @@ export function buildAgentChoices(scope: HookScope, cwd: string) {
|
|
|
103
138
|
|
|
104
139
|
const EVERYTHING = "__everything__";
|
|
105
140
|
const ALL_CLIS = "__all_clis__";
|
|
141
|
+
/** Sentinel for the locked "Custom" row — informational, never resolves to
|
|
142
|
+
* builtin policy names (custom policies load by convention, not by config). */
|
|
143
|
+
const CUSTOM = "__custom__";
|
|
106
144
|
|
|
107
145
|
/** The themed preset bundles for the wizard's multi-select, plus an "Everything"
|
|
108
146
|
* option that enables the full builtin policy set. */
|
|
109
|
-
export function buildPresetChoices() {
|
|
110
|
-
const choices:
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
export function buildPresetChoices(cwd: string = process.cwd(), enabled = true) {
|
|
148
|
+
const choices: MultiChoice<string>[] = POLICY_PRESETS.map((p) => ({
|
|
149
|
+
label: p.label,
|
|
150
|
+
value: p.id,
|
|
151
|
+
hint: p.description,
|
|
152
|
+
}));
|
|
113
153
|
choices.push({
|
|
114
154
|
label: "Everything",
|
|
115
155
|
value: EVERYTHING,
|
|
116
156
|
hint: `all ${resolveEverything().length} policies`,
|
|
117
157
|
});
|
|
158
|
+
|
|
159
|
+
// The Custom row is ALWAYS present, because it is the only place the feature
|
|
160
|
+
// is discoverable: a user who has never written a policy cannot learn the
|
|
161
|
+
// capability exists, and one who wrote a badly-named file cannot learn why
|
|
162
|
+
// nothing happened.
|
|
163
|
+
//
|
|
164
|
+
// When there are loadable files it is a REAL checkbox — unticking writes
|
|
165
|
+
// `customPoliciesEnabled: false`, which switches convention discovery off
|
|
166
|
+
// without renaming or deleting anything. With nothing to toggle (no files,
|
|
167
|
+
// or only skipped ones) it falls back to a locked status row.
|
|
168
|
+
const custom = describeCustomPolicies(cwd);
|
|
169
|
+
const skipped = custom.warnings.length;
|
|
170
|
+
const plural = (n: number) => `${n} file${n === 1 ? "" : "s"}`;
|
|
171
|
+
const skippedNote = skipped > 0 ? ` · ${skipped} skipped, see next screen` : "";
|
|
172
|
+
|
|
173
|
+
if (custom.fileCount > 0) {
|
|
174
|
+
choices.push({
|
|
175
|
+
label: "Custom",
|
|
176
|
+
value: CUSTOM,
|
|
177
|
+
checked: enabled,
|
|
178
|
+
// Deliberately NOT summaryExclude'd: this one is a real choice, and the
|
|
179
|
+
// step summary is the only place the user sees what they picked. Hiding
|
|
180
|
+
// it meant unticking Custom and every bundle showed "none", giving no
|
|
181
|
+
// way to tell the toggle had registered.
|
|
182
|
+
hint: `${plural(custom.fileCount)} in ${custom.scopes.join(" + ")}${skippedNote}`,
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
choices.push({
|
|
186
|
+
label: "Custom",
|
|
187
|
+
value: CUSTOM,
|
|
188
|
+
locked: true,
|
|
189
|
+
checked: false,
|
|
190
|
+
summaryExclude: true,
|
|
191
|
+
hint:
|
|
192
|
+
skipped > 0
|
|
193
|
+
? `${plural(skipped)} found but NOT loaded — see next screen`
|
|
194
|
+
: "none yet · drop *-policies.mjs in .failproofai/policies/",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
118
197
|
return choices;
|
|
119
198
|
}
|
|
120
199
|
|
|
@@ -124,14 +203,100 @@ export function buildPresetChoices() {
|
|
|
124
203
|
* enables the full policy set and wins over any presets.
|
|
125
204
|
*/
|
|
126
205
|
export function resolvePresetSelection(values: string[]): string[] {
|
|
127
|
-
|
|
128
|
-
|
|
206
|
+
// The Custom row is informational — custom policies are discovered from disk
|
|
207
|
+
// by the loader, never named in the enabled-policies config — so it must not
|
|
208
|
+
// reach resolvePreset(), which only knows builtin bundle ids.
|
|
209
|
+
const selected = values.filter((v) => v !== CUSTOM);
|
|
210
|
+
if (selected.includes(EVERYTHING)) return resolveEverything();
|
|
211
|
+
return [...new Set(selected.flatMap((id) => resolvePreset(id)))];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const DIM_NOTE = "(auto-loaded)";
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Persist the Custom checkbox into the scope's config, after installHooks has
|
|
218
|
+
* written it (installHooks copies the previous config forward, so writing
|
|
219
|
+
* first would be overwritten).
|
|
220
|
+
*
|
|
221
|
+
* Writes the key only to turn discovery OFF, and removes it when turning back
|
|
222
|
+
* on, so the common case leaves no `customPoliciesEnabled: true` noise in the
|
|
223
|
+
* file and "absent means enabled" stays the single default. `undefined` means
|
|
224
|
+
* there was nothing to toggle — leave whatever is there alone.
|
|
225
|
+
*/
|
|
226
|
+
export function setCustomPoliciesEnabled(
|
|
227
|
+
scope: HookScope,
|
|
228
|
+
cwd: string,
|
|
229
|
+
enabled: boolean | undefined,
|
|
230
|
+
): void {
|
|
231
|
+
if (enabled === undefined) return;
|
|
232
|
+
const path = getConfigPathForScope(scope, cwd);
|
|
233
|
+
let config: Record<string, unknown> = {};
|
|
234
|
+
try {
|
|
235
|
+
if (existsSync(path)) config = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
236
|
+
} catch {
|
|
237
|
+
return; // a malformed config is the install path's problem, not ours
|
|
238
|
+
}
|
|
239
|
+
if (enabled) delete config.customPoliciesEnabled;
|
|
240
|
+
else config.customPoliciesEnabled = false;
|
|
241
|
+
try {
|
|
242
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
243
|
+
} catch {
|
|
244
|
+
/* best-effort: never fail a completed setup over this flag */
|
|
245
|
+
}
|
|
129
246
|
}
|
|
130
247
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Summarise the custom policy files on disk, for the review screen.
|
|
250
|
+
*
|
|
251
|
+
* Only lists files — it deliberately does NOT load them. Loading executes
|
|
252
|
+
* user code, which is fine on the hook path but wrong in an interactive
|
|
253
|
+
* wizard the user hasn't confirmed yet.
|
|
254
|
+
*
|
|
255
|
+
* `warnings` covers the silent-skip trap: a file in the right directory whose
|
|
256
|
+
* name doesn't end in `policies.{js,mjs,ts}` is ignored entirely, with nothing
|
|
257
|
+
* on screen to say so. Surfacing it here is the difference between "my rule
|
|
258
|
+
* isn't working and I don't know why" and a one-line rename.
|
|
259
|
+
*/
|
|
260
|
+
export function describeCustomPolicies(cwd: string): {
|
|
261
|
+
active: string[];
|
|
262
|
+
warnings: string[];
|
|
263
|
+
fileCount: number;
|
|
264
|
+
scopes: string[];
|
|
265
|
+
} {
|
|
266
|
+
const active: string[] = [];
|
|
267
|
+
const warnings: string[] = [];
|
|
268
|
+
const scopes: string[] = [];
|
|
269
|
+
let fileCount = 0;
|
|
270
|
+
const dirs: Array<{ dir: string; label: string }> = [
|
|
271
|
+
{ dir: resolve(cwd, ".failproofai", "policies"), label: "project" },
|
|
272
|
+
{ dir: resolve(homedir(), ".failproofai", "policies"), label: "global" },
|
|
273
|
+
];
|
|
274
|
+
for (const { dir, label } of dirs) {
|
|
275
|
+
const found = discoverPolicyFiles(dir);
|
|
276
|
+
if (found.length > 0) {
|
|
277
|
+
active.push(`${found.length} file${found.length === 1 ? "" : "s"} (${label})`);
|
|
278
|
+
scopes.push(label);
|
|
279
|
+
fileCount += found.length;
|
|
280
|
+
}
|
|
281
|
+
for (const name of findSkippedPolicyFiles(dir)) {
|
|
282
|
+
warnings.push(
|
|
283
|
+
`! ${homeify(resolve(dir, name))} is NOT loaded — rename to ` +
|
|
284
|
+
`${name.replace(/\.(js|mjs|ts)$/, "-policies.$1")}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { active, warnings, fileCount, scopes };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function reviewLines(state: {
|
|
292
|
+
scope: HookScope;
|
|
293
|
+
clis: IntegrationType[];
|
|
294
|
+
policies: string[];
|
|
295
|
+
cwd: string;
|
|
296
|
+
/** The Custom checkbox. `undefined` = nothing to toggle, leave as-is. */
|
|
297
|
+
customEnabled?: boolean;
|
|
298
|
+
}): string[] {
|
|
299
|
+
const { scope, clis, policies, cwd, customEnabled } = state;
|
|
135
300
|
const where =
|
|
136
301
|
scope === "project" ? `This project (${homeify(cwd)})` : "Everywhere (global)";
|
|
137
302
|
const lines: string[] = [];
|
|
@@ -139,6 +304,20 @@ export function reviewLines(
|
|
|
139
304
|
lines.push(` Where : ${where}`);
|
|
140
305
|
lines.push(` Assistants : ${assistantNames.length ? summarize(assistantNames, "assistants") : "(none)"}`);
|
|
141
306
|
lines.push(` Policies : ${policies.length} enabled`);
|
|
307
|
+
|
|
308
|
+
// Reflect the Custom decision, not just what is on disk. Reporting
|
|
309
|
+
// "1 file (project) (auto-loaded)" after the user had just unticked the row
|
|
310
|
+
// stated the opposite of what was about to happen.
|
|
311
|
+
const custom = describeCustomPolicies(cwd);
|
|
312
|
+
if (custom.active.length > 0) {
|
|
313
|
+
lines.push(
|
|
314
|
+
` Custom : ${custom.active.join(" · ")} ${
|
|
315
|
+
customEnabled === false ? "— DISABLED, will not load" : DIM_NOTE
|
|
316
|
+
}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
for (const warning of custom.warnings) lines.push(` ${warning}`);
|
|
320
|
+
|
|
142
321
|
lines.push("");
|
|
143
322
|
lines.push(" This will update:");
|
|
144
323
|
for (const cli of clis) {
|
|
@@ -279,7 +458,12 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
279
458
|
{
|
|
280
459
|
label: "Everything available",
|
|
281
460
|
value: ALL_CLIS,
|
|
282
|
-
|
|
461
|
+
// Counts only what this scope can actually take — expanding to all 12
|
|
462
|
+
// under project scope is what crashed the apply on Hermes.
|
|
463
|
+
hint: `protect all ${clisSupportingScope(scope).length} CLIs configurable here`,
|
|
464
|
+
// A selector, not an assistant. Counting it gave "13 assistants" for
|
|
465
|
+
// the 12 supported CLIs, and listed "Everything available" among them.
|
|
466
|
+
summaryExclude: true,
|
|
283
467
|
},
|
|
284
468
|
...buildAgentChoices(scope, cwd),
|
|
285
469
|
],
|
|
@@ -290,15 +474,28 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
290
474
|
stdout,
|
|
291
475
|
});
|
|
292
476
|
if (clisSel === null) return cancel();
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
477
|
+
// Filter to what this scope supports in BOTH branches: "Everything
|
|
478
|
+
// available" must not expand to CLIs that cannot take this scope, and a
|
|
479
|
+
// locked row can't be ticked but belt-and-braces keeps the invariant local
|
|
480
|
+
// to the one place `clis` is built.
|
|
481
|
+
const supported = new Set(clisSupportingScope(scope));
|
|
482
|
+
const clis: IntegrationType[] = (
|
|
483
|
+
clisSel.includes(ALL_CLIS)
|
|
484
|
+
? [...INTEGRATION_TYPES]
|
|
485
|
+
: (clisSel.filter((v) => v !== ALL_CLIS) as IntegrationType[])
|
|
486
|
+
).filter((id) => supported.has(id));
|
|
296
487
|
|
|
297
488
|
// 3 — Which policies? Multi-select of themed presets — additive, so the
|
|
298
489
|
// enabled set is the union of every ticked bundle.
|
|
490
|
+
// Seed the Custom checkbox from whatever the config already says, so the
|
|
491
|
+
// wizard shows the current state rather than resetting it every run.
|
|
492
|
+
const customEnabledBefore = readHooksConfig().customPoliciesEnabled !== false;
|
|
493
|
+
const presetChoices = buildPresetChoices(cwd, customEnabledBefore);
|
|
494
|
+
const hasCustomFiles = describeCustomPolicies(cwd).fileCount > 0;
|
|
495
|
+
|
|
299
496
|
const presets = await multiSelect<string>({
|
|
300
497
|
message: "What should we guard against?",
|
|
301
|
-
choices:
|
|
498
|
+
choices: presetChoices,
|
|
302
499
|
minSelected: 1,
|
|
303
500
|
summaryNoun: "bundles",
|
|
304
501
|
hint: "space toggles · combine presets · ↵ confirm",
|
|
@@ -307,11 +504,14 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
307
504
|
});
|
|
308
505
|
if (presets === null) return cancel();
|
|
309
506
|
const policies = resolvePresetSelection(presets);
|
|
507
|
+
// Only meaningful when there are files to switch off; with none, the row is
|
|
508
|
+
// locked-unchecked and must not write a disabling flag.
|
|
509
|
+
const customEnabled = hasCustomFiles ? presets.includes(CUSTOM) : undefined;
|
|
310
510
|
|
|
311
511
|
// 4 — Review & apply
|
|
312
512
|
const decision = await selectOne<"apply" | "cancel">({
|
|
313
513
|
message: "Ready to apply?",
|
|
314
|
-
body: reviewLines({ scope, clis, policies, cwd }),
|
|
514
|
+
body: reviewLines({ scope, clis, policies, cwd, customEnabled }),
|
|
315
515
|
choices: [
|
|
316
516
|
{ label: "Yes, apply now", value: "apply", hint: "write the config" },
|
|
317
517
|
{ label: "Cancel", value: "cancel", hint: "quit, no changes" },
|
|
@@ -345,12 +545,28 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
345
545
|
clis,
|
|
346
546
|
{ replace: true, quiet: true },
|
|
347
547
|
);
|
|
548
|
+
setCustomPoliciesEnabled(scope, cwd, customEnabled);
|
|
348
549
|
await applied;
|
|
349
550
|
// Only now — a completed apply — is the launcher considered "seen", so the
|
|
350
551
|
// first-run bare invocation stops redirecting here and opens the dashboard.
|
|
351
552
|
markLauncherSeen();
|
|
352
553
|
|
|
353
|
-
|
|
354
|
-
|
|
554
|
+
// Keep this inside a standard 80-column terminal. `writeLines` truncates with
|
|
555
|
+
// a hard cut and no ellipsis, so an over-long line doesn't just lose its tail
|
|
556
|
+
// — it reads as broken output. Naming all ten CLIs took it to 182 characters;
|
|
557
|
+
// the count alone carries the same information, and the user picked them two
|
|
558
|
+
// screens ago.
|
|
559
|
+
const customNote =
|
|
560
|
+
customEnabled === true
|
|
561
|
+
? " + your custom policies"
|
|
562
|
+
: customEnabled === false
|
|
563
|
+
? " · custom policies DISABLED"
|
|
564
|
+
: "";
|
|
565
|
+
const assistants = `${clis.length} assistant${clis.length === 1 ? "" : "s"}`;
|
|
566
|
+
outro(
|
|
567
|
+
`Setup complete — ${policies.length} policies${customNote} · ${assistants}`,
|
|
568
|
+
{ ok: true },
|
|
569
|
+
stdout,
|
|
570
|
+
);
|
|
355
571
|
return { applied: true, scope, clis, policies };
|
|
356
572
|
}
|
|
@@ -27,6 +27,9 @@ const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
|
|
|
27
27
|
/** Regex matching convention policy filenames: *policies.{js,mjs,ts} */
|
|
28
28
|
const CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
|
|
29
29
|
|
|
30
|
+
/** Script extensions we could load, used to spot near-miss filenames. */
|
|
31
|
+
const LOADABLE_EXT_RE = /\.(js|mjs|ts)$/;
|
|
32
|
+
|
|
30
33
|
/**
|
|
31
34
|
* Scan a directory for convention policy files (*policies.{js,mjs,ts}).
|
|
32
35
|
* Returns sorted absolute paths. Returns [] if the directory doesn't exist.
|
|
@@ -44,6 +47,36 @@ export function discoverPolicyFiles(dir: string): string[] {
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Script files sitting in a policies directory that the convention will NOT
|
|
52
|
+
* load, because the name doesn't end in `policies.{js,mjs,ts}`.
|
|
53
|
+
*
|
|
54
|
+
* The failure this exists for is silent and total: a file in exactly the right
|
|
55
|
+
* directory, exporting exactly the right thing, named `block-foo.mjs` instead
|
|
56
|
+
* of `block-foo-policies.mjs`, is skipped with no warning — it looks installed
|
|
57
|
+
* and enforces nothing. This repo shipped `block-version-bumps.mjs` that way,
|
|
58
|
+
* so the guard written after a bad version bump had never once run. Callers
|
|
59
|
+
* surface these as a warning rather than loading them, since a file that opts
|
|
60
|
+
* out of the naming convention may well not be a policy file at all.
|
|
61
|
+
*/
|
|
62
|
+
export function findSkippedPolicyFiles(dir: string): string[] {
|
|
63
|
+
if (!existsSync(dir)) return [];
|
|
64
|
+
try {
|
|
65
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
66
|
+
.filter(
|
|
67
|
+
(e) =>
|
|
68
|
+
e.isFile() &&
|
|
69
|
+
LOADABLE_EXT_RE.test(e.name) &&
|
|
70
|
+
!CONVENTION_FILE_RE.test(e.name) &&
|
|
71
|
+
!e.name.endsWith(".d.ts"),
|
|
72
|
+
)
|
|
73
|
+
.map((e) => e.name)
|
|
74
|
+
.sort((a, b) => a.localeCompare(b));
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
47
80
|
/**
|
|
48
81
|
* Load a single policy file into the globalThis custom hooks registry.
|
|
49
82
|
* Does NOT clear the registry — caller is responsible for that.
|
|
@@ -135,9 +168,25 @@ export interface LoadAllResult {
|
|
|
135
168
|
* Each file is loaded independently (fail-open per file).
|
|
136
169
|
* Convention hooks are tagged with __conventionScope so the handler can build scoped prefixes.
|
|
137
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* Warn once per directory about script files the naming convention will skip.
|
|
173
|
+
* Fires on the hook path, so it must stay cheap (one readdir) and must never
|
|
174
|
+
* throw — a warning is not worth failing a tool call over.
|
|
175
|
+
*/
|
|
176
|
+
function warnSkippedPolicyFiles(dir: string, scope: "project" | "user"): void {
|
|
177
|
+
const skipped = findSkippedPolicyFiles(dir);
|
|
178
|
+
if (skipped.length === 0) return;
|
|
179
|
+
const suggest = (n: string) => n.replace(/\.(js|mjs|ts)$/, "-policies.$1");
|
|
180
|
+
hookLogWarn(
|
|
181
|
+
`${scope} policies: ${skipped.length} file(s) in ${dir} are NOT loaded — ` +
|
|
182
|
+
`the convention only loads names ending in "policies.js|mjs|ts". ` +
|
|
183
|
+
skipped.map((n) => `${n} → rename to ${suggest(n)}`).join("; "),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
138
187
|
export async function loadAllCustomHooks(
|
|
139
188
|
customPoliciesPath: string | undefined,
|
|
140
|
-
opts?: { sessionCwd?: string },
|
|
189
|
+
opts?: { sessionCwd?: string; customPoliciesEnabled?: boolean },
|
|
141
190
|
): Promise<LoadAllResult> {
|
|
142
191
|
clearCustomHooks();
|
|
143
192
|
|
|
@@ -145,6 +194,13 @@ export async function loadAllCustomHooks(
|
|
|
145
194
|
|
|
146
195
|
const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
|
|
147
196
|
|
|
197
|
+
// Convention discovery can be switched off from config without renaming or
|
|
198
|
+
// deleting anything. Absent means on, so dropping a file in works with no
|
|
199
|
+
// config at all and upgrades don't silently disable anyone's rules. An
|
|
200
|
+
// explicit `customPoliciesPath` is NOT gated by this: that one was named on
|
|
201
|
+
// purpose, so switching off *discovery* shouldn't silently drop it too.
|
|
202
|
+
const conventionEnabled = opts?.customPoliciesEnabled !== false;
|
|
203
|
+
|
|
148
204
|
// 1. Explicit customPoliciesPath (existing behavior)
|
|
149
205
|
if (customPoliciesPath) {
|
|
150
206
|
const absPath = isAbsolute(customPoliciesPath)
|
|
@@ -161,7 +217,8 @@ export async function loadAllCustomHooks(
|
|
|
161
217
|
|
|
162
218
|
// 2. Project convention: {projectRoot}/.failproofai/policies/*policies.{js,mjs,ts}
|
|
163
219
|
const projectDir = resolve(projectRoot, ".failproofai", "policies");
|
|
164
|
-
|
|
220
|
+
if (conventionEnabled) warnSkippedPolicyFiles(projectDir, "project");
|
|
221
|
+
const projectFiles = conventionEnabled ? discoverPolicyFiles(projectDir) : [];
|
|
165
222
|
for (const file of projectFiles) {
|
|
166
223
|
const hooksBefore = getCustomHooks().length;
|
|
167
224
|
await loadSingleFile(file, { conventionScope: "project" });
|
|
@@ -177,7 +234,8 @@ export async function loadAllCustomHooks(
|
|
|
177
234
|
|
|
178
235
|
// 3. User convention: ~/.failproofai/policies/*policies.{js,mjs,ts}
|
|
179
236
|
const userDir = resolve(homedir(), ".failproofai", "policies");
|
|
180
|
-
|
|
237
|
+
if (conventionEnabled) warnSkippedPolicyFiles(userDir, "user");
|
|
238
|
+
const userFiles = conventionEnabled ? discoverPolicyFiles(userDir) : [];
|
|
181
239
|
for (const file of userFiles) {
|
|
182
240
|
const hooksBefore = getCustomHooks().length;
|
|
183
241
|
await loadSingleFile(file, { conventionScope: "user" });
|
|
@@ -194,7 +252,15 @@ export async function loadAllCustomHooks(
|
|
|
194
252
|
const allHooks = getCustomHooks();
|
|
195
253
|
const conventionCount = allHooks.length - hooksBeforeConvention;
|
|
196
254
|
|
|
197
|
-
if (
|
|
255
|
+
if (!conventionEnabled) {
|
|
256
|
+
// Say so. Silently loading nothing is exactly the failure this feature set
|
|
257
|
+
// exists to remove — a user who forgot the flag would see their policies
|
|
258
|
+
// do nothing with no way to tell why.
|
|
259
|
+
hookLogInfo(
|
|
260
|
+
"convention policies: DISABLED via customPoliciesEnabled:false — " +
|
|
261
|
+
"no .failproofai/policies/ files loaded",
|
|
262
|
+
);
|
|
263
|
+
} else if (projectFiles.length > 0 || userFiles.length > 0) {
|
|
198
264
|
hookLogInfo(
|
|
199
265
|
`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`,
|
|
200
266
|
);
|
package/src/hooks/handler.ts
CHANGED
|
@@ -243,7 +243,10 @@ export async function handleHookEvent(
|
|
|
243
243
|
registerBuiltinPolicies(config.enabledPolicies);
|
|
244
244
|
|
|
245
245
|
// Load and register custom hooks (layer 2, after builtins)
|
|
246
|
-
const loadResult = await loadAllCustomHooks(config.customPoliciesPath, {
|
|
246
|
+
const loadResult = await loadAllCustomHooks(config.customPoliciesPath, {
|
|
247
|
+
sessionCwd: session.cwd,
|
|
248
|
+
customPoliciesEnabled: config.customPoliciesEnabled,
|
|
249
|
+
});
|
|
247
250
|
const customHooksList = loadResult.hooks;
|
|
248
251
|
const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
|
|
249
252
|
|
|
@@ -85,4 +85,12 @@ export interface HooksConfig {
|
|
|
85
85
|
llm?: LlmConfig;
|
|
86
86
|
policyParams?: Record<string, Record<string, unknown>>;
|
|
87
87
|
customPoliciesPath?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Turn off convention-discovered custom policies (`.failproofai/policies/`)
|
|
90
|
+
* without deleting or renaming the files. Absent means enabled — the default
|
|
91
|
+
* has to be "on" so that dropping a policy file in works with no config at
|
|
92
|
+
* all, and so existing installs don't lose their custom rules on upgrade.
|
|
93
|
+
* Only ever written explicitly as `false`.
|
|
94
|
+
*/
|
|
95
|
+
customPoliciesEnabled?: boolean;
|
|
88
96
|
}
|