failproofai 1.0.2-beta.6 → 1.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.
Files changed (122) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +15 -15
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +6 -5
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__1lt47k4._.js} +1 -1
  58. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +3 -3
  61. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +10 -0
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__128se8m._.js → [root-of-the-server]__1-wzs7f._.js} +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07gm3zl._.js → [root-of-the-server]__1n2ywfz._.js} +5 -22
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tag8_-._.js → [root-of-the-server]__1wlvpe5._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/{_11fc-hb._.js → _0l7zv8b._.js} +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/{_0no96tu._.js → _1h33neq._.js} +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +17 -0
  82. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +3 -0
  87. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  88. package/.next/standalone/.next/server/middleware.js +1 -1
  89. package/.next/standalone/.next/server/pages/404.html +1 -1
  90. package/.next/standalone/.next/server/pages/500.html +1 -1
  91. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  93. package/.next/standalone/.next/static/chunks/{0931avh-06lj5.js → 0i3a6wf-f_yky.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/{1id7o44jjegf0.js → 0u2ojcw25c_np.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{276n-edifojbd.js → 11a_m8yksthtf.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0sdgnflvw8kta.js → 1ch5l9t2p16y4.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{0h-3zffoy-_ji.js → 1n04wzsaes73g.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{2pxr7nr1nyotn.js → 2iryw37qfqy5y.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{2jfskwd2u8o5r.js → 34j7nmidlcuni.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{3vu_7_eor1m-k.js → 3927pu4ss_l4k.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{0p84npzxdgpc5.js → 3r0udrtpn78-e.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{1mfzsja6ftim5.css → 40nr6ehj4p3cw.css} +1 -1
  103. package/.next/standalone/.next/static/chunks/{0z7au3njlak0j.js → 416zumre3fvfw.js} +1 -1
  104. package/.next/standalone/package.json +9 -9
  105. package/.next/standalone/server.js +1 -1
  106. package/bin/failproofai.mjs +831 -679
  107. package/dist/cli.mjs +9461 -7620
  108. package/dist/worker.mjs +6 -3
  109. package/package.json +9 -9
  110. package/scripts/build-policy-pack.mjs +28 -4
  111. package/src/audit/cli.ts +40 -39
  112. package/src/hooks/backfill-cli.ts +1 -1
  113. package/src/hooks/configure-wizard.ts +24 -10
  114. package/src/hooks/manager.ts +41 -6
  115. package/src/hooks/pack-cli.ts +827 -97
  116. package/src/hooks/pack-manifest.ts +34 -1
  117. package/src/hooks/pack-store.ts +208 -16
  118. package/src/hooks/tui.ts +194 -40
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15r6r7w._.js +0 -10
  120. /package/.next/standalone/.next/static/{mAYN9BOxWVFREnVp3yCHp → NgD5HvuyPxwgrpeIiafJL}/_buildManifest.js +0 -0
  121. /package/.next/standalone/.next/static/{mAYN9BOxWVFREnVp3yCHp → NgD5HvuyPxwgrpeIiafJL}/_clientMiddlewareManifest.js +0 -0
  122. /package/.next/standalone/.next/static/{mAYN9BOxWVFREnVp3yCHp → NgD5HvuyPxwgrpeIiafJL}/_ssgManifest.js +0 -0
package/dist/worker.mjs CHANGED
@@ -2559,7 +2559,7 @@ var init_builtin_policies = __esm(() => {
2559
2559
  });
2560
2560
 
2561
2561
  // package.json
2562
- var version = "1.0.2-beta.6";
2562
+ var version = "1.0.2-beta.8";
2563
2563
  var init_package = () => {};
2564
2564
 
2565
2565
  // src/posthog-key.ts
@@ -8911,10 +8911,12 @@ function parsePackIdentity(value) {
8911
8911
  if (value.effect !== undefined && value.effect !== "enforce" && value.effect !== "observe") {
8912
8912
  throw new Error(`unknown effect ${JSON.stringify(value.effect)} for pack ${value.id}`);
8913
8913
  }
8914
+ const commit = typeof value.commit === "string" && PACK_COMMIT_RE.test(value.commit.trim().toLowerCase()) ? value.commit.trim().toLowerCase() : undefined;
8914
8915
  return {
8915
8916
  id: value.id,
8916
8917
  version: value.version,
8917
- effect: value.effect ?? "enforce"
8918
+ effect: value.effect ?? "enforce",
8919
+ ...commit ? { commit } : {}
8918
8920
  };
8919
8921
  }
8920
8922
  function hasInstalledPacks() {
@@ -9077,7 +9079,7 @@ function safeDeclared(raw) {
9077
9079
  function errText(err) {
9078
9080
  return err instanceof Error ? err.message : String(err);
9079
9081
  }
9080
- var ACCEPTED_PACK_SCHEMA_VERSIONS, SHA256_RE2, PACK_ID_RE, PACK_VERSION_RE, PACK_POLICY_NAME_RE;
9082
+ var ACCEPTED_PACK_SCHEMA_VERSIONS, SHA256_RE2, PACK_ID_RE, PACK_VERSION_RE, PACK_COMMIT_RE, PACK_POLICY_NAME_RE;
9081
9083
  var init_pack_manifest = __esm(() => {
9082
9084
  init_fp_home();
9083
9085
  init_cloud_managed_policies();
@@ -9085,6 +9087,7 @@ var init_pack_manifest = __esm(() => {
9085
9087
  SHA256_RE2 = /^[a-f0-9]{64}$/;
9086
9088
  PACK_ID_RE = /^[A-Za-z0-9._-]{1,64}\/[A-Za-z0-9._-]{1,64}$/;
9087
9089
  PACK_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
9090
+ PACK_COMMIT_RE = /^[0-9a-f]{7,40}$/;
9088
9091
  PACK_POLICY_NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
9089
9092
  });
9090
9093
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.2-beta.6",
3
+ "version": "1.0.2-beta.8",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -120,15 +120,15 @@
120
120
  "sharp": "0.35.0"
121
121
  },
122
122
  "optionalDependencies": {
123
- "@failproofai/failproofaid-linux-x64": "1.0.2-beta.6",
124
- "@failproofai/failproofaid-linux-arm64": "1.0.2-beta.6",
125
- "@failproofai/failproofaid-darwin-x64": "1.0.2-beta.6",
126
- "@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.6"
123
+ "@failproofai/failproofaid-linux-x64": "1.0.2-beta.8",
124
+ "@failproofai/failproofaid-linux-arm64": "1.0.2-beta.8",
125
+ "@failproofai/failproofaid-darwin-x64": "1.0.2-beta.8",
126
+ "@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.8"
127
127
  },
128
128
  "failproofaidBinaries": {
129
- "linux-x64": "1e883d50ea40dad46394a137fab9b8ecddb7b37064312af9ddf89cb3de5eb7b8",
130
- "linux-arm64": "47a51a75a01c021417a4d67fb7eb5caa8fa9c46abca5ec85d23e8ddead1ebfdd",
131
- "darwin-x64": "bbc4e714c1e961d53d7dd6c65b84dcc74b4e24439349ebdef3f7d8681ece1776",
132
- "darwin-arm64": "ca6f6e688070a2d2a63fd333f7c3ac7c7d2bd991ca499cfced23b249e7ca684b"
129
+ "linux-x64": "7b49023fd9351ccdddc066f9560d998e6b582b763ab4ad4253831a2548e99ae1",
130
+ "linux-arm64": "b43af87cb0ca7e83bfbc053051ef4627ce9594cb7b6a520492689f750302adc1",
131
+ "darwin-x64": "c03d157deddf78d1d7928aa6d33909e1ff585eace77d5d25090c56c7eb2a9983",
132
+ "darwin-arm64": "a2489beaf7e580a3e012a45d346c8df5764f426f460c0c6fb9bd7b405ac65366"
133
133
  }
134
134
  }
@@ -37,6 +37,9 @@ import { createHash } from "node:crypto";
37
37
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
38
38
  import { dirname, join, resolve } from "node:path";
39
39
  import { fileURLToPath } from "node:url";
40
+ // The one place the pack's id is decided. Imported rather than restated — see
41
+ // PACK_ID below for what restating it cost.
42
+ import { CORE_SOURCE } from "../src/hooks/pack-store.ts";
40
43
 
41
44
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
42
45
  // `--out <dir>` lets the conformance test generate into a temp directory rather
@@ -47,10 +50,17 @@ const outArg = process.argv.indexOf("--out");
47
50
  const OUT_DIR = outArg !== -1 && process.argv[outArg + 1]
48
51
  ? resolve(process.argv[outArg + 1])
49
52
  : join(ROOT, "policy-pack");
50
- // Not `failproofai/builtins`. These stop being builtins the moment they ship as
51
- // a pack, and an id is the most durable place a retired word can hide — it ends
52
- // up in every machine's installed.json and in the listing every user reads.
53
- const PACK_ID = "failproofai/core";
53
+ // Not restated. The id is the most durable place a retired word can hide — it
54
+ // ends up in every machine's installed.json and in the listing every user reads
55
+ // and restating it here is how it hid twice: `failproofai/builtins` shipped
56
+ // first, was corrected to `failproofai/core` HERE and nowhere else, and then
57
+ // `core` was retired in pack-store while this line went on saying it. The one
58
+ // published release still declares `failproofai/builtins`, so a machine that
59
+ // installed it cannot be addressed by either name anybody would type.
60
+ //
61
+ // Read from the layer that owns the set, so the next rename reaches this file
62
+ // whether or not somebody remembers it exists.
63
+ const PACK_ID = CORE_SOURCE;
54
64
  const ENTRY_ASSET = "failproofai-pack.mjs";
55
65
  const MANIFEST_ASSET = "failproofai-pack.json";
56
66
  const CHECKSUMS_ASSET = "SHA256SUMS";
@@ -60,6 +70,17 @@ const version = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).ver
60
70
  // A generated entry that registers every non-alwaysOn builtin through the public
61
71
  // policy API — the same API a third-party pack uses. `failproofai` stays external
62
72
  // so the loader's shim resolves it, exactly as it does for any other pack.
73
+ //
74
+ // It registers `category` and `defaultEnabled` as well as name/description/
75
+ // match/fn, and that is not decoration. This script builds ITS manifest straight
76
+ // from POLICY_CATALOG, so omitting them from the ENTRY looked harmless — and was
77
+ // not: the entry IS the artifact, and anything that rebuilds a manifest from it
78
+ // saw 38 policies filed under "General" with none on by default. `failproofai
79
+ // publish` does exactly that, which is how a release that would have enforced
80
+ // nothing on a default install got as far as a dry run.
81
+ //
82
+ // Note for whoever edits the template below: it is a template literal, so a
83
+ // backtick inside it ends the string. Comments in there stay backtick-free.
63
84
  const entrySource = `
64
85
  import { customPolicies } from "failproofai";
65
86
  import { BUILTIN_POLICIES } from ${JSON.stringify(join(ROOT, "src/hooks/builtin-policies.ts"))};
@@ -69,6 +90,9 @@ for (const policy of BUILTIN_POLICIES) {
69
90
  customPolicies.add({
70
91
  name: policy.name,
71
92
  description: policy.description,
93
+ // Both fields matter here; see the note above entrySource.
94
+ category: policy.category,
95
+ defaultEnabled: policy.defaultEnabled === true,
72
96
  match: policy.match,
73
97
  fn: policy.fn,
74
98
  });
package/src/audit/cli.ts CHANGED
@@ -35,7 +35,8 @@ import { getInstanceId } from "../../lib/telemetry-id";
35
35
  import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
36
36
  import { openWhenReady } from "./open-browser";
37
37
  import { describeOutcome, reportHarm } from "./report-harm";
38
- import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM } from "../hooks/tui";
38
+ import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM, helpScreen, helpOptsFor } from "../hooks/tui";
39
+ import { version } from "../../package.json";
39
40
 
40
41
  /** Port the bundled dashboard binds to. Matches `scripts/launch.ts`'s default
41
42
  * for `start` mode, which `failproofai` (bare) already uses. */
@@ -64,9 +65,6 @@ export const AUDIT_STAGES: ReadonlyArray<{ label: string; detail: string }> = [
64
65
  { label: "aggregating results", detail: "counting hits, ranking by frequency" },
65
66
  ];
66
67
 
67
- /** Column the description text starts at. The widest command line is 37 wide. */
68
- const HELP_DESC_COL = 40;
69
-
70
68
  /**
71
69
  * `audit --help`.
72
70
  *
@@ -82,41 +80,44 @@ const HELP_DESC_COL = 40;
82
80
  * one. It still works, and still refuses every argument it always refused.
83
81
  */
84
82
  export function helpText(): string {
85
- const row = (command: string, lines: string[]): string => {
86
- // Padding is measured on the RAW command `c()` adds escape bytes that
87
- // occupy no columns, so colouring first would misalign every row.
88
- const pad = " ".repeat(Math.max(1, HELP_DESC_COL - 2 - command.length));
89
- return lines
90
- .map((line, i) =>
91
- i === 0 ? ` ${c(CYAN, command)}${pad}${line}` : `${" ".repeat(HELP_DESC_COL)}${line}`,
92
- )
93
- .join("\n");
94
- };
95
-
96
- return [
97
- `${c(BOLD, "failproofai audit")} — review your agent CLIs for risky and wasteful patterns.`,
98
- c(DIM, "Everything runs on this machine; only a scheduled digest ever leaves it."),
99
- "",
100
- c(BOLD, "USAGE"),
101
- row("failproofai audit", [
102
- "Scan your session history, then open",
103
- `http://localhost:${DASHBOARD_PORT}/audit`,
104
- ]),
105
- row("failproofai audit --schedule [days]", [
106
- "Scan on a timer and email the findings.",
107
- "Default 7 days, range 1-90.",
108
- "Signs you in the first time; add",
109
- "--email <address> to skip a prompt.",
110
- ]),
111
- row("failproofai audit --no-schedule", ["Stop the timer. Leaves you signed in."]),
112
- row("failproofai audit --status", [
113
- "Whether scheduling is on, where reports",
114
- "go, the daemon's state, and when the",
115
- "next scan is due.",
116
- ]),
117
- row("failproofai audit -h, --help", ["Show this help."]),
118
- "",
119
- ].join("\n");
83
+ // Drawn by the same renderer as `config --help` and `policies --help`, from
84
+ // the same kit `audit`'s output already uses for its palette. It used to
85
+ // build its own rows against a hand-set HELP_DESC_COL, which is how `audit`
86
+ // and `config` ended up with their descriptions in different columns.
87
+ //
88
+ // The `failproofai audit` prefix is dropped from the rows: the heading two
89
+ // lines above already carries it, and repeating it cost 17 of the 80 columns
90
+ // on every row — which is what forced the four-word-per-line descriptions.
91
+ const lines = helpScreen(
92
+ {
93
+ command: "audit",
94
+ version,
95
+ tagline: "review your agent CLIs for risky and wasteful patterns",
96
+ sections: [
97
+ {
98
+ label: "usage",
99
+ entries: [
100
+ ["(bare)", `Scan your session history, then open http://localhost:${DASHBOARD_PORT}/audit`],
101
+ ["--schedule [days]", "Scan on a timer and email the findings. Default 7 days, range 1-90. Signs you in the first time."],
102
+ // Its own row now, rather than a clause inside --schedule's. It
103
+ // does not stand alone, which is why it used to be a clause — but
104
+ // a clause wraps, and `--email <address>` landing with the flag at
105
+ // the end of one line and its placeholder at the start of the next
106
+ // is not a flag anybody can read or copy.
107
+ ["--email <address>", "With --schedule, skips the sign-in prompt."],
108
+ ["--no-schedule", "Stop the timer. Leaves you signed in."],
109
+ ["--status", "Whether scheduling is on, where reports go, the daemon's state, and when the next scan is due."],
110
+ ["-h, --help", "Show this help."],
111
+ ],
112
+ },
113
+ ],
114
+ footer: ["Everything runs on this machine; only a scheduled digest ever leaves it."],
115
+ },
116
+ helpOptsFor(process.stdout),
117
+ );
118
+ // The margins `printBlock` would add, spelled out because this one returns
119
+ // its text for a caller to write rather than writing it.
120
+ return ["", ...lines, ""].join("\n") + "\n";
120
121
  }
121
122
 
122
123
  // ── ANSI helpers ────────────────────────────────────────────────────────────
@@ -149,7 +149,7 @@ export function runBackfillCommand(opts: BackfillOptions = {}): BackfillResult {
149
149
  exitCode: 1,
150
150
  lines: [
151
151
  "This machine is not connected, so there is nowhere to send history.",
152
- "Connect first: `failproofai config --connect <url> --token <key>`.",
152
+ "Connect first: `failproofai config --token <key>`.",
153
153
  ],
154
154
  };
155
155
  }
@@ -743,6 +743,16 @@ export async function runConfigureWizard(
743
743
  // reaching this function, so setup never runs off the back of some other
744
744
  // command on a headless box.
745
745
  const unattended = !stdin.isTTY || !stdout.isTTY;
746
+ // A key on the command line settles the whole run, not just the question it
747
+ // literally answers.
748
+ //
749
+ // Somebody typing `failproofai config --token <key>` has said: set this
750
+ // machine up, connect it, send its data. There is nothing left to confirm —
751
+ // so asking "Connect to Cloud?", then the key, then "Ready to apply?" made
752
+ // the flag look like it had not been read. The only thing still worth
753
+ // stopping for is the sudo password, which is a CREDENTIAL rather than a
754
+ // question, and no flag can supply it.
755
+ const preAnswered = Boolean(answers.token);
746
756
 
747
757
 
748
758
  // Running the wizard itself under sudo configures the WRONG ACCOUNT, and
@@ -1020,9 +1030,15 @@ export async function runConfigureWizard(
1020
1030
  // one — so an unattended run with a token skips straight past the question,
1021
1031
  // and one without a token stays local, which is the same "Not now" branch a
1022
1032
  // person picks. Neither needs a separate flag to say so.
1023
- const choice = unattended
1024
- ? (answers.token ? "key" : "local")
1025
- : await selectOne<"key" | "local">({
1033
+ const choice = answers.token
1034
+ // A key on the command line IS the answer to this question. Gating it on
1035
+ // there being no terminal was wrong: `--token` means "use this key", not
1036
+ // "use this key only if nobody is watching", and a run that asked anyway
1037
+ // made the flag look broken to the person who had just typed it.
1038
+ ? "key"
1039
+ : unattended
1040
+ ? "local"
1041
+ : await selectOne<"key" | "local">({
1026
1042
  message: "Connect this machine to FailproofAI Cloud?",
1027
1043
  // The hint says what you GET; the body says what LEAVES.
1028
1044
  //
@@ -1186,12 +1202,10 @@ export async function runConfigureWizard(
1186
1202
  }
1187
1203
  }
1188
1204
 
1189
- if (token === null && unattended) {
1190
- // Supplied on the command line, so nothing is asked and nothing is
1191
- // echoed. Reaching here without one cannot happen an unattended run
1192
- // only takes the "key" branch BECAUSE a token was given.
1193
- token = answers.token ?? null;
1194
- }
1205
+ // Supplied on the command line, so nothing is asked and nothing is
1206
+ // echoed terminal or not. Prompting for a value the caller already gave
1207
+ // is the failure this whole flag exists to avoid.
1208
+ if (token === null) token = answers.token ?? null;
1195
1209
  if (token === null) {
1196
1210
  token = await promptText({
1197
1211
  // The destination is in the question now that it is no longer a
@@ -1255,7 +1269,7 @@ export async function runConfigureWizard(
1255
1269
  // NOT the same call as `uninstall`, which does require --yes: that one
1256
1270
  // REMOVES things, so silence there would destroy on a signal as weak as a
1257
1271
  // missing terminal. This one does exactly what its name says.
1258
- const decision = unattended ? "apply" : await selectOne<"apply" | "cancel">({
1272
+ const decision = unattended || preAnswered ? "apply" : await selectOne<"apply" | "cancel">({
1259
1273
  message: "Ready to apply?",
1260
1274
  body: reviewLines({
1261
1275
  target,
@@ -15,7 +15,6 @@ import { INTEGRATION_TYPES,
15
15
  type IntegrationType,
16
16
  } from "./types";
17
17
  import { claudeCode, getIntegration, settingsPathsFor, type Integration } from "./integrations";
18
- import { promptPolicySelection } from "./install-prompt";
19
18
  import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
20
19
  import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
21
20
  import { BUILTIN_POLICIES } from "./builtin-policies";
@@ -462,9 +461,25 @@ async function installHooksImpl(
462
461
  ? [...new Set(incoming)]
463
462
  : [...new Set([...previousConfig.enabledPolicies, ...incoming])];
464
463
  } else {
465
- // Interactive pre-load current config if it exists
466
- const preSelected = previousConfig.enabledPolicies.length > 0 ? previousConfig.enabledPolicies : undefined;
467
- selectedPolicies = await promptPolicySelection(preSelected, { includeBeta });
464
+ // NOT a policy picker. `--install` wires hooks; choosing what they enforce
465
+ // is `policies add` / `policies remove`.
466
+ //
467
+ // It used to open a second picker here, over `BUILTIN_POLICIES` — the
468
+ // COMPILED catalog, which is no longer how policies arrive. So it offered a
469
+ // list that did not match what was installed, in an older prompt engine
470
+ // that looks nothing like the rest, and wrote its answer to
471
+ // `enabledPolicies` while every pack records its selection in
472
+ // `installed.json`.
473
+ //
474
+ // Two independent enabled-sets, and this one silently overwrote the other:
475
+ // `policies remove block-env-files` followed by `policies --install` put
476
+ // block-env-files straight back on, because the picker pre-ticked from the
477
+ // legacy key and saved all of it again. A command whose job is wiring must
478
+ // not undo a policy decision made somewhere else.
479
+ //
480
+ // Whatever was enabled stays enabled. Explicit names still work
481
+ // (`--install block-sudo`) for the migration shim, which has only this key.
482
+ selectedPolicies = previousConfig.enabledPolicies;
468
483
  }
469
484
 
470
485
  // Preserve existing config fields when updating. New writes use the plural
@@ -556,7 +571,21 @@ async function installHooksImpl(
556
571
  // pack does not carry it and asking for it by name is a selection the pack
557
572
  // cannot satisfy. It ships compiled in and registers regardless.
558
573
  const alwaysOnNames = new Set(BUILTIN_POLICIES.filter((p) => p.alwaysOn).map((p) => p.name));
559
- const fromPack = selectedPolicies.filter((name) => !alwaysOnNames.has(name));
574
+ // ONLY names the caller actually asked for.
575
+ //
576
+ // This used to take whatever sat in `enabledPolicies` and switch those names
577
+ // on in the pack. It is additive — it can turn a policy on and never off — so
578
+ // a stale entry in that key resurrected a policy the user had deliberately
579
+ // removed, on the next `policies --install`, with no mention of it.
580
+ // `remove block-env-files` then `--install` put it straight back.
581
+ //
582
+ // And the key goes stale by design now: nothing writes pack selections there,
583
+ // so anything left in it is a leftover from before packs, kept only for the
584
+ // migration shim. Re-applying a leftover as if it were a decision is how a
585
+ // machine ends up enforcing something its owner switched off.
586
+ const fromPack = policyNames === undefined
587
+ ? []
588
+ : selectedPolicies.filter((name) => !alwaysOnNames.has(name));
560
589
  if (fromPack.length > 0) {
561
590
  // ONLY when there is no pack yet. Fetching the core pack with
562
591
  // `only: <selection>` REPLACES whatever the machine had chosen, and
@@ -592,7 +621,13 @@ async function installHooksImpl(
592
621
  }
593
622
  }
594
623
  }
595
- console.log(`\nEnabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}\n`);
624
+ // Only when this run actually changed something. `--install` with no names
625
+ // wires hooks and touches no policy, and announcing "Enabled 0 policy(ies):"
626
+ // with an empty list described work it had not done — or worse, re-stated a
627
+ // stale key as though it were this run's decision.
628
+ if (policyNames !== undefined && selectedPolicies.length > 0) {
629
+ console.log(`\nEnabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}\n`);
630
+ }
596
631
  if (removeCustomHooks) {
597
632
  console.log("Custom hooks path cleared.");
598
633
  } else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {