failproofai 1.0.0-beta.6 → 1.0.0-beta.7

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 (118) 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 +2 -2
  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/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  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/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/index.html +1 -1
  39. package/.next/standalone/.next/server/app/index.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  42. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  43. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  44. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  45. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  46. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  49. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  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.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  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/server-reference-manifest.json +4 -4
  62. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_a_gwn._.js → [root-of-the-server]__0c_qcgp._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kbc9bs._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/{_0igtyj5._.js → _194yh9u._.js} +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  86. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  87. package/.next/standalone/.next/server/pages/404.html +1 -1
  88. package/.next/standalone/.next/server/pages/500.html +1 -1
  89. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  90. package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
  91. package/.next/standalone/.next/static/chunks/0945ybz3fsvba.js +1 -0
  92. package/.next/standalone/.next/static/chunks/{3rbpw_waieafm.js → 0gr76ps8_zm26.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/{0_2n13i94rnw5.js → 0m-m7wgtxdoav.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/11l6oh9cyb35a.js +1 -0
  95. package/.next/standalone/.next/static/chunks/166f8qnh03jqc.js +6 -0
  96. package/.next/standalone/.next/static/chunks/2aej0vi1fm87m.js +1 -0
  97. package/.next/standalone/.next/static/chunks/{3f_939wi3kfnx.js → 2bxld6-rym3ah.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{3p1vpm2_-tfwm.js → 2p2kz4aj1tooj.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{28zkwzu50puki.js → 36pc3_8fpb6ic.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{34fa65k0cldkj.js → 3t1wr_kx6brwc.js} +1 -1
  101. package/.next/standalone/package.json +5 -5
  102. package/.next/standalone/server.js +1 -1
  103. package/bin/failproofai.mjs +25 -26
  104. package/dist/cli.mjs +167 -62
  105. package/dist/worker.mjs +1 -1
  106. package/package.json +5 -5
  107. package/src/hooks/configure-wizard.ts +54 -0
  108. package/src/hooks/daemon-service.ts +10 -2
  109. package/src/hooks/fp-home.ts +13 -0
  110. package/src/hooks/onboarding-attempt.ts +179 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e7-pa7._.js +0 -3
  112. package/.next/standalone/.next/static/chunks/23557z4e6dtsr.js +0 -1
  113. package/.next/standalone/.next/static/chunks/3081gykdubv8b.js +0 -1
  114. package/.next/standalone/.next/static/chunks/35v07j9gc1pvr.js +0 -6
  115. package/.next/standalone/.next/static/chunks/40ffnfpkeyvb0.js +0 -1
  116. /package/.next/standalone/.next/static/{Ptn8aBQl_cnNgOd0ebyv0 → LUaLkKg40nSkHrIajexIk}/_buildManifest.js +0 -0
  117. /package/.next/standalone/.next/static/{Ptn8aBQl_cnNgOd0ebyv0 → LUaLkKg40nSkHrIajexIk}/_clientMiddlewareManifest.js +0 -0
  118. /package/.next/standalone/.next/static/{Ptn8aBQl_cnNgOd0ebyv0 → LUaLkKg40nSkHrIajexIk}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -19,7 +19,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // package.json
22
- var version2 = "1.0.0-beta.6";
22
+ var version2 = "1.0.0-beta.7";
23
23
  var init_package = () => {};
24
24
 
25
25
  // src/posthog-key.ts
@@ -1025,7 +1025,7 @@ function resettablePaths() {
1025
1025
  logsDir()
1026
1026
  ];
1027
1027
  }
1028
- var LAYOUT_VERSION = 2, at = (...parts) => resolve(failproofaiHome(), ...parts), atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts), versionFile = (home) => atHome(home, "VERSION"), configFile = (home) => atHome(home, "config.toml"), credentialsFile = (home) => atHome(home, "credentials.toml"), binDir = (home) => atHome(home, "bin"), policiesDir = (home) => atHome(home, "policies"), localPoliciesDir = (home) => atHome(home, "policies", "local-policies"), globalPolicyConfigFile = (home) => resolve(localPoliciesDir(home), "policies-config.json"), cloudPoliciesDir = (home) => resolve(policiesDir(home), "cloud-policies"), customPoliciesDir = (home) => resolve(policiesDir(home), "custom-policies"), customAgentsDir = (home) => atHome(home, "custom-agents"), auditDir = (home) => atHome(home, "audit"), auditDashboardFile = (home) => resolve(auditDir(home), "dashboard.json"), auditCacheDir = (home) => resolve(auditDir(home), "cache"), hookActivityDir = (home) => atHome(home, "hook-activity"), runDir = (home) => atHome(home, "run"), daemonSocket = () => process.env.FAILPROOFAI_DAEMON_SOCKET || resolve(runDir(), "failproofaid.sock"), auditLockFile = (home) => resolve(runDir(home), "audit.lock"), stateDir = (home) => atHome(home, "state"), shimsDir = (home) => resolve(stateDir(home), "shims"), sessionPauseDir = () => resolve(stateDir(), "sessions"), telemetryIdFile = (home) => resolve(stateDir(home), "telemetry-id"), launcherMarker = (home) => resolve(stateDir(home), "launcher-configured"), lastVersionFile = (home) => resolve(stateDir(home), "last-version"), onboardingLockFile = (home) => resolve(stateDir(home), "onboarding.lock"), codexSessionPathsFile = (home) => resolve(stateDir(home), "codex-session-paths.json"), logsDir = (home) => atHome(home, "logs"), legacy;
1028
+ var LAYOUT_VERSION = 2, at = (...parts) => resolve(failproofaiHome(), ...parts), atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts), versionFile = (home) => atHome(home, "VERSION"), configFile = (home) => atHome(home, "config.toml"), credentialsFile = (home) => atHome(home, "credentials.toml"), binDir = (home) => atHome(home, "bin"), policiesDir = (home) => atHome(home, "policies"), localPoliciesDir = (home) => atHome(home, "policies", "local-policies"), globalPolicyConfigFile = (home) => resolve(localPoliciesDir(home), "policies-config.json"), cloudPoliciesDir = (home) => resolve(policiesDir(home), "cloud-policies"), customPoliciesDir = (home) => resolve(policiesDir(home), "custom-policies"), customAgentsDir = (home) => atHome(home, "custom-agents"), auditDir = (home) => atHome(home, "audit"), auditDashboardFile = (home) => resolve(auditDir(home), "dashboard.json"), auditCacheDir = (home) => resolve(auditDir(home), "cache"), hookActivityDir = (home) => atHome(home, "hook-activity"), runDir = (home) => atHome(home, "run"), daemonSocket = () => process.env.FAILPROOFAI_DAEMON_SOCKET || resolve(runDir(), "failproofaid.sock"), auditLockFile = (home) => resolve(runDir(home), "audit.lock"), stateDir = (home) => atHome(home, "state"), shimsDir = (home) => resolve(stateDir(home), "shims"), sessionPauseDir = () => resolve(stateDir(), "sessions"), telemetryIdFile = (home) => resolve(stateDir(home), "telemetry-id"), launcherMarker = (home) => resolve(stateDir(home), "launcher-configured"), onboardingAttemptFile = (home) => resolve(stateDir(home), "onboarding-attempt.json"), lastVersionFile = (home) => resolve(stateDir(home), "last-version"), onboardingLockFile = (home) => resolve(stateDir(home), "onboarding.lock"), codexSessionPathsFile = (home) => resolve(stateDir(home), "codex-session-paths.json"), logsDir = (home) => atHome(home, "logs"), legacy;
1029
1029
  var init_fp_home = __esm(() => {
1030
1030
  legacy = {
1031
1031
  policyConfig: () => at("policies-config.json"),
@@ -22438,8 +22438,87 @@ var init_setup_state = __esm(() => {
22438
22438
  init_fp_home();
22439
22439
  });
22440
22440
 
22441
+ // src/hooks/onboarding-attempt.ts
22442
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
22443
+ import { dirname as dirname12 } from "node:path";
22444
+ function readOnboardingAttempt() {
22445
+ const path3 = onboardingAttemptFile();
22446
+ if (!existsSync24(path3))
22447
+ return null;
22448
+ try {
22449
+ const raw = JSON.parse(readFileSync19(path3, "utf8"));
22450
+ if (raw.schemaVersion !== SCHEMA_VERSION3)
22451
+ return null;
22452
+ if (typeof raw.reason !== "string" || typeof raw.cliVersion !== "string")
22453
+ return null;
22454
+ return {
22455
+ schemaVersion: SCHEMA_VERSION3,
22456
+ reason: raw.reason,
22457
+ cliVersion: raw.cliVersion,
22458
+ daemonStatus: typeof raw.daemonStatus === "string" ? raw.daemonStatus : "",
22459
+ at: typeof raw.at === "number" ? raw.at : 0
22460
+ };
22461
+ } catch {
22462
+ return null;
22463
+ }
22464
+ }
22465
+ function recordOnboardingAttempt(reason, cliVersion, daemonStatus, now = Date.now()) {
22466
+ try {
22467
+ const path3 = onboardingAttemptFile();
22468
+ mkdirSync14(dirname12(path3), { recursive: true });
22469
+ const record = {
22470
+ schemaVersion: SCHEMA_VERSION3,
22471
+ reason,
22472
+ cliVersion,
22473
+ daemonStatus,
22474
+ at: now
22475
+ };
22476
+ writeFileSync11(path3, JSON.stringify(record, null, 2), "utf8");
22477
+ } catch {}
22478
+ }
22479
+ function clearOnboardingAttempt() {
22480
+ try {
22481
+ rmSync8(onboardingAttemptFile(), { force: true });
22482
+ } catch {}
22483
+ }
22484
+ function blockerCleared(attempt, probe) {
22485
+ switch (attempt.reason) {
22486
+ case "needs_root":
22487
+ return probe.canElevate();
22488
+ case "daemon_failed":
22489
+ return probe.daemonStatus() !== attempt.daemonStatus || probe.canElevate();
22490
+ case "cancelled":
22491
+ return probe.cliVersion !== attempt.cliVersion;
22492
+ case "not_a_tty":
22493
+ case "running_as_sudo":
22494
+ return true;
22495
+ default:
22496
+ return true;
22497
+ }
22498
+ }
22499
+ function attemptHintLines(attempt) {
22500
+ const why = {
22501
+ needs_root: "it needs root to install the failproofaid service",
22502
+ daemon_failed: "the failproofaid service could not be started",
22503
+ cancelled: "it was cancelled",
22504
+ not_a_tty: "there was no terminal to ask in",
22505
+ running_as_sudo: "it was run under sudo"
22506
+ };
22507
+ const detail = why[attempt.reason] ?? "it did not finish";
22508
+ return [
22509
+ ``,
22510
+ `[failproofai] Setup is not finished — ${detail}.`,
22511
+ ` Run \`failproofai config\` when you are ready.`,
22512
+ ``
22513
+ ];
22514
+ }
22515
+ var SCHEMA_VERSION3 = 1;
22516
+ var init_onboarding_attempt = __esm(() => {
22517
+ init_fp_home();
22518
+ });
22519
+
22441
22520
  // src/hooks/onboarding-lock.ts
22442
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync19, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
22521
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync20, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
22443
22522
  import { homedir as homedir26 } from "node:os";
22444
22523
  function onboardingLockPath(home = homedir26()) {
22445
22524
  return onboardingLockFile(home);
@@ -22456,7 +22535,7 @@ function processAlive(pid) {
22456
22535
  }
22457
22536
  function readLock(path3) {
22458
22537
  try {
22459
- const parsed = JSON.parse(readFileSync19(path3, "utf8"));
22538
+ const parsed = JSON.parse(readFileSync20(path3, "utf8"));
22460
22539
  if (typeof parsed.pid !== "number")
22461
22540
  return null;
22462
22541
  return { pid: parsed.pid, startedAt: Number(parsed.startedAt) || 0 };
@@ -22470,9 +22549,9 @@ function acquireOnboardingLock(home = homedir26()) {
22470
22549
  const held = readLock(path3);
22471
22550
  if (held && held.pid !== process.pid && processAlive(held.pid))
22472
22551
  return null;
22473
- mkdirSync14(stateDir(home), { recursive: true });
22552
+ mkdirSync15(stateDir(home), { recursive: true });
22474
22553
  const body = { pid: process.pid, startedAt: Date.now() };
22475
- writeFileSync11(path3, JSON.stringify(body), "utf8");
22554
+ writeFileSync12(path3, JSON.stringify(body), "utf8");
22476
22555
  let released = false;
22477
22556
  return {
22478
22557
  release() {
@@ -22482,7 +22561,7 @@ function acquireOnboardingLock(home = homedir26()) {
22482
22561
  try {
22483
22562
  const current = readLock(path3);
22484
22563
  if (!current || current.pid === process.pid)
22485
- rmSync8(path3, { force: true });
22564
+ rmSync9(path3, { force: true });
22486
22565
  } catch {}
22487
22566
  }
22488
22567
  };
@@ -22495,7 +22574,7 @@ var init_onboarding_lock = __esm(() => {
22495
22574
  });
22496
22575
 
22497
22576
  // lib/claude-sessions.ts
22498
- import { readdirSync as readdirSync14, statSync as statSync16, existsSync as existsSync25 } from "node:fs";
22577
+ import { readdirSync as readdirSync14, statSync as statSync16, existsSync as existsSync26 } from "node:fs";
22499
22578
  import { join as join28, basename as basename5 } from "node:path";
22500
22579
  function getClaudeProjectsRoot() {
22501
22580
  return getClaudeProjectsPath();
@@ -22542,7 +22621,7 @@ function listClaudeTranscripts(project) {
22542
22621
  } catch {}
22543
22622
  } else if (entry.isDirectory() && UUID_RE2.test(entry.name)) {
22544
22623
  const subDir = join28(project.path, entry.name, "subagents");
22545
- if (!existsSync25(subDir))
22624
+ if (!existsSync26(subDir))
22546
22625
  continue;
22547
22626
  let subEntries;
22548
22627
  try {
@@ -23817,7 +23896,7 @@ var init_features = __esm(() => {
23817
23896
 
23818
23897
  // src/audit/cache.ts
23819
23898
  import { createHash as createHash6 } from "node:crypto";
23820
- import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync20, writeFileSync as writeFileSync12, chmodSync as chmodSync4 } from "node:fs";
23899
+ import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync21, writeFileSync as writeFileSync13, chmodSync as chmodSync4 } from "node:fs";
23821
23900
  import { join as join29 } from "node:path";
23822
23901
  function getEngineVersion() {
23823
23902
  if (cachedEngineVersion)
@@ -23844,10 +23923,10 @@ function readCachedTranscriptResult(transcriptPath, mtimeMs, sizeBytes) {
23844
23923
  if (sizeBytes === 0)
23845
23924
  return null;
23846
23925
  const cachePath = getCachePathFor(transcriptPath);
23847
- if (!existsSync26(cachePath))
23926
+ if (!existsSync27(cachePath))
23848
23927
  return null;
23849
23928
  try {
23850
- const raw = readFileSync20(cachePath, "utf-8");
23929
+ const raw = readFileSync21(cachePath, "utf-8");
23851
23930
  const entry = JSON.parse(raw);
23852
23931
  if (entry.schemaVersion !== CACHE_SCHEMA_VERSION)
23853
23932
  return null;
@@ -23871,7 +23950,7 @@ function writeCachedTranscriptResult(transcriptPath, mtimeMs, sizeBytes, result)
23871
23950
  return;
23872
23951
  const cachePath = getCachePathFor(transcriptPath);
23873
23952
  try {
23874
- mkdirSync15(auditCacheDir(), { recursive: true });
23953
+ mkdirSync16(auditCacheDir(), { recursive: true });
23875
23954
  const entry = {
23876
23955
  schemaVersion: CACHE_SCHEMA_VERSION,
23877
23956
  cachedAt: Date.now(),
@@ -23881,7 +23960,7 @@ function writeCachedTranscriptResult(transcriptPath, mtimeMs, sizeBytes, result)
23881
23960
  detectorVersion: getDetectorVersion(),
23882
23961
  result
23883
23962
  };
23884
- writeFileSync12(cachePath, JSON.stringify(entry), { encoding: "utf-8", mode: 384 });
23963
+ writeFileSync13(cachePath, JSON.stringify(entry), { encoding: "utf-8", mode: 384 });
23885
23964
  try {
23886
23965
  chmodSync4(cachePath, 384);
23887
23966
  } catch {}
@@ -24247,11 +24326,11 @@ var init_audit = __esm(() => {
24247
24326
  import {
24248
24327
  closeSync,
24249
24328
  linkSync,
24250
- mkdirSync as mkdirSync16,
24329
+ mkdirSync as mkdirSync17,
24251
24330
  openSync,
24252
- readFileSync as readFileSync21,
24331
+ readFileSync as readFileSync22,
24253
24332
  unlinkSync as unlinkSync4,
24254
- writeFileSync as writeFileSync13,
24333
+ writeFileSync as writeFileSync14,
24255
24334
  writeSync
24256
24335
  } from "node:fs";
24257
24336
  function acquireAuditLock(source, now = Date.now()) {
@@ -24310,7 +24389,7 @@ function handleFor(path3, info) {
24310
24389
  }
24311
24390
  function readLock2(path3) {
24312
24391
  try {
24313
- const parsed = JSON.parse(readFileSync21(path3, "utf8"));
24392
+ const parsed = JSON.parse(readFileSync22(path3, "utf8"));
24314
24393
  if (typeof parsed?.pid !== "number" || typeof parsed?.startedAt !== "number")
24315
24394
  return null;
24316
24395
  return {
@@ -24326,7 +24405,7 @@ function writeLockExclusive(path3, info) {
24326
24405
  const body = JSON.stringify(info);
24327
24406
  const staging = `${path3}.${process.pid}.tmp`;
24328
24407
  try {
24329
- writeFileSync13(staging, body, { mode: 384 });
24408
+ writeFileSync14(staging, body, { mode: 384 });
24330
24409
  } catch {
24331
24410
  return false;
24332
24411
  }
@@ -24360,8 +24439,8 @@ function createThenWrite(path3, body) {
24360
24439
  }
24361
24440
  }
24362
24441
  function ensureRunDir() {
24363
- mkdirSync16(failproofaiHome(), { recursive: true });
24364
- mkdirSync16(runDir(), { recursive: true, mode: 448 });
24442
+ mkdirSync17(failproofaiHome(), { recursive: true });
24443
+ mkdirSync17(runDir(), { recursive: true, mode: 448 });
24365
24444
  }
24366
24445
  var AUDIT_LOCK_MAX_AGE_MS;
24367
24446
  var init_audit_lock = __esm(() => {
@@ -24525,26 +24604,26 @@ function parseScriptArgs(argv) {
24525
24604
  var init_parse_script_args = () => {};
24526
24605
 
24527
24606
  // scripts/install-diagnosis.mjs
24528
- import { existsSync as existsSync27, readFileSync as readFileSync22, realpathSync as realpathSync3 } from "node:fs";
24529
- import { dirname as dirname12, resolve as resolve21 } from "node:path";
24607
+ import { existsSync as existsSync28, readFileSync as readFileSync23, realpathSync as realpathSync3 } from "node:fs";
24608
+ import { dirname as dirname13, resolve as resolve21 } from "node:path";
24530
24609
  import { homedir as homedir27, platform as platform4 } from "node:os";
24531
24610
  import { spawnSync } from "node:child_process";
24532
24611
  function findPackageRoot(start) {
24533
24612
  try {
24534
24613
  let dir = realpathSync3(start);
24535
- if (existsSync27(dir) && !existsSync27(resolve21(dir, "package.json"))) {
24536
- dir = dirname12(dir);
24614
+ if (existsSync28(dir) && !existsSync28(resolve21(dir, "package.json"))) {
24615
+ dir = dirname13(dir);
24537
24616
  }
24538
24617
  while (true) {
24539
24618
  const pkgPath = resolve21(dir, "package.json");
24540
- if (existsSync27(pkgPath)) {
24619
+ if (existsSync28(pkgPath)) {
24541
24620
  try {
24542
- const pkg = JSON.parse(readFileSync22(pkgPath, "utf8"));
24621
+ const pkg = JSON.parse(readFileSync23(pkgPath, "utf8"));
24543
24622
  if (pkg.name === PKG_NAME)
24544
24623
  return dir;
24545
24624
  } catch {}
24546
24625
  }
24547
- const parent = dirname12(dir);
24626
+ const parent = dirname13(dir);
24548
24627
  if (parent === dir)
24549
24628
  return null;
24550
24629
  dir = parent;
@@ -24557,7 +24636,7 @@ function readPackageVersion(packageRoot) {
24557
24636
  if (!packageRoot)
24558
24637
  return null;
24559
24638
  try {
24560
- const pkg = JSON.parse(readFileSync22(resolve21(packageRoot, "package.json"), "utf8"));
24639
+ const pkg = JSON.parse(readFileSync23(resolve21(packageRoot, "package.json"), "utf8"));
24561
24640
  return typeof pkg.version === "string" ? pkg.version : null;
24562
24641
  } catch {
24563
24642
  return null;
@@ -24584,7 +24663,7 @@ function locateNpmGlobal() {
24584
24663
  if (!root)
24585
24664
  return null;
24586
24665
  const candidate = resolve21(root, PKG_NAME);
24587
- return existsSync27(resolve21(candidate, "package.json")) ? candidate : null;
24666
+ return existsSync28(resolve21(candidate, "package.json")) ? candidate : null;
24588
24667
  } catch {
24589
24668
  return null;
24590
24669
  }
@@ -24592,7 +24671,7 @@ function locateNpmGlobal() {
24592
24671
  function locateBunGlobal() {
24593
24672
  try {
24594
24673
  const candidate = resolve21(homedir27(), ".bun", "install", "global", "node_modules", PKG_NAME);
24595
- return existsSync27(resolve21(candidate, "package.json")) ? candidate : null;
24674
+ return existsSync28(resolve21(candidate, "package.json")) ? candidate : null;
24596
24675
  } catch {
24597
24676
  return null;
24598
24677
  }
@@ -24699,8 +24778,8 @@ __export(exports_launch, {
24699
24778
  launch: () => launch
24700
24779
  });
24701
24780
  import { spawn as spawn2 } from "child_process";
24702
- import { realpathSync as realpathSync4, existsSync as existsSync28 } from "node:fs";
24703
- import { resolve as resolve22, dirname as dirname13 } from "node:path";
24781
+ import { realpathSync as realpathSync4, existsSync as existsSync29 } from "node:fs";
24782
+ import { resolve as resolve22, dirname as dirname14 } from "node:path";
24704
24783
  import { fileURLToPath as fileURLToPath2 } from "node:url";
24705
24784
  import { createInterface } from "node:readline";
24706
24785
  function launch(mode) {
@@ -24724,9 +24803,9 @@ function launch(mode) {
24724
24803
  process.env.PORT = port;
24725
24804
  process.env.HOSTNAME = bindHost;
24726
24805
  cmd = "node";
24727
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve22(dirname13(realpathSync4(fileURLToPath2(import.meta.url))), "..");
24806
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve22(dirname14(realpathSync4(fileURLToPath2(import.meta.url))), "..");
24728
24807
  const serverJsPath = resolve22(packageRoot, ".next/standalone/server.js");
24729
- if (!existsSync28(serverJsPath)) {
24808
+ if (!existsSync29(serverJsPath)) {
24730
24809
  let shadowMessage = null;
24731
24810
  try {
24732
24811
  const diag = diagnoseShadow({ selfPackageRoot: packageRoot, selfVersion: version2 });
@@ -25171,9 +25250,9 @@ __export(exports_configure_wizard, {
25171
25250
  buildPresetChoices: () => buildPresetChoices,
25172
25251
  buildAgentChoices: () => buildAgentChoices
25173
25252
  });
25174
- import { existsSync as existsSync29, mkdirSync as mkdirSync17, writeFileSync as writeFileSync14, readFileSync as readFileSync23 } from "node:fs";
25253
+ import { existsSync as existsSync30, mkdirSync as mkdirSync18, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
25175
25254
  import { homedir as homedir28 } from "node:os";
25176
- import { dirname as dirname14, resolve as resolve23, sep as sep5 } from "node:path";
25255
+ import { dirname as dirname15, resolve as resolve23, sep as sep5 } from "node:path";
25177
25256
  async function emit(event, props) {
25178
25257
  try {
25179
25258
  await trackHookEvent2(getInstanceId2(), event, props);
@@ -25280,8 +25359,8 @@ function setCustomPoliciesEnabled(scope, cwd, enabled) {
25280
25359
  const path3 = getConfigPathForScope(scope, cwd);
25281
25360
  let config = {};
25282
25361
  try {
25283
- if (existsSync29(path3))
25284
- config = JSON.parse(readFileSync23(path3, "utf8"));
25362
+ if (existsSync30(path3))
25363
+ config = JSON.parse(readFileSync24(path3, "utf8"));
25285
25364
  } catch {
25286
25365
  return;
25287
25366
  }
@@ -25290,7 +25369,7 @@ function setCustomPoliciesEnabled(scope, cwd, enabled) {
25290
25369
  else
25291
25370
  config.customPoliciesEnabled = false;
25292
25371
  try {
25293
- writeFileSync14(path3, JSON.stringify(config, null, 2) + `
25372
+ writeFileSync15(path3, JSON.stringify(config, null, 2) + `
25294
25373
  `, "utf8");
25295
25374
  } catch {}
25296
25375
  }
@@ -25390,14 +25469,33 @@ function firstRunMarkerPath() {
25390
25469
  return launcherMarker();
25391
25470
  }
25392
25471
  function hasSeenLauncher() {
25393
- return existsSync29(firstRunMarkerPath());
25472
+ return existsSync30(firstRunMarkerPath());
25394
25473
  }
25395
25474
  function markLauncherSeen() {
25396
25475
  try {
25397
- mkdirSync17(dirname14(firstRunMarkerPath()), { recursive: true });
25398
- writeFileSync14(firstRunMarkerPath(), "1", "utf8");
25476
+ mkdirSync18(dirname15(firstRunMarkerPath()), { recursive: true });
25477
+ writeFileSync15(firstRunMarkerPath(), "1", "utf8");
25399
25478
  } catch {}
25400
25479
  }
25480
+ function retryProbe() {
25481
+ return {
25482
+ canElevate: () => {
25483
+ try {
25484
+ return canElevate();
25485
+ } catch {
25486
+ return false;
25487
+ }
25488
+ },
25489
+ daemonStatus: () => {
25490
+ try {
25491
+ return daemonServiceStatus();
25492
+ } catch {
25493
+ return "";
25494
+ }
25495
+ },
25496
+ cliVersion: version2
25497
+ };
25498
+ }
25401
25499
  async function maybeFirstRunConfigure(io = {}, opts = {}) {
25402
25500
  if (process.env.FAILPROOFAI_NO_FIRST_RUN === "1")
25403
25501
  return false;
@@ -25409,6 +25507,13 @@ async function maybeFirstRunConfigure(io = {}, opts = {}) {
25409
25507
  markLauncherSeen();
25410
25508
  return false;
25411
25509
  }
25510
+ const attempt = opts.force ? null : readOnboardingAttempt();
25511
+ if (attempt && !blockerCleared(attempt, retryProbe())) {
25512
+ for (const line of attemptHintLines(attempt))
25513
+ stdout.write(`${line}
25514
+ `);
25515
+ return false;
25516
+ }
25412
25517
  if (!stdin.isTTY || !stdout.isTTY) {
25413
25518
  stdout.write(`
25414
25519
  [failproofai] Not set up yet — run \`failproofai config\` to get started.
@@ -25427,6 +25532,9 @@ async function maybeFirstRunConfigure(io = {}, opts = {}) {
25427
25532
  try {
25428
25533
  emit("first_run_configure_shown", {});
25429
25534
  const result = await runConfigureWizard(io);
25535
+ if (!result.applied && result.abort) {
25536
+ recordOnboardingAttempt(result.abort, version2, daemonServiceStatus());
25537
+ }
25430
25538
  if (result.applied && opts.postSetupAudit !== false) {
25431
25539
  try {
25432
25540
  const { runPostSetupAudit: runPostSetupAudit2 } = await Promise.resolve().then(() => (init_cli(), exports_cli));
@@ -25850,6 +25958,7 @@ Hooks keep enforcing; scheduled audits stay off until it is.
25850
25958
  }
25851
25959
  await applied;
25852
25960
  markLauncherSeen();
25961
+ clearOnboardingAttempt();
25853
25962
  const customNote = customEnabled === true ? " + your custom policies" : customEnabled === false ? " · custom policies DISABLED" : "";
25854
25963
  const daemonNote = daemonInstalled ? " · daemon on" : "";
25855
25964
  const cloudNote = connected ? " · reporting on" : "";
@@ -25885,6 +25994,7 @@ var init_configure_wizard = __esm(() => {
25885
25994
  init_fp_home();
25886
25995
  init_daemon_download();
25887
25996
  init_package();
25997
+ init_onboarding_attempt();
25888
25998
  init_onboarding_lock();
25889
25999
  });
25890
26000
 
@@ -25898,7 +26008,7 @@ __export(exports_manager, {
25898
26008
  getSettingsPath: () => getSettingsPath2
25899
26009
  });
25900
26010
  import { execSync as execSync7 } from "node:child_process";
25901
- import { existsSync as existsSync30 } from "node:fs";
26011
+ import { existsSync as existsSync31 } from "node:fs";
25902
26012
  import { resolve as resolve24, basename as basename6 } from "node:path";
25903
26013
  import { platform as platform5, arch as arch4, release as release3, hostname as hostname5 } from "node:os";
25904
26014
  function getSettingsPath2(scope, cwd) {
@@ -26009,7 +26119,7 @@ async function installHooksImpl2(policyNames, scope = "user", cwd, includeBeta =
26009
26119
  } else if (customPoliciesPath && (typeof customPoliciesPath === "string" || customPoliciesPath.length > 0)) {
26010
26120
  const incoming = (typeof customPoliciesPath === "string" ? [customPoliciesPath] : customPoliciesPath).map((path3) => resolve24(path3));
26011
26121
  const carried = (previousConfig.customPoliciesPaths ?? (previousConfig.customPoliciesPath ? [previousConfig.customPoliciesPath] : [])).map((path3) => resolve24(path3)).filter((path3) => {
26012
- if (existsSync30(path3))
26122
+ if (existsSync31(path3))
26013
26123
  return true;
26014
26124
  console.log(`Dropping custom policies path (file no longer exists): ${path3}`);
26015
26125
  return false;
@@ -26201,7 +26311,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
26201
26311
  const scopesToRemove = scope === "all" ? [...integration.scopes] : integration.scopes.includes(scope) ? [scope] : [];
26202
26312
  for (const s of scopesToRemove) {
26203
26313
  const settingsPaths = settingsPathsFor(integration, s, cwd);
26204
- const existing = settingsPaths.filter((p) => existsSync30(p));
26314
+ const existing = settingsPaths.filter((p) => existsSync31(p));
26205
26315
  if (existing.length === 0) {
26206
26316
  if (scope !== "all" && selectedClis.length === 1) {
26207
26317
  console.log("No settings file found. Nothing to remove.");
@@ -26402,7 +26512,7 @@ Failproof AI Hook Policies
26402
26512
  for (const path3 of explicitPaths) {
26403
26513
  const absPath = resolve24(findProjectConfigDir(cwd ?? process.cwd()), path3);
26404
26514
  console.log(` ${absPath}`);
26405
- if (!existsSync30(absPath)) {
26515
+ if (!existsSync31(absPath)) {
26406
26516
  console.log(` \x1B[31m✗ File not found: ${absPath}\x1B[0m`);
26407
26517
  continue;
26408
26518
  }
@@ -27537,8 +27647,8 @@ __export(exports_launch2, {
27537
27647
  launch: () => launch2
27538
27648
  });
27539
27649
  import { spawn as spawn3 } from "child_process";
27540
- import { realpathSync as realpathSync5, existsSync as existsSync31 } from "node:fs";
27541
- import { resolve as resolve25, dirname as dirname15 } from "node:path";
27650
+ import { realpathSync as realpathSync5, existsSync as existsSync32 } from "node:fs";
27651
+ import { resolve as resolve25, dirname as dirname16 } from "node:path";
27542
27652
  import { fileURLToPath as fileURLToPath3 } from "node:url";
27543
27653
  import { createInterface as createInterface2 } from "node:readline";
27544
27654
  function launch2(mode) {
@@ -27562,9 +27672,9 @@ function launch2(mode) {
27562
27672
  process.env.PORT = port;
27563
27673
  process.env.HOSTNAME = bindHost;
27564
27674
  cmd = "node";
27565
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve25(dirname15(realpathSync5(fileURLToPath3(import.meta.url))), "..");
27675
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve25(dirname16(realpathSync5(fileURLToPath3(import.meta.url))), "..");
27566
27676
  const serverJsPath = resolve25(packageRoot, ".next/standalone/server.js");
27567
- if (!existsSync31(serverJsPath)) {
27677
+ if (!existsSync32(serverJsPath)) {
27568
27678
  let shadowMessage = null;
27569
27679
  try {
27570
27680
  const diag = diagnoseShadow({ selfPackageRoot: packageRoot, selfVersion: version2 });
@@ -27661,17 +27771,17 @@ var init_cli_error2 = __esm(() => {
27661
27771
 
27662
27772
  // bin/failproofai.mjs
27663
27773
  import { realpathSync as realpathSync6 } from "fs";
27664
- import { dirname as dirname16, resolve as resolve26 } from "path";
27774
+ import { dirname as dirname17, resolve as resolve26 } from "path";
27665
27775
  import { fileURLToPath as fileURLToPath4 } from "url";
27666
27776
  // package.json
27667
- var version = "1.0.0-beta.6";
27777
+ var version = "1.0.0-beta.7";
27668
27778
 
27669
27779
  // bin/failproofai.mjs
27670
27780
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
27671
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve26(dirname16(realpathSync6(fileURLToPath4(import.meta.url))), "..");
27781
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve26(dirname17(realpathSync6(fileURLToPath4(import.meta.url))), "..");
27672
27782
  }
27673
27783
  if (!process.env.FAILPROOFAI_DIST_PATH) {
27674
- process.env.FAILPROOFAI_DIST_PATH = resolve26(dirname16(realpathSync6(fileURLToPath4(import.meta.url))), "..", "dist");
27784
+ process.env.FAILPROOFAI_DIST_PATH = resolve26(dirname17(realpathSync6(fileURLToPath4(import.meta.url))), "..", "dist");
27675
27785
  }
27676
27786
  var args = process.argv.slice(2);
27677
27787
  if (args[0] === "p")
@@ -27731,15 +27841,10 @@ if (hookIdx >= 0) {
27731
27841
  let result;
27732
27842
  if (attempt.ok) {
27733
27843
  result = attempt.response;
27734
- } else if (attempt.failure === "protocol-mismatch") {
27735
- console.error("[failproofai] failproofaid speaks a different protocol version than this CLI \u2014 " + "evaluating in-process instead (slower, identical policies). Run `failproofai config` to update the daemon.");
27736
- result = await evaluateHookEvent2(eventType, cli, stdinRead.payload);
27737
27844
  } else {
27845
+ const reason = attempt.failure === "protocol-mismatch" ? "failproofaid is running a different protocol version than this CLI, so it cannot evaluate this call. Run `failproofai config` to update the daemon." : "failproofaid could not be reached. This machine is configured to run hooks through it " + "\u2014 check the daemon (see `failproofai config`) rather than retrying blindly.";
27738
27846
  result = await evaluateHookEvent2(eventType, cli, stdinRead.payload, {
27739
- forceDecision: {
27740
- decision: "deny",
27741
- reason: "failproofaid could not be reached. This machine is configured to run hooks through it " + "\u2014 check the daemon (see `failproofai config`) rather than retrying blindly."
27742
- }
27847
+ forceDecision: { decision: "deny", reason }
27743
27848
  });
27744
27849
  }
27745
27850
  if (result.stdout)
package/dist/worker.mjs CHANGED
@@ -2434,7 +2434,7 @@ var init_builtin_policies = __esm(() => {
2434
2434
  });
2435
2435
 
2436
2436
  // package.json
2437
- var version = "1.0.0-beta.6";
2437
+ var version = "1.0.0-beta.7";
2438
2438
  var init_package = () => {};
2439
2439
 
2440
2440
  // src/posthog-key.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.7",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -114,9 +114,9 @@
114
114
  "sharp": "0.35.0"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@failproofai/failproofaid-linux-x64": "1.0.0-beta.6",
118
- "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.6",
119
- "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.6",
120
- "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.6"
117
+ "@failproofai/failproofaid-linux-x64": "1.0.0-beta.7",
118
+ "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.7",
119
+ "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.7",
120
+ "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.7"
121
121
  }
122
122
  }
@@ -61,6 +61,7 @@ import { discoverPolicyFiles, findSkippedPolicyFiles } from "./custom-hooks-load
61
61
  import { trackHookEvent } from "./hook-telemetry";
62
62
  import { getInstanceId } from "../../lib/telemetry-id";
63
63
  import {
64
+ canElevate,
64
65
  isDaemonSupportedPlatform,
65
66
  installDaemonService,
66
67
  daemonServiceFilePath,
@@ -99,6 +100,14 @@ import {
99
100
  import { customPoliciesDir, launcherMarker } from "./fp-home";
100
101
  import { pruneOldDaemonBinaries } from "./daemon-download";
101
102
  import { version as cliVersion } from "../../package.json";
103
+ import {
104
+ attemptHintLines,
105
+ blockerCleared,
106
+ clearOnboardingAttempt,
107
+ readOnboardingAttempt,
108
+ recordOnboardingAttempt,
109
+ type RetryProbe,
110
+ } from "./onboarding-attempt";
102
111
  import { acquireOnboardingLock } from "./onboarding-lock";
103
112
 
104
113
  export interface WizardIO {
@@ -552,6 +561,33 @@ export interface FirstRunOptions {
552
561
  force?: boolean;
553
562
  }
554
563
 
564
+ /**
565
+ * The live values `blockerCleared` compares an earlier attempt against.
566
+ *
567
+ * Built here rather than imported into `onboarding-attempt.ts` so that module
568
+ * stays pure and unit-testable without a service manager or a sudo binary.
569
+ */
570
+ function retryProbe(): RetryProbe {
571
+ return {
572
+ canElevate: () => {
573
+ try {
574
+ return canElevate();
575
+ } catch {
576
+ // No sudo binary at all is a blocker that has not cleared.
577
+ return false;
578
+ }
579
+ },
580
+ daemonStatus: () => {
581
+ try {
582
+ return daemonServiceStatus();
583
+ } catch {
584
+ return "";
585
+ }
586
+ },
587
+ cliVersion,
588
+ };
589
+ }
590
+
555
591
  export async function maybeFirstRunConfigure(
556
592
  io: WizardIO = {},
557
593
  opts: FirstRunOptions = {},
@@ -572,6 +608,17 @@ export async function maybeFirstRunConfigure(
572
608
  return false;
573
609
  }
574
610
 
611
+ // A previous attempt that ABORTED. Setup writes nothing on those paths, by
612
+ // design, so without this record the machine is indistinguishable from one
613
+ // that has never been offered setup — and the wizard relaunches on every
614
+ // command forever. `--force` (an explicit `failproofai config`) never reaches
615
+ // here, so asking for setup by name always gets it.
616
+ const attempt = opts.force ? null : readOnboardingAttempt();
617
+ if (attempt && !blockerCleared(attempt, retryProbe())) {
618
+ for (const line of attemptHintLines(attempt)) stdout.write(`${line}\n`);
619
+ return false;
620
+ }
621
+
575
622
  if (!stdin.isTTY || !stdout.isTTY) {
576
623
  // Never launch a wizard nobody can answer. This is the CI / piped path,
577
624
  // and it must stay a hint rather than a failure: the command the user
@@ -600,6 +647,11 @@ export async function maybeFirstRunConfigure(
600
647
  // an apply — so cancelling keeps offering setup on the next run rather
601
648
  // than silently never mentioning it again.
602
649
  const result = await runConfigureWizard(io);
650
+ // Remember WHY, so the next command can hint instead of relaunching. Only
651
+ // on an abort: a completed apply clears the record below.
652
+ if (!result.applied && result.abort) {
653
+ recordOnboardingAttempt(result.abort, cliVersion, daemonServiceStatus());
654
+ }
603
655
 
604
656
  // Onboarding-only: after a completed first-run setup, run the audit pipeline
605
657
  // (scan + cache warm) before the caller boots the dashboard. The explicit
@@ -1306,6 +1358,8 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1306
1358
  // Only now — a completed apply — is the launcher considered "seen", so
1307
1359
  // first-run onboarding stops offering itself on every command.
1308
1360
  markLauncherSeen();
1361
+ // And any record of an earlier failure is now false: this machine got set up.
1362
+ clearOnboardingAttempt();
1309
1363
 
1310
1364
  // Keep this inside a standard 80-column terminal. `writeLines` truncates with
1311
1365
  // a hard cut and no ellipsis, so an over-long line doesn't just lose its tail
@@ -450,8 +450,16 @@ export function primeElevation(): boolean {
450
450
  }
451
451
  }
452
452
 
453
- /** True when privileged commands can run without prompting for a password. */
454
- function canElevate(): boolean {
453
+ /**
454
+ * True when privileged commands can run without prompting for a password.
455
+ *
456
+ * Exported because onboarding re-checks it: `needs_root` is the most common
457
+ * reason setup aborts, and it is the one most likely to stop being true (the
458
+ * user gets sudo rights, or primes their timestamp in another terminal). One
459
+ * `sudo -n true`, no prompt, milliseconds — and never on the hook path, which
460
+ * does not reach the first-run gate at all.
461
+ */
462
+ export function canElevate(): boolean {
455
463
  if (typeof process.getuid === "function" && process.getuid() === 0) return true;
456
464
  try {
457
465
  execFileSync("sudo", ["-n", "true"], { stdio: "ignore", timeout: SERVICE_CMD_TIMEOUT_MS });