failproofai 1.0.2-beta.0 → 1.0.2-beta.2

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 (133) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  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/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/index.html +1 -1
  36. package/.next/standalone/.next/server/app/index.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  38. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  63. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +14 -14
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__090sxxw._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_0wb8osu._.js +23 -0
  82. package/.next/standalone/.next/server/chunks/ssr/{_12l4a_k._.js → _1oggjel._.js} +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  89. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  90. package/.next/standalone/.next/server/pages/404.html +1 -1
  91. package/.next/standalone/.next/server/pages/500.html +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  94. package/.next/standalone/.next/static/chunks/{3y54nw0i7dsjl.js → 0nnbtp0w4sxrr.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{0sa9v5mhc8861.js → 0ykd_go4oahan.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0eqky-7n7dgdm.js → 140ip21rqmelt.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2i2yt3hzjredm.js → 1da5a_a-3btas.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 1fac8krph-zgp.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{2hs7ycvsav7v2.js → 1nzy5jc_vf9e0.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{1sl2xmn9inx3y.js → 1yw71gn8vqco1.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{1nuqytbta44af.js → 29eh4k77otcw6.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{2aawsjld9ied_.js → 2axi5ehjbld9h.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/{2gn7ut4svofm2.js → 2fzxl7n717pbc.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{35xg6retpe6rp.js → 3jjmdqqzem21x.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{turbopack-3kjgdgwci4tqf.js → turbopack-2vqpa-dts1gy4.js} +1 -1
  106. package/.next/standalone/app/actions/pack-actions.ts +21 -11
  107. package/.next/standalone/app/policies/hooks-client.tsx +3 -4
  108. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
  109. package/.next/standalone/package.json +10 -11
  110. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  111. package/.next/standalone/server.js +1 -1
  112. package/bin/failproofai.mjs +455 -316
  113. package/dist/cli.mjs +2374 -1873
  114. package/dist/worker.mjs +3 -13
  115. package/package.json +10 -11
  116. package/src/hooks/configure-wizard.ts +67 -444
  117. package/src/hooks/fp-reset.ts +10 -19
  118. package/src/hooks/handler.ts +2 -2
  119. package/src/hooks/manager.ts +58 -24
  120. package/src/hooks/pack-cli.ts +488 -43
  121. package/src/hooks/pack-failclosed.ts +1 -1
  122. package/src/hooks/pack-manifest.ts +14 -6
  123. package/src/hooks/pack-store.ts +89 -128
  124. package/src/hooks/tui.ts +150 -48
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zig5sv._.js +0 -3
  126. package/.next/standalone/.next/server/chunks/ssr/_00e9tnf._.js +0 -23
  127. package/policy-pack/SHA256SUMS +0 -2
  128. package/policy-pack/failproofai-pack.json +0 -732
  129. package/policy-pack/failproofai-pack.mjs +0 -2017
  130. package/src/hooks/policy-presets.ts +0 -137
  131. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_buildManifest.js +0 -0
  132. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_clientMiddlewareManifest.js +0 -0
  133. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_ssgManifest.js +0 -0
@@ -33,7 +33,6 @@ import { dirname, resolve, sep } from "node:path";
33
33
  import {
34
34
  selectOne,
35
35
  multiSelect,
36
- BACK,
37
36
  promptText,
38
37
  intro,
39
38
  outro,
@@ -54,8 +53,7 @@ import {
54
53
  } from "./integrations";
55
54
  import { INTEGRATION_TYPES, type IntegrationType, type HookScope } from "./types";
56
55
  import { installHooks } from "./manager";
57
- import { getConfigPathForScope, readHooksConfig, readScopedHooksConfig } from "./hooks-config";
58
- import { POLICY_PRESETS, resolvePreset, resolveEverything, RECOMMENDED_POLICIES } from "./policy-presets";
56
+ import { getConfigPathForScope, readScopedHooksConfig } from "./hooks-config";
59
57
  import { discoverPolicyFiles, findSkippedPolicyFiles } from "./custom-hooks-loader";
60
58
  import { trackHookEvent } from "./hook-telemetry";
61
59
  import { getInstanceId } from "../../lib/telemetry-id";
@@ -91,7 +89,6 @@ import {
91
89
  import {
92
90
  detectSetupState,
93
91
  isConfigured,
94
- buildTargetChoices,
95
92
  scopesFor,
96
93
  type SetupTarget,
97
94
  } from "./setup-state";
@@ -226,175 +223,7 @@ export function buildAgentChoices(scope: HookScope, cwd: string): MultiChoice<In
226
223
  });
227
224
  }
228
225
 
229
- const EVERYTHING = "__everything__";
230
226
  const ALL_CLIS = "__all_clis__";
231
- /** Sentinel for the locked "Custom" row — informational, never resolves to
232
- * builtin policy names (custom policies load by convention, not by config). */
233
- const CUSTOM = "__custom__";
234
- /**
235
- * Sentinel for the locked "enabled individually" row.
236
- *
237
- * Policies enabled one at a time (`failproofai policies add <name>`) need not map
238
- * onto any preset, so seeding the preset boxes cannot represent them. The wizard
239
- * writes with `replace: true`, which makes the ticked set the WHOLE enabled set —
240
- * so anything this row stands for must be unioned back in, or confirming the
241
- * wizard would silently drop it. Locked and pre-checked, because it reports a
242
- * state rather than offering a choice.
243
- */
244
- const INDIVIDUAL = "__individual__";
245
-
246
- /**
247
- * Split what is enabled now into the bundles that cover it and the leftovers.
248
- *
249
- * A pure function, and the SINGLE definition of that split — `buildPresetChoices`
250
- * renders it and the wizard writes from it, so the row the user sees and the set
251
- * that gets written can never disagree. The first version of this derived the
252
- * leftovers by parsing them back out of the row's hint text, which coupled a
253
- * display string to enforcement behaviour and would have broken on any policy
254
- * name containing the separator.
255
- */
256
- export function splitEnabled(currentlyEnabled: readonly string[] = []): {
257
- /** Preset ids (or `EVERYTHING`) whose policies are all already enabled. */
258
- presets: string[];
259
- /** Enabled policies no ticked bundle accounts for. */
260
- individual: string[];
261
- } {
262
- const current = new Set(currentlyEnabled);
263
- // A bundle is ticked when everything it turns on is already on. Not "any", or
264
- // one shared policy would tick every bundle containing it and confirming would
265
- // enable all of them.
266
- const isOn = (policies: string[]) =>
267
- policies.length > 0 && policies.every((name) => current.has(name));
268
-
269
- const everything = resolveEverything();
270
- const presets = isOn(everything)
271
- ? [EVERYTHING]
272
- : POLICY_PRESETS.filter((p) => isOn(resolvePreset(p.id))).map((p) => p.id);
273
-
274
- // Against the TICKED bundles, not all of them: a policy belonging only to a
275
- // bundle the user has NOT enabled is still enabled, and that is the fact the
276
- // locked row exists to make visible.
277
- const accounted = new Set(
278
- presets.flatMap((id) => (id === EVERYTHING ? everything : resolvePreset(id))),
279
- );
280
- const individual = [...current].filter((name) => !accounted.has(name)).sort();
281
- return { presets, individual };
282
- }
283
-
284
- /** The themed preset bundles for the wizard's multi-select, plus an "Everything"
285
- * option that enables the full builtin policy set. */
286
- export function buildPresetChoices(
287
- cwd: string = process.cwd(),
288
- enabled = true,
289
- /**
290
- * What is enabled at this scope RIGHT NOW, used to tick the boxes.
291
- *
292
- * Without it every row rendered unticked on every run while the wizard wrote
293
- * with `replace: true` — so re-running setup showed a blank slate and then made
294
- * that blank slate authoritative, discarding the user's selection with nothing
295
- * on screen to say it had happened. The comment on the Custom row below has
296
- * always described the intended behaviour ("shows the current state rather than
297
- * resetting it every run"); it was implemented for that one row out of eight.
298
- *
299
- * Optional so the first-run call sites stay unchanged: an empty set ticks
300
- * nothing, which is the correct rendering for a machine with no selection.
301
- */
302
- currentlyEnabled: readonly string[] = [],
303
- ) {
304
- const { presets: onPresets, individual } = splitEnabled(currentlyEnabled);
305
- const on = new Set(onPresets);
306
-
307
- const choices: MultiChoice<string>[] = POLICY_PRESETS.map((p) => ({
308
- label: p.label,
309
- value: p.id,
310
- hint: p.description,
311
- checked: on.has(p.id),
312
- }));
313
- choices.push({
314
- label: "Everything",
315
- value: EVERYTHING,
316
- hint: `all ${resolveEverything().length} policies`,
317
- checked: on.has(EVERYTHING),
318
- });
319
-
320
- // The Custom row is ALWAYS present, because it is the only place the feature
321
- // is discoverable: a user who has never written a policy cannot learn the
322
- // capability exists, and one who wrote a badly-named file cannot learn why
323
- // nothing happened.
324
- //
325
- // When there are loadable files it is a REAL checkbox — unticking writes
326
- // `customPoliciesEnabled: false`, which switches convention discovery off
327
- // without renaming or deleting anything. With nothing to toggle (no files,
328
- // or only skipped ones) it falls back to a locked status row.
329
- const custom = describeCustomPolicies(cwd);
330
- const skipped = custom.warnings.length;
331
- const plural = (n: number) => `${n} file${n === 1 ? "" : "s"}`;
332
- const skippedNote = skipped > 0 ? ` · ${skipped} skipped, see next screen` : "";
333
-
334
- if (custom.fileCount > 0) {
335
- choices.push({
336
- label: "Custom",
337
- value: CUSTOM,
338
- checked: enabled,
339
- // Deliberately NOT summaryExclude'd: this one is a real choice, and the
340
- // step summary is the only place the user sees what they picked. Hiding
341
- // it meant unticking Custom and every bundle showed "none", giving no
342
- // way to tell the toggle had registered.
343
- hint: `${plural(custom.fileCount)} in ${custom.scopes.join(" + ")}${skippedNote}`,
344
- });
345
- } else {
346
- choices.push({
347
- label: "Custom",
348
- value: CUSTOM,
349
- locked: true,
350
- checked: false,
351
- summaryExclude: true,
352
- hint:
353
- skipped > 0
354
- ? `${plural(skipped)} found but NOT loaded — see next screen`
355
- : "none yet · drop *-policies.mjs in .failproofai/policies/",
356
- });
357
- }
358
- if (individual.length > 0) {
359
- choices.push({
360
- label: `${individual.length} enabled individually`,
361
- value: INDIVIDUAL,
362
- locked: true,
363
- hint: `kept as-is · ${individual.join(", ")}`,
364
- // Not one of the bundles being counted, like the Everything and Custom rows.
365
- summaryExclude: true,
366
- });
367
- }
368
- return choices;
369
- }
370
-
371
-
372
- /**
373
- * Resolve the ticked options to a concrete policy set. Presets are additive —
374
- * the deduped union of every selected preset's policies — while "Everything"
375
- * enables the full policy set and wins over any presets.
376
- */
377
- export function resolvePresetSelection(
378
- values: string[],
379
- /**
380
- * What the locked "enabled individually" row stands for. Unioned in whenever
381
- * that row is present, INCLUDING under "Everything": `resolveEverything()`
382
- * covers the non-beta builtins only, so a beta policy someone enabled by hand
383
- * would otherwise be dropped by the very branch meant to enable everything.
384
- */
385
- individual: readonly string[] = [],
386
- ): string[] {
387
- // The Custom row is informational — custom policies are discovered from disk
388
- // by the loader, never named in the enabled-policies config — so it must not
389
- // reach resolvePreset(), which only knows builtin bundle ids. Same for the
390
- // locked individually-enabled row, which carries its policies in `individual`.
391
- const selected = values.filter((v) => v !== CUSTOM && v !== INDIVIDUAL);
392
- const carried = values.includes(INDIVIDUAL) ? individual : [];
393
- if (selected.includes(EVERYTHING)) {
394
- return [...new Set([...resolveEverything(), ...carried])];
395
- }
396
- return [...new Set([...selected.flatMap((id) => resolvePreset(id)), ...carried])];
397
- }
398
227
 
399
228
  const DIM_NOTE = "(auto-loaded)";
400
229
 
@@ -528,15 +357,6 @@ export function buildCompletionSummary(
528
357
  customEnabled: boolean | undefined,
529
358
  daemonInstalled: boolean,
530
359
  connected: boolean,
531
- /**
532
- * What was ticked on the policy step, so the summary can NAME the bundles.
533
- *
534
- * "9 policies" is a number the user cannot check and did not choose — they
535
- * picked two named bundles two screens earlier, and the line that confirms
536
- * their setup should say which. Optional so the existing callers and tests
537
- * that only have a count keep working and keep the old wording.
538
- */
539
- presetValues?: readonly string[],
540
360
  ): string {
541
361
  const extras: string[] = [];
542
362
  if (customEnabled === true) extras.push("custom");
@@ -554,9 +374,7 @@ export function buildCompletionSummary(
554
374
  // full line will not fit, which is checked rather than guessed at: the extras
555
375
  // clause grows too ("custom, daemon, reporting" is 25 characters), so a names
556
376
  // budget alone was wrong for exactly the combinations that need it most.
557
- const named = line(describeSelection(policiesCount, presetValues));
558
- if (named.length <= MAX_SUMMARY_COLUMNS) return named;
559
- return line(describeSelection(policiesCount, undefined));
377
+ return line(describeSelection(policiesCount));
560
378
  }
561
379
 
562
380
  /**
@@ -603,43 +421,13 @@ export function policyNamesLine(names: string[]): string[] {
603
421
  return [];
604
422
  }
605
423
 
606
- /**
607
- * Name the bundles rather than counting the policies inside them.
608
- *
609
- * BOUNDED AT TWO NAMES ON PURPOSE. `writeLines` truncates with a hard cut and no
610
- * ellipsis, so an over-long line does not lose its tail, it reads as broken
611
- * output — the same constraint that stopped this summary naming all twelve CLIs.
612
- * All four bundle labels joined is 57 characters, which with the prefix, the
613
- * harness clause and the extras clause runs to about 106. Two names plus a count
614
- * of the rest stays inside 80 for every combination, and two is also the common
615
- * case, so most runs see every name.
616
- *
617
- * Falls back to the old "N policies" when nothing maps to a bundle — a machine
618
- * whose policies were all enabled one at a time with `policies add` has no bundle
619
- * to name, and inventing one would be worse than the count.
620
- */
621
- function describeSelection(policiesCount: number, presetValues?: readonly string[]): string {
622
- const plural = `${policiesCount} polic${policiesCount === 1 ? "y" : "ies"}`;
623
- if (!presetValues) return plural;
624
-
625
- // "Everything" is one name for the whole set, and the count is the useful half
626
- // of it — "Everything" alone does not say how much that is.
627
- if (presetValues.includes(EVERYTHING)) return `Everything (${plural})`;
628
-
629
- const named = POLICY_PRESETS.filter((p) => presetValues.includes(p.id)).map((p) => p.label);
630
- // The locked "enabled individually" row stands for policies outside every
631
- // bundle, so it is counted among the unnamed rest rather than named.
632
- const individual = presetValues.includes(INDIVIDUAL) ? 1 : 0;
633
- if (named.length === 0) return plural;
634
-
635
- const shown = named.slice(0, 2);
636
- const rest = named.length - shown.length + individual;
637
- // `+N` rather than `+N more`: five characters, and they decide whether the
638
- // mixed case (two bundles plus a policy added by hand) gets named at all — with
639
- // "more" the line is 83 and falls back to a bare count.
640
- return rest > 0 ? `${shown.join(", ")} +${rest}` : shown.join(", ");
424
+ /** The Policies line's value. With policies no longer chosen during setup, this
425
+ * reports what is enabled and nothing about how it got that way. */
426
+ function describeSelection(policiesCount: number): string {
427
+ return `${policiesCount} ${policiesCount === 1 ? "policy" : "policies"}`;
641
428
  }
642
429
 
430
+
643
431
  export function reviewLines(state: {
644
432
  /** What the scope step resolved to. Expands to one or two real scopes. */
645
433
  target: SetupTarget;
@@ -1104,247 +892,84 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1104
892
 
1105
893
  // 0 — Recommended, or choose everything yourself?
1106
894
  //
1107
- // Setup used to open by asking four questions — scope, policy bundles,
1108
- // harnesses, cloud — of somebody who has just installed the tool and does not
1109
- // yet know what any of them mean. Every one of those answers has a defensible
1110
- // default, so asking for all four up front is making the person least able to
1111
- // answer do the most work.
1112
- //
1113
- // Recommended is not a shortcut past the decisions; it IS a decision, taken
1114
- // once, here, on their behalf: global scope, the CLIs actually on this
1115
- // machine, and `RECOMMENDED_POLICIES` — which is written out in
1116
- // policy-presets.ts with the reasoning for every inclusion and every
1117
- // omission, because "what does Recommended do" has to be answerable without
1118
- // reading code.
895
+ // ONE linear flow, and no opening fork.
1119
896
  //
1120
- // Customize is the wizard exactly as it was. Nothing is removed and nothing
1121
- // is hidden; it stops being the only way through.
1122
- const detectedNow = detectInstalledClis();
1123
- const recommendedClis = detectedNow.filter((id) => clisSupportingScope("user").includes(id));
1124
- const mode = await selectOne<"recommended" | "customize">({
1125
- message: "Set up failproofai",
1126
- choices: [
1127
- {
1128
- label: "Recommended",
1129
- value: "recommended",
1130
- hint: recommendedClis.length
1131
- ? `${recommendedClis.length} detected ${recommendedClis.length === 1 ? "CLI" : "CLIs"} · ${RECOMMENDED_POLICIES.length} policies · global`
1132
- : `${RECOMMENDED_POLICIES.length} policies · global`,
1133
- },
1134
- {
1135
- label: "Customize",
1136
- value: "customize",
1137
- hint: "choose scope, policies and harnesses",
1138
- },
1139
- ],
1140
- stdin,
1141
- stdout,
1142
- });
1143
- if (mode === null) return cancel();
1144
-
1145
- // 1 — Where? Inferred from cwd, then confirmed.
897
+ // Setup used to ask "Recommended or Customize?" before anything else, which
898
+ // is a question about the wizard rather than about the machine you cannot
899
+ // answer it until you know what the alternatives are, and you only learn that
900
+ // by picking one. Recommended then silently took global scope, the detected
901
+ // CLIs, and fifteen policies nobody had seen.
1146
902
  //
1147
- // Running from inside a project and running from a home directory are two
1148
- // different intents, and asking a context-free "global or project?" made the
1149
- // user restate something they had already expressed by choosing where to run
1150
- // the command. So the choices are built from what actually exists here — and
1151
- // labelled Update vs Set up accordingly with the likelier target first.
1152
- const setupState = detectSetupState(cwd);
1153
- const targetChoices = buildTargetChoices(setupState);
1154
-
1155
- let target: SetupTarget;
1156
- if (mode === "recommended") {
1157
- // Global, always. A project-scoped install guards the one directory the
1158
- // command happened to be run from and silently leaves every other repo on
1159
- // the machine unguarded — which is the opposite of what somebody choosing
1160
- // "Recommended" is asking for.
1161
- target = "user";
1162
- } else if (targetChoices.length === 1) {
1163
- // From a home directory there is no project to configure, so there is no
1164
- // question to ask. Say what is about to happen rather than silently
1165
- // deciding it.
1166
- target = targetChoices[0].value;
1167
- stdout.write(`Configuring ${targetChoices[0].label.toLowerCase()} — ${targetChoices[0].hint}.\n\n`);
1168
- } else {
1169
- const chosen = await selectOne<SetupTarget>({
1170
- message: "What are we configuring?",
1171
- choices: targetChoices.map((c) => ({
1172
- label: c.label,
1173
- value: c.value,
1174
- hint: c.hint,
1175
- })),
1176
- stdin,
1177
- stdout,
1178
- });
1179
- if (chosen === null) return cancel();
1180
- target = chosen;
1181
- }
903
+ // What is left is three questions, in the order the machine needs them:
904
+ // the daemon (already installed above, because it is the only step that needs
905
+ // a password), then which harnesses, then whether to connect. Scope is not
906
+ // among them: it is GLOBAL, always. A project-scoped install guards the one
907
+ // directory the command happened to be run from and silently leaves every
908
+ // other repo on the machine unguarded — `failproofai policies --install
909
+ // --scope project` is still there for someone who genuinely wants that, and
910
+ // knows they do.
911
+ const target: SetupTarget = "user";
1182
912
  const scopes = scopesFor(target);
1183
- // The scope whose CURRENT state seeds the pickers below. With "Both" the
1184
- // project is the more specific of the two and the one the user is standing
1185
- // in, so it wins; anything it does not define still falls back to global at
1186
- // merge time, which is exactly the layering the policy loader already does.
1187
913
  const primaryScope: HookScope = scopes.includes("project") ? "project" : "user";
1188
914
 
1189
- // 2 — Which policies? Multi-select of themed presets — additive, so the
1190
- // enabled set is the union of every ticked bundle.
1191
- //
1192
- // Before the assistants step, because "what do you want guarded" is the
1193
- // question the user came here to answer; which CLIs to wire it into is
1194
- // plumbing that follows from it.
915
+ // 2 — Which harnesses?
1195
916
  //
1196
- // Seed the Custom checkbox AND the bundle boxes from whatever the config already
1197
- // says, so the wizard shows the current state rather than resetting it every run.
917
+ // Setup no longer asks which policies to enable, and that is deliberate.
918
+ // failproofai ships no policies of its own any more: they arrive as packs
919
+ // from inside this package (`pack add core`) or from anyone's GitHub release.
920
+ // A wizard that pre-ticks OUR list makes a product decision on behalf of
921
+ // somebody who has not seen the list yet, and not everyone wants the set we
922
+ // would have chosen. So setup wires the hooks, and choosing what they
923
+ // enforce is a separate act, taken later and on purpose.
1198
924
  //
1199
- // Read at the scope this run will WRITE to, not the merged view. `installHooks`
1200
- // is called with `replace: true` per scope, so seeding from the merge would tick
1201
- // a bundle because it is enabled at PROJECT scope and then write it into USER
1202
- // scope — copying a selection between scopes as a side effect of opening the
1203
- // wizard. `readHooksConfig()` stays for the custom flag, which is read the same
1204
- // merged way everywhere else.
1205
- const customEnabledBefore = readHooksConfig().customPoliciesEnabled !== false;
925
+ // Whatever is already enabled at THIS scope is read and carried through
926
+ // untouched. `installHooks` is called with `replace: true`, so passing
927
+ // anything less would switch OFF policies the user had turned on running
928
+ // setup a second time must never reduce protection.
1206
929
  const enabledHere = readScopedHooksConfig(primaryScope, cwd).enabledPolicies ?? [];
1207
- const presetChoices = buildPresetChoices(cwd, customEnabledBefore, enabledHere);
1208
- // The policies no ticked bundle accounts for. Derived from the SAME pure split
1209
- // the rows are built from, so the locked row and the written set agree.
1210
- const carriedIndividual = splitEnabled(enabledHere).individual;
1211
- const hasCustomFiles = describeCustomPolicies(cwd).fileCount > 0;
1212
-
1213
- // No minimum. Ticking nothing is a real answer — someone who only wants their
1214
- // own custom policies, or who intends to pick bundles later from the
1215
- // dashboard, was previously stuck on this step with no way forward and no
1216
- // explanation beyond "Select at least 1". An empty set is already supported
1217
- // end to end: `installHooksImpl` documents its explicit-array path as "may be
1218
- // empty", `replace: true` makes it the full enabled set, and `summarize([])`
1219
- // renders "none". Hooks still install, so enforcement can be switched on
1220
- // later without re-running setup.
930
+ const policies = enabledHere;
931
+
932
+ // Left alone, in both modes. This was a checkbox on the policy step, which is
933
+ // gone; with no row to read, the only honest value is "do not touch it".
934
+ // Writing `false` here switched off every convention policy on disk as a side
935
+ // effect of finishing setup.
936
+ const customEnabled: boolean | undefined = undefined;
937
+
938
+ // An "Everything available" row protects every supported CLI (detected +
939
+ // set-up-ahead); when ticked it wins over the individual boxes.
1221
940
  //
1222
- // The assistants step below keeps its minimum deliberately: an empty CLI list
1223
- // does NOT mean "no assistants" there `installHooksImpl` falls back to
1224
- // ["claude"], so letting it through would silently install for Claude.
1225
- // Steps 2 and 3 are navigable: ← on the harness step returns to the policy
1226
- // step with the previous answer still selected. Before this, changing an
1227
- // earlier answer meant abandoning setup and starting over, because a prompt
1228
- // had exactly one way out and it was `null`.
941
+ // Always asked, never inferred. This is the one decision setup genuinely
942
+ // cannot make for you: which agents on this machine should be guarded. The
943
+ // detected ones are pre-ticked, so the common answer is a single keypress.
1229
944
  //
1230
- // The policy step itself takes no `allowBack`: the only thing before it is
1231
- // the scope question, which is frequently not asked at all (a single choice
1232
- // is stated, not prompted), so ← there would sometimes go nowhere.
1233
- let presets: string[] | null = null;
1234
- let clisSel: string[] | null = null;
1235
- /**
1236
- * What the harness step had ticked when ← was last pressed.
1237
- *
1238
- * A SEPARATE variable, because `clisSel` cannot do this job: it is the loop's
1239
- * own condition (`while (clisSel === null)`), so it is null on every entry into
1240
- * the body by definition, and it is assigned only on the line that ends the
1241
- * loop. The restore that read `clisSel` was therefore unreachable — provably
1242
- * dead, with a comment stating the opposite intent.
1243
- *
1244
- * The cost was not cosmetic: deselect a CLI, press to fix an earlier answer,
1245
- * come back, and the step showed the detected defaults again. Pressing ↵ then —
1246
- * reasonably, having been told the selection was carried back — re-enabled hook
1247
- * installation for a CLI the user had explicitly turned off.
1248
- *
1249
- * `presets` just above works because it is assigned MID-loop and survives to the
1250
- * next iteration; this mirrors that, filled from the prompt's `onBack`.
1251
- */
1252
- const carried: { clis: string[] | null } = { clis: null };
1253
- // The recommended path answers both questions here, which is what SKIPS the
1254
- // loop below — its condition is `clisSel === null` and this fills it in.
1255
- // Written as a pre-fill rather than as an extra clause on the loop condition
1256
- // so the invariant the rest of the function depends on ("past this loop, both
1257
- // are assigned") stays provable by the compiler rather than by argument.
1258
- if (mode === "recommended") {
1259
- presets = [];
1260
- clisSel = recommendedClis;
1261
- }
1262
- while (clisSel === null) {
1263
- // Re-entering after a ← must show what was picked, not a blank slate.
1264
- // Selection state lives on each choice, so carry it back in.
1265
- // Loop-carried: narrowed to `null` on the first pass, repopulated on a ←.
1266
- const priorPresets = presets as string[] | null;
1267
- presets = await multiSelect<string>({
1268
- message: "What should we guard against?",
1269
- choices: priorPresets
1270
- ? presetChoices.map((c) => ({ ...c, checked: priorPresets.includes(c.value) }))
1271
- : presetChoices,
1272
- summaryNoun: "bundles",
1273
- hint: "space toggles · combine presets · ↵ confirm · none is fine",
1274
- stdin,
1275
- stdout,
1276
- });
1277
- if (presets === null) return cancel();
1278
-
1279
- // 3 — Which harnesses? An "Everything available" row protects every supported
1280
- // CLI (detected + set-up-ahead); when ticked it wins over the individual boxes.
1281
- // Read off a HOLDER OBJECT, not a bare `let`, and not through a cast.
1282
- //
1283
- // A `let` assigned only inside a callback is narrowed by control-flow analysis
1284
- // to its initializer, so `priorClis.includes` will not compile — and the
1285
- // original defeated that with `clisSel as string[] | null`. That cast is
1286
- // precisely why the dead code type-checked and nobody noticed: it silenced the
1287
- // compiler making exactly the point the reviewer later made by hand, that the
1288
- // value could only ever be null. A property read carries the declared type
1289
- // without suppressing anything.
1290
- const priorClis = carried.clis;
1291
- const picked: string[] | typeof BACK | null = await multiSelect<string>({
1292
- message: "Which harnesses should it protect?",
1293
- choices: [
1294
- {
1295
- label: "Everything available",
1296
- value: ALL_CLIS,
1297
- // Counts only what this scope can actually take — expanding to all 12
1298
- // under project scope is what crashed the apply on Hermes.
1299
- hint: `protect all ${clisSupportingScope(primaryScope).length} CLIs configurable here`,
1300
- // A selector, not a harness. Counting it gave "13 harnesses" for
1301
- // the 12 supported CLIs, and listed "Everything available" among them.
1302
- summaryExclude: true,
1303
- },
1304
- ...buildAgentChoices(primaryScope, cwd),
1305
- ].map((c) => (priorClis ? { ...c, checked: priorClis.includes(c.value) } : c)),
945
+ // No `allowBack`: with the policy step and the scope fork both gone, there is
946
+ // nothing inside setup to return to.
947
+ let clisSel: string[];
948
+ {
949
+ const picked = await multiSelect<string>({
950
+ message: "Which harnesses should it protect?",
951
+ choices: [
952
+ {
953
+ label: "Everything available",
954
+ value: ALL_CLIS,
955
+ // Counts only what this scope can actually take expanding to all 12
956
+ // under project scope is what crashed the apply on Hermes.
957
+ hint: `protect all ${clisSupportingScope(primaryScope).length} CLIs configurable here`,
958
+ // A selector, not a harness. Counting it gave "13 harnesses" for
959
+ // the 12 supported CLIs, and listed "Everything available" among them.
960
+ summaryExclude: true,
961
+ },
962
+ ...buildAgentChoices(primaryScope, cwd),
963
+ ],
1306
964
  minSelected: 1,
1307
965
  summaryNoun: "harnesses",
1308
- hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ← back · ↵ confirm",
1309
- allowBack: true as const,
1310
- // `BACK` is a symbol and cannot carry the selection, so the prompt reports
1311
- // it here instead — otherwise a ← discards what the user had ticked and the
1312
- // next pass redraws the detected defaults.
1313
- onBack: (checkedNow) => {
1314
- carried.clis = checkedNow;
1315
- },
966
+ hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ↵ confirm",
1316
967
  stdin,
1317
968
  stdout,
1318
969
  });
1319
970
  if (picked === null) return cancel();
1320
- // ← re-runs the loop, which re-asks the policy step with its answer intact.
1321
- if (picked === BACK) continue;
1322
971
  clisSel = picked;
1323
972
  }
1324
- // Non-null by construction: the loop only exits once both are assigned, and
1325
- // the recommended path assigned both before it, which is why it never ran.
1326
- const chosenPresets: string[] = presets ?? [];
1327
- const policies =
1328
- mode === "recommended"
1329
- ? // UNION with what is already enabled here, never a replacement for it.
1330
- // `installHooks` is called with `replace: true`, so writing the bare
1331
- // recommended list would silently switch OFF anything the user had
1332
- // added themselves — turning "give me the sensible defaults" into a
1333
- // reduction in protection, which is the one direction this must never
1334
- // move. On a fresh machine `enabledHere` is empty and this is exactly
1335
- // the 15.
1336
- [...new Set([...RECOMMENDED_POLICIES, ...enabledHere])]
1337
- : resolvePresetSelection(chosenPresets, carriedIndividual);
1338
- // Only meaningful when there are files to switch off; with none, the row is
1339
- // locked-unchecked and must not write a disabling flag.
1340
- //
1341
- // `undefined` on the recommended path, which means "leave the flag alone".
1342
- // The customize expression would read as `false` here — no bundle was ticked,
1343
- // so `chosenPresets.includes(CUSTOM)` is false — and would write
1344
- // `customPoliciesEnabled: false`, disabling every convention policy the user
1345
- // has on disk as a side effect of choosing the default setup.
1346
- const customEnabled =
1347
- mode === "recommended" ? undefined : hasCustomFiles ? chosenPresets.includes(CUSTOM) : undefined;
1348
973
  // Filter to what the chosen scopes support in BOTH branches: "Everything
1349
974
  // available" must not expand to CLIs that cannot take any selected scope,
1350
975
  // and a locked row can't be ticked but belt-and-braces keeps the invariant
@@ -1764,7 +1389,6 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1764
1389
  cli: clis,
1765
1390
  cli_count: clis.length,
1766
1391
  policy_count: policies.length,
1767
- source: chosenPresets.join("+"),
1768
1392
  connected: connect !== null,
1769
1393
  });
1770
1394
 
@@ -1867,7 +1491,6 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1867
1491
  customEnabled,
1868
1492
  daemonInstalled,
1869
1493
  connected,
1870
- chosenPresets,
1871
1494
  ),
1872
1495
  { ok: true },
1873
1496
  stdout,
@@ -98,8 +98,6 @@ import {
98
98
  isDaemonSupportedPlatform,
99
99
  probeDaemonEndToEnd,
100
100
  } from "./daemon-service";
101
- import { installBundledPack } from "./pack-store";
102
- import { readHooksConfig } from "./hooks-config";
103
101
 
104
102
  export interface ResetOutcome {
105
103
  /** Paths that existed and were removed. */
@@ -966,24 +964,17 @@ export function resetHome(from: number, to: number = LAYOUT_VERSION): ResetOutco
966
964
  // straight out of the same file. Both paths end at the same key, and only one
967
965
  // of them represents something a person typed.
968
966
  if (telemetryOptOut) updateConfig({ telemetry: { enabled: false } });
969
- // `packs/` is resettable, but the package's default pack is also the offline
970
- // enforcement floor. Restore it from the installed package before declaring
971
- // the migration complete; third-party packs remain explicitly re-fetchable.
967
+ // No pack is installed here, deliberately.
972
968
  //
973
- // Carrying `enabledPolicies` INTO the selection, not installing defaults over
974
- // it. This is the upgrade path every existing user takes, and installing
975
- // defaults meant a machine that had switched `block-force-push` and
976
- // `block-kubectl` on came back with them off guards that were denying
977
- // before the upgrade and allowing after, with nothing said.
978
- const carried = (() => {
979
- try {
980
- const names = readHooksConfig().enabledPolicies ?? [];
981
- return names.length > 0 ? { only: names } : undefined;
982
- } catch {
983
- return undefined;
984
- }
985
- })();
986
- installBundledPack(carried);
969
+ // There is no copy in this package to install — our policies are fetched from
970
+ // their GitHub release like anybody else's and `resetHome` is SYNCHRONOUS
971
+ // and runs inside `failproofai update`. An upgrade that blocks on github.com,
972
+ // and fails when it is unreachable, is a worse upgrade than one that finishes.
973
+ //
974
+ // Nothing needs carrying either: `enabledPolicies` is left exactly where it
975
+ // is, and the no-pack fallback reads it, so a machine mid-upgrade keeps
976
+ // enforcing precisely what it enforced before. `failproofai policies add core`
977
+ // turns those names into a real pack, carrying the same selection into it.
987
978
  // The step's OWN target, not LAYOUT_VERSION.
988
979
  //
989
980
  // Every step used to end stamping the current layout, which was harmless