failproofai 1.0.2-beta.4 → 1.0.2-beta.6

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 (155) 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 +4 -4
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  34. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/index.html +1 -1
  38. package/.next/standalone/.next/server/app/index.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  40. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  67. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03r-lqq._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0clqfab._.js → [root-of-the-server]__1tg4pu6._.js} +2 -2
  74. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{_1lbh81u._.js → _19uwam-._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +7 -7
  77. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1m84s36._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +6 -6
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__14muoc4._.js → [root-of-the-server]__128se8m._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/{_0ovx4lz._.js → _0no96tu._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/{_1dkh76z._.js → _11fc-hb._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/lib_1cf5ya2._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
  108. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  109. package/.next/standalone/.next/server/pages/404.html +1 -1
  110. package/.next/standalone/.next/server/pages/500.html +1 -1
  111. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  112. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  113. package/.next/standalone/.next/static/chunks/{0lshnlpy1m6bc.js → 0931avh-06lj5.js} +1 -1
  114. package/.next/standalone/.next/static/chunks/{0k_yvjc1yz0pq.js → 0h-3zffoy-_ji.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/{0ds64uculrze8.js → 0p84npzxdgpc5.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/{0d_re32wscs0b.js → 0sdgnflvw8kta.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{2dbi_x0c8n8fi.js → 0z7au3njlak0j.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{1o0ahew9568ls.js → 1id7o44jjegf0.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{00_t-xvc59tkb.js → 276n-edifojbd.js} +2 -2
  120. package/.next/standalone/.next/static/chunks/{07j2u70_3qsx8.js → 2jfskwd2u8o5r.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{2817_8e94ah9z.js → 2pxr7nr1nyotn.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{1oz0svxwt9574.js → 3i8rg2fv9nd6e.js} +2 -2
  123. package/.next/standalone/.next/static/chunks/{1q76jk_1_wgjq.js → 3vu_7_eor1m-k.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/{turbopack-17utl_jw0wj11.js → turbopack-2x-o099q64i8h.js} +1 -1
  125. package/.next/standalone/lib/opencode-db.ts +77 -0
  126. package/.next/standalone/lib/opencode-projects.ts +37 -41
  127. package/.next/standalone/lib/opencode-sessions.ts +59 -36
  128. package/.next/standalone/package.json +9 -9
  129. package/.next/standalone/server.js +1 -1
  130. package/bin/failproofai.mjs +116 -34
  131. package/dist/cli.mjs +1732 -1177
  132. package/dist/worker.mjs +180 -165
  133. package/lib/opencode-db.ts +77 -0
  134. package/lib/opencode-projects.ts +37 -41
  135. package/lib/opencode-sessions.ts +59 -36
  136. package/package.json +9 -9
  137. package/src/audit/cache.ts +167 -11
  138. package/src/audit/cli-adapters/claude.ts +111 -1
  139. package/src/audit/cli-adapters/index.ts +31 -1
  140. package/src/audit/cli.ts +12 -0
  141. package/src/audit/index.ts +164 -13
  142. package/src/hooks/configure-wizard.ts +112 -18
  143. package/src/hooks/fp-reset.ts +1 -1
  144. package/src/hooks/handler.ts +13 -1
  145. package/src/hooks/manager.ts +69 -4
  146. package/src/hooks/onboarding-attempt.ts +4 -4
  147. package/src/hooks/pack-cli.ts +399 -32
  148. package/src/hooks/pack-store.ts +21 -8
  149. package/src/hooks/read-stdin.ts +78 -8
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
  153. /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_buildManifest.js +0 -0
  154. /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_clientMiddlewareManifest.js +0 -0
  155. /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_ssgManifest.js +0 -0
@@ -30,6 +30,8 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
30
30
  import { homedir } from "node:os";
31
31
  import { dirname, resolve, sep } from "node:path";
32
32
 
33
+ import { CORE_SOURCE } from "./pack-store";
34
+
33
35
  import {
34
36
  selectOne,
35
37
  promptText,
@@ -109,6 +111,31 @@ export interface WizardIO {
109
111
  stdout?: TTYOut;
110
112
  }
111
113
 
114
+ /**
115
+ * Answers supplied up front, so the run needs nobody at the keyboard.
116
+ *
117
+ * This is the whole of headless setup, and it is small for a reason nobody
118
+ * planned: the wizard stopped DECIDING most of what it used to. Scope, agents,
119
+ * policies and the custom-policy toggle are all hardcoded now (see the
120
+ * constants further down), so there is no decision surface left to expose —
121
+ * only the three questions that remain, and each has one sensible unattended
122
+ * answer.
123
+ *
124
+ * None of this skips sudo. Installing a system service needs root and nothing
125
+ * here changes that: elevation goes through `sudo -n`, which never prompts, and
126
+ * a machine that cannot elevate is told exactly what to run as root.
127
+ */
128
+ export interface WizardAnswers {
129
+ /** Cloud API key. Its presence IS the request to connect. */
130
+ token?: string;
131
+ /** Where to connect. Defaults to the same URL the wizard infers. */
132
+ url?: string;
133
+ machineId?: string;
134
+ machineLabel?: string;
135
+ /** Record decisions only, no session transcripts. */
136
+ noTranscripts?: boolean;
137
+ }
138
+
112
139
  /**
113
140
  * Why a wizard run ended without applying. Distinguished so the caller can
114
141
  * pick an exit code — a user who pressed Esc did nothing wrong (exit 0), a
@@ -120,8 +147,9 @@ export type WizardAbort =
120
147
  | "needs_root"
121
148
  | "daemon_failed"
122
149
  | "unsupported_platform"
123
- | "not_a_tty"
124
- | "running_as_sudo";
150
+ | "running_as_sudo"
151
+ /** A cloud key was supplied and the server refused it. Unattended only. */
152
+ | "cloud_unverified";
125
153
 
126
154
  export interface WizardResult {
127
155
  applied: boolean;
@@ -695,19 +723,27 @@ export async function maybeFirstRunConfigure(
695
723
 
696
724
  // ── The wizard ───────────────────────────────────────────────────────────────
697
725
 
698
- export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResult> {
726
+ export async function runConfigureWizard(
727
+ io: WizardIO = {},
728
+ answers: WizardAnswers = {},
729
+ ): Promise<WizardResult> {
699
730
  const stdin: TTYIn = io.stdin ?? process.stdin;
700
731
  const stdout: TTYOut = io.stdout ?? process.stdout;
701
732
  const cwd = process.cwd();
733
+ // No terminal means no questions — not a refusal.
734
+ //
735
+ // `failproofai config` IS the authorisation: somebody typed the command whose
736
+ // entire job is to configure this machine. Requiring a flag on top of that to
737
+ // permit what the command already means is asking the same question twice,
738
+ // and this used to refuse outright — so a CI job, a container or an agent
739
+ // could not set a machine up at all, which is the gap this closes.
740
+ //
741
+ // Safe to do here because the IMPLICIT path is guarded separately:
742
+ // `maybeFirstRunConfigure` has its own TTY check and returns before ever
743
+ // reaching this function, so setup never runs off the back of some other
744
+ // command on a headless box.
745
+ const unattended = !stdin.isTTY || !stdout.isTTY;
702
746
 
703
- if (!stdin.isTTY || !stdout.isTTY) {
704
- stdout.write(
705
- "failproofai config needs an interactive terminal.\n" +
706
- "Use the flag form instead, e.g.:\n" +
707
- " failproofai policies --install --scope user --cli claude\n",
708
- );
709
- return { applied: false };
710
- }
711
747
 
712
748
  // Running the wizard itself under sudo configures the WRONG ACCOUNT, and
713
749
  // does it silently: homedir() becomes /root, so the hooks land in root's
@@ -724,7 +760,10 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
724
760
  "The one step that needs root (installing the service) asks for your password\n" +
725
761
  "on its own.\n",
726
762
  );
727
- return { applied: false };
763
+ // Same reason as above: this configured nothing, and it is not a
764
+ // cancellation. `running_as_sudo` has been in `WizardAbort` since it was
765
+ // written and was never once assigned.
766
+ return { applied: false, abort: "running_as_sudo" };
728
767
  }
729
768
 
730
769
  // Fire-and-forget: never block the wizard's first paint on telemetry.
@@ -847,7 +886,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
847
886
  ? "failproofaid can't evaluate — every tool call is denied. Rebuilding needs sudo.\n\n"
848
887
  : "Installing failproofaid — needs sudo once.\n\n",
849
888
  );
850
- if (!primeElevation()) {
889
+ // `primeElevation` runs `sudo -v`, which PROMPTS. An unattended run has
890
+ // nobody to type a password, so it goes straight to the `sudo -n` that
891
+ // `runPrivileged` uses anyway — exactly what `failproofai update` already
892
+ // does — and a machine that cannot elevate gets the commands below instead
893
+ // of a hung terminal. This is the same answer for both: sudo cannot be
894
+ // conjured away, only asked for at a moment somebody can answer.
895
+ if (!(unattended ? canElevate() : primeElevation())) {
851
896
  // Required means required: write nothing at all, so a machine that could
852
897
  // not be set up is left exactly as it was found rather than carrying
853
898
  // half a configuration. The commands are printed so an admin can do the
@@ -866,7 +911,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
866
911
  "failproofaid is running, but from a service definition written by an older\n" +
867
912
  "version — it cannot start a scheduled audit. Refreshing it needs root once.\n\n",
868
913
  );
869
- if (!primeElevation()) {
914
+ if (!(unattended ? canElevate() : primeElevation())) {
870
915
  // NOT an abort, unlike the install branch above. There is a working
871
916
  // daemon here and hooks are enforcing; only the scheduled audit is out
872
917
  // of reach. Stopping setup over that would make an upgrade the thing
@@ -914,7 +959,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
914
959
  //
915
960
  // Setup no longer asks which policies to enable, and that is deliberate.
916
961
  // failproofai ships no policies of its own any more: they arrive as packs —
917
- // from inside this package (`pack add core`) or from anyone's GitHub release.
962
+ // from inside this package (`policies add FailproofAI/policies`) or from anyone's GitHub release.
918
963
  // A wizard that pre-ticks OUR list makes a product decision on behalf of
919
964
  // somebody who has not seen the list yet, and not everyone wants the set we
920
965
  // would have chosen. So setup wires the hooks, and choosing what they
@@ -971,7 +1016,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
971
1016
  let connect: { url: string; token: string; machineId: string; machineLabel: string } | null = null;
972
1017
 
973
1018
  {
974
- const choice = await selectOne<"key" | "local">({
1019
+ // Supplying a key IS asking to connect — there is no other reason to pass
1020
+ // one — so an unattended run with a token skips straight past the question,
1021
+ // and one without a token stays local, which is the same "Not now" branch a
1022
+ // person picks. Neither needs a separate flag to say so.
1023
+ const choice = unattended
1024
+ ? (answers.token ? "key" : "local")
1025
+ : await selectOne<"key" | "local">({
975
1026
  message: "Connect this machine to FailproofAI Cloud?",
976
1027
  // The hint says what you GET; the body says what LEAVES.
977
1028
  //
@@ -1054,7 +1105,16 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1054
1105
  let machineId = resolveMachineId();
1055
1106
  let machineLabel = existing?.machineLabel ?? resolveMachineLabel();
1056
1107
 
1057
- if (existing) {
1108
+ if (existing && unattended && !answers.url) {
1109
+ // Already enrolled, and this run did not name a different place to
1110
+ // enrol. Reusing is what the person picks here too, and re-verifying a
1111
+ // working credential is the one thing an unattended re-run must not
1112
+ // turn into a failure.
1113
+ url = existing.url;
1114
+ token = answers.token ?? existing.token;
1115
+ machineId = answers.machineId ?? existing.machineId;
1116
+ machineLabel = answers.machineLabel ?? existing.machineLabel ?? machineLabel;
1117
+ } else if (existing && !unattended) {
1058
1118
  const reuse = await selectOne<"reuse" | "other">({
1059
1119
  message: `Use this machine's existing connection to ${existing.url}?`,
1060
1120
  choices: [
@@ -1126,6 +1186,12 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1126
1186
  }
1127
1187
  }
1128
1188
 
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
+ }
1129
1195
  if (token === null) {
1130
1196
  token = await promptText({
1131
1197
  // The destination is in the question now that it is no longer a
@@ -1149,6 +1215,14 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1149
1215
  // persisted here.
1150
1216
  stdout.write("\nChecking the key… ");
1151
1217
  const probe = await validateIngestKey({ url: ingestUrlFor(url), key: token });
1218
+ if (!probe.ok && unattended) {
1219
+ // A key that does not work is a FAILED setup, not a prompt. The
1220
+ // interactive path offers "save it anyway" because a person can weigh
1221
+ // an outage against their own impatience; a script cannot, and one that
1222
+ // exited 0 here would leave a fleet believing it was reporting.
1223
+ stdout.write(`\nThat key did not work: ${probe.reason}\n`);
1224
+ return { applied: false, abort: "cloud_unverified" };
1225
+ }
1152
1226
  if (!probe.ok) {
1153
1227
  stdout.write(`\nThat did not work: ${probe.reason}\n`);
1154
1228
  const retry = await selectOne<"skip" | "anyway">({
@@ -1173,7 +1247,15 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1173
1247
  }
1174
1248
  }
1175
1249
  // 5 — Review & apply
1176
- const decision = await selectOne<"apply" | "cancel">({
1250
+ //
1251
+ // The last question, and the only one a headless run skips outright rather
1252
+ // than answering from a flag. There is nothing to confirm when nobody is
1253
+ // watching, and the command itself was the confirmation.
1254
+ //
1255
+ // NOT the same call as `uninstall`, which does require --yes: that one
1256
+ // REMOVES things, so silence there would destroy on a signal as weak as a
1257
+ // missing terminal. This one does exactly what its name says.
1258
+ const decision = unattended ? "apply" : await selectOne<"apply" | "cancel">({
1177
1259
  message: "Ready to apply?",
1178
1260
  body: reviewLines({
1179
1261
  target,
@@ -1463,6 +1545,18 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1463
1545
  { ok: true },
1464
1546
  stdout,
1465
1547
  );
1548
+ // Setup wires the hooks and deliberately chooses NO policies, so a machine
1549
+ // that has just finished it enforces almost nothing — and the summary line
1550
+ // says "0 policies" without saying what to do about it. Anyone else's pack is
1551
+ // typed the same way, which is the point: ours is named in full rather than
1552
+ // by a short name only we could use.
1553
+ if (policies.length === 0) {
1554
+ stdout.write(
1555
+ `\n Nothing is enforcing yet. Take ours, or anyone's:\n` +
1556
+ ` failproofai policies add ${CORE_SOURCE}\n` +
1557
+ ` failproofai policies show <owner>/<repo> (look first)\n\n`,
1558
+ );
1559
+ }
1466
1560
  return {
1467
1561
  applied: true,
1468
1562
  target,
@@ -973,7 +973,7 @@ export function resetHome(from: number, to: number = LAYOUT_VERSION): ResetOutco
973
973
  //
974
974
  // Nothing needs carrying either: `enabledPolicies` is left exactly where it
975
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`
976
+ // enforcing precisely what it enforced before. `failproofai policies add FailproofAI/policies`
977
977
  // turns those names into a real pack, carrying the same selection into it.
978
978
  // The step's OWN target, not LAYOUT_VERSION.
979
979
  //
@@ -323,7 +323,7 @@ export async function evaluateHookEvent(
323
323
  if (legacyNames.length > 0) {
324
324
  hookLogWarn(
325
325
  `enforcing ${legacyNames.length} policies from this build because no pack is installed — ` +
326
- `run \`failproofai policies add core\` to move them into a real pack`,
326
+ `run \`failproofai policies add FailproofAI/policies\` to move them into a real pack`,
327
327
  );
328
328
  }
329
329
  // What actually registered, for the pack-duplicate check below. A paused
@@ -786,6 +786,18 @@ export async function handleHookEvent(eventType: string, cli: IntegrationType =
786
786
  error_type: "oversized",
787
787
  });
788
788
  }
789
+ if (stdinRead.timedOut) {
790
+ // Reported distinctly from a read error because the remedy is different and
791
+ // the cause is upstream: the agent spawned the hook and did not close its
792
+ // stdin. Before this was bounded it was not reported at all — the process
793
+ // simply never returned, and the tool call it was gating never resolved.
794
+ hookLogWarn(`stdin never closed for ${eventType}; evaluated nothing`);
795
+ void trackHookEvent(getInstanceId(), "hook_stdin_error", {
796
+ event_type: eventType,
797
+ cli,
798
+ error_type: "timeout",
799
+ });
800
+ }
789
801
 
790
802
  const result = await evaluateHookEvent(eventType, cli, stdinRead.payload);
791
803
 
@@ -14,7 +14,7 @@ import { INTEGRATION_TYPES,
14
14
  type HookScope,
15
15
  type IntegrationType,
16
16
  } from "./types";
17
- import { claudeCode, getIntegration, settingsPathsFor } from "./integrations";
17
+ import { claudeCode, getIntegration, settingsPathsFor, type Integration } from "./integrations";
18
18
  import { promptPolicySelection } from "./install-prompt";
19
19
  import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
20
20
  import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
@@ -271,9 +271,44 @@ function deduplicateScopes(scopes: readonly HookScope[], cwd?: string): HookScop
271
271
  * rows of the listing already answer it.
272
272
  */
273
273
  export function hooksInstalledInSettings(scope: HookScope, cwd?: string): boolean {
274
- return INTEGRATION_TYPES.some((id) => {
274
+ return integrationsInstalledAt(scope, cwd).length > 0;
275
+ }
276
+
277
+ /**
278
+ * Which integrations are wired at EXACTLY this scope — the honest form of the
279
+ * question above, and the one the multi-scope warning needs.
280
+ *
281
+ * Two ways an integration answers yes for a scope it is not actually installed
282
+ * at, both of which made `failproofai policies` warn about "hooks in multiple
283
+ * scopes" on a machine whose hooks are in exactly one file:
284
+ *
285
+ * - It does not SUPPORT the scope. Hermes and OpenClaw are user-scope only
286
+ * (`HERMES_HOOK_SCOPES = ["user"]`) and their `getSettingsPath` ignores the
287
+ * scope argument entirely, so they hand back the user file — and report it
288
+ * installed — for `project` and `local` alike. `scopes` already declares
289
+ * this; nothing was reading it.
290
+ * - Its project path RESOLVES to its user path. Run this from `$HOME` and
291
+ * `<cwd>/.claude/settings.json` is `~/.claude/settings.json`: one file,
292
+ * counted as two scopes. Cheap to detect and impossible to get right by
293
+ * asking each integration separately.
294
+ */
295
+ export function integrationsInstalledAt(scope: HookScope, cwd?: string): IntegrationType[] {
296
+ return INTEGRATION_TYPES.filter((id) => {
275
297
  try {
276
- return getIntegration(id).hooksInstalledInSettings(scope, cwd);
298
+ const integration = getIntegration(id);
299
+ if (!integration.scopes.includes(scope)) return false;
300
+ if (!integration.hooksInstalledInSettings(scope, cwd)) return false;
301
+ // A narrower scope that lands on the same file as a wider one is that
302
+ // wider one, seen twice. Attribute it to the widest scope that resolves
303
+ // there so exactly one of them counts.
304
+ const here = integration.getSettingsPath(scope, cwd);
305
+ return !HOOK_SCOPES.some(
306
+ (other) =>
307
+ other !== scope &&
308
+ HOOK_SCOPES.indexOf(other) < HOOK_SCOPES.indexOf(scope) &&
309
+ integration.scopes.includes(other) &&
310
+ safeSettingsPath(integration, other, cwd) === here,
311
+ );
277
312
  } catch {
278
313
  // An integration whose settings file is unreadable is not evidence that
279
314
  // nothing is installed — keep asking the rest.
@@ -282,6 +317,18 @@ export function hooksInstalledInSettings(scope: HookScope, cwd?: string): boolea
282
317
  });
283
318
  }
284
319
 
320
+ function safeSettingsPath(
321
+ integration: Integration,
322
+ scope: HookScope,
323
+ cwd?: string,
324
+ ): string | null {
325
+ try {
326
+ return integration.getSettingsPath(scope, cwd);
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+
285
332
  export interface InstallHooksOptions {
286
333
  /** Replace the enabled set at this scope instead of unioning (default: additive). */
287
334
  replace?: boolean;
@@ -531,7 +578,7 @@ async function installHooksImpl(
531
578
  } catch (err) {
532
579
  console.log(
533
580
  `\nWarning: could not fetch the policy pack (${err instanceof Error ? err.message : String(err)}).\n` +
534
- `Run \`failproofai policies add core\` once you are online.`,
581
+ `Run \`failproofai policies add FailproofAI/policies\` once you are online.`,
535
582
  );
536
583
  }
537
584
  } else {
@@ -944,6 +991,24 @@ export async function listHooks(cwd?: string): Promise<void> {
944
991
  // a reader scrolling back from their prompt actually sees.
945
992
  const footer: Array<string[] | null> = [note(`Config: ${globalPolicyConfigFile()}`, opts)];
946
993
 
994
+ // A machine with hooks wired and no policies to run is the normal state right
995
+ // after setup — setup installs the hooks and deliberately chooses nothing —
996
+ // and this listing said NOTHING about how to leave it. A reader who had just
997
+ // finished `failproofai config` saw a header, a config path, and no next
998
+ // step. Ours is named in full, the same way anyone else's pack is typed,
999
+ // because it IS one.
1000
+ if (packCount === 0) {
1001
+ footer.unshift(
1002
+ nextStep(
1003
+ `failproofai policies add ${CORE_SOURCE}`,
1004
+ "Nothing is enforcing yet. Take ours, or anyone's:",
1005
+ opts,
1006
+ ),
1007
+ note("Someone else's: failproofai policies add <owner>/<repo>", opts),
1008
+ note("Look first: failproofai policies show <owner>/<repo>", opts),
1009
+ );
1010
+ }
1011
+
947
1012
  if (installedScopes.length > 1) {
948
1013
  footer.push(
949
1014
  warning(
@@ -157,10 +157,11 @@ export function blockerCleared(attempt: OnboardingAttempt, probe: RetryProbe): b
157
157
  // command would just repeat the hard-fail. Only a new release is a reason
158
158
  // to ask again (it might add support for this platform).
159
159
  return probe.cliVersion !== attempt.cliVersion;
160
- case "not_a_tty":
161
160
  case "running_as_sudo":
162
- // Both are properties of the invocation, not of the machine, so the next
163
- // one may well be fine.
161
+ // A property of the invocation, not of the machine, so the next one may
162
+ // well be fine. (`not_a_tty` used to sit here too. It is gone because
163
+ // there is no such refusal any more: a run with no terminal has no
164
+ // questions to ask, so it applies rather than declining.)
164
165
  return true;
165
166
  default:
166
167
  return true;
@@ -174,7 +175,6 @@ export function attemptHintLines(attempt: OnboardingAttempt): string[] {
174
175
  daemon_failed: "the failproofaid service could not be started",
175
176
  cancelled: "it was cancelled",
176
177
  unsupported_platform: "failproofaid does not run on this platform",
177
- not_a_tty: "there was no terminal to ask in",
178
178
  running_as_sudo: "it was run under sudo",
179
179
  };
180
180
  const detail = why[attempt.reason] ?? "it did not finish";