@indigoai-us/hq-cli 5.345.199 → 5.345.201

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 (61) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +6 -4
  3. package/dist/command-catalog.generated.d.ts +44 -0
  4. package/dist/command-catalog.generated.js +52 -0
  5. package/dist/commands/bot-runtime.d.ts +35 -0
  6. package/dist/commands/bot-runtime.js +115 -0
  7. package/dist/commands/bot.d.ts +53 -0
  8. package/dist/commands/bot.js +239 -30
  9. package/dist/commands/cloud.d.ts +39 -0
  10. package/dist/commands/cloud.js +173 -82
  11. package/dist/commands/creators.js +2 -1
  12. package/dist/commands/deploy-domains.js +8 -4
  13. package/dist/commands/library-remove.js +2 -1
  14. package/dist/commands/library.js +5 -4
  15. package/dist/commands/mesh.d.ts +0 -7
  16. package/dist/commands/mesh.js +5 -1
  17. package/dist/commands/sync-held.js +2 -2
  18. package/dist/lib/bot/api.d.ts +8 -0
  19. package/dist/lib/bot/api.js +9 -0
  20. package/dist/lib/bot/fleet-download.d.ts +27 -0
  21. package/dist/lib/bot/fleet-download.js +162 -0
  22. package/dist/lib/bot/fleet-local.d.ts +79 -2
  23. package/dist/lib/bot/fleet-local.js +246 -74
  24. package/dist/lib/bot/fleet-python.d.ts +2 -0
  25. package/dist/lib/bot/fleet-python.js +29 -0
  26. package/dist/lib/bot/jobs/source-report.d.ts +31 -0
  27. package/dist/lib/bot/jobs/source-report.js +276 -0
  28. package/dist/lib/bot/legacy-convert.d.ts +107 -4
  29. package/dist/lib/bot/legacy-convert.js +159 -13
  30. package/dist/lib/bot/legacy-provider-login.d.ts +34 -0
  31. package/dist/lib/bot/legacy-provider-login.js +144 -0
  32. package/dist/lib/bot/run.js +20 -1
  33. package/dist/lib/doctor/checks/bots.d.ts +4 -0
  34. package/dist/lib/doctor/checks/bots.js +33 -0
  35. package/dist/lib/lanes/mesh-events.js +3 -1
  36. package/dist/lib/lanes/store.js +7 -0
  37. package/dist/lib/lanes/types.d.ts +2 -0
  38. package/dist/lib/lanes/types.js +1 -0
  39. package/dist/lib/lanes/watcher.d.ts +0 -33
  40. package/dist/lib/lanes/watcher.js +7 -0
  41. package/dist/lib/mesh/api.js +2 -1
  42. package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
  43. package/dist/lib/mesh/live/daemon/run.js +3 -0
  44. package/dist/lib/mesh/live/emit.d.ts +2 -11
  45. package/dist/lib/mesh/live/emit.js +20 -2
  46. package/dist/lib/mesh/live/flush.d.ts +2 -0
  47. package/dist/lib/mesh/live/flush.js +34 -3
  48. package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
  49. package/dist/lib/mesh/live/reporting-privacy.js +47 -0
  50. package/dist/lib/work-context/company.d.ts +2 -3
  51. package/dist/lib/work-context/company.js +4 -22
  52. package/dist/secrets-env-action.js +5 -2
  53. package/dist/utils/cognito-callback-port.d.ts +2 -0
  54. package/dist/utils/cognito-callback-port.js +10 -5
  55. package/dist/utils/cognito-config.js +6 -1
  56. package/dist/utils/cognito-session.js +6 -1
  57. package/dist/utils/interactive-caller.d.ts +8 -0
  58. package/dist/utils/interactive-caller.js +10 -0
  59. package/dist/utils/login-browser.js +2 -1
  60. package/dist/utils/resident-package-preload.js +57 -4
  61. package/package.json +7 -7
@@ -1,5 +1,8 @@
1
1
  import { withLocalJobs } from "../lib/bot/local-jobs.js";
2
- import { fleetMoveFailurePath, fleetMoveRecentlyFailed, legacyConversionDecision, writeFleetMoveFailure } from "../lib/bot/legacy-convert.js";
2
+ import { clearFleetMoveRecords, fleetMoveHoldPath, fleetMoveRecentlyFailed, holdReportDue, legacyConversionDecision, legacyHoldFailureReport, legacyHoldNotice, legacyHoldNotifiesOwner, legacyMoveState, ownerHoldNoticeDue, readFleetMoveFailure, readFleetMoveHold, writeFleetMoveFailure, writeFleetMoveHold, } from "../lib/bot/legacy-convert.js";
3
+ import { legacyProviderLoginProbe } from "../lib/bot/legacy-provider-login.js";
4
+ import { randomUUID } from "node:crypto";
5
+ import { CLI_VERSION } from "../cli-version.js";
3
6
  import { runSignals } from "../lib/signals/engine.js";
4
7
  import { BOT_SIGNALS_ENGINE_JOB_FLAG, signalsEngineForJob } from "../lib/bot/signals-engine-flag.js";
5
8
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
@@ -70,7 +73,9 @@ import { unresolvedLastFailure } from "../lib/doctor/checks/bots.js";
70
73
  import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
71
74
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
72
75
  import { BOT_LOGIN_RUNTIMES, botAuthEnv, botRuntimeAuth, capacityAccountHome, describeBotAuth, formatBotAuth, isBotLoginRuntime, restoreBotSessions, setAsideBotSessions, signInBotRuntime, signOutBotRuntime, withRuntimeAuth, } from "../lib/bot/runtime-auth.js";
73
- import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, HQ_FLEET_LOCAL_RELEASE, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetLocalServiceInstalled, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetOwnerCompanies, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, installTargetFleetLocalDistribution, resolveFleetLocalPointerTarget, resolveFleetLocalTargetRelease, fleetLocalProfileDir, fleetProfileOwnerCompanies, fleetLocalChannels, readFleetLocalGatewayState, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
76
+ import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, HQ_FLEET_LOCAL_RELEASE, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetLocalServiceInstalled, fleetProviderForRuntime, importFleetLocalMachineCredential, importFleetLocalProviderLogin, fleetProviderNeedsSignIn, fleetOwnerCompanies, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, installTargetFleetLocalDistribution, resolveFleetLocalPointerTarget, resolveFleetLocalTargetRelease, fleetLocalProfileDir, fleetProfileOwnerCompanies, fleetLocalChannels, readFleetLocalGatewayState, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
77
+ import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
78
+ import { fleetInstallProgressReporter, runBotRuntimeInstall, runBotRuntimeStatus } from "./bot-runtime.js";
74
79
  function collectRepeated(value, previous) {
75
80
  return [...previous, value];
76
81
  }
@@ -332,7 +337,15 @@ export function botTokenSupplier(dir) {
332
337
  process.env.HQ_MACHINE_TOKEN_STATE_DIR = botTokenStateDir(dir);
333
338
  return () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
334
339
  }
335
- async function ownerToken(interactive = process.stdin.isTTY === true) {
340
+ /**
341
+ * Whether `hq bot` may open a browser sign-in for its owner: a TTY on stdin,
342
+ * and the shared interactive-caller gate (no lane, daemon, bot runtime, or
343
+ * HQ_NO_BROWSER_LOGIN child).
344
+ */
345
+ export function ownerSignInAllowed() {
346
+ return gatedInteractiveLogin(process.stdin.isTTY === true);
347
+ }
348
+ async function ownerToken(interactive = ownerSignInAllowed()) {
336
349
  if (peekHqApiKey()) {
337
350
  throw Object.assign(new Error("hq bot needs your HQ login session (HQ_API_KEY is not supported here). Run `hq login`."), {
338
351
  expected: true,
@@ -358,11 +371,39 @@ export function runtimeSignedIn(status, processAlive) {
358
371
  return false;
359
372
  return status.runtimeSignInCheck?.signedIn ?? null;
360
373
  }
374
+ /**
375
+ * Whether the bot's model provider must be signed in before it can run, from
376
+ * what this computer knows: the bot's settings, its last reported failure or
377
+ * sign-in check, and (legacy) why it has not moved to the HQ Fleet Runtime.
378
+ */
379
+ export function brainSignInFor(name, config, status, legacy) {
380
+ const provider = config.runtime;
381
+ const command = fleetAuthCommand(name, config);
382
+ if (legacy && !legacy.decision.convert && legacyHoldNotifiesOwner(legacy.decision.reason)) {
383
+ return { provider, needed: true, reason: legacy.notice?.cause ?? legacy.decision.detail, command };
384
+ }
385
+ if (config.fleetAuthPending === true)
386
+ return { provider, needed: true, reason: `${name} has not signed in to ${runtimeDisplayName(provider)} yet.`, command };
387
+ // A legacy bot that moves at its next start needs no sign-in, whatever its old runner last reported.
388
+ if (legacy?.decision.convert)
389
+ return null;
390
+ const failure = unresolvedLastFailure(status);
391
+ if (failure?.class === "auth")
392
+ return { provider, needed: true, reason: failure.cause, command };
393
+ if (status?.runtimeSignIn)
394
+ return { provider, needed: true, reason: signedOutReason(name, status.runtimeSignIn), command };
395
+ return null;
396
+ }
397
+ function signedOutReason(name, issue) {
398
+ const tool = runtimeDisplayName(issue.runtime);
399
+ return issue.state === "signed-out" ? `${tool} is not signed in for ${name} on this computer.` : `${tool} needs ${name}'s owner to sign in again on this computer.`;
400
+ }
361
401
  function rowFor(name, opts = {}) {
362
402
  const dir = botDir(name);
363
403
  const config = readBotConfig(dir);
364
404
  const fleet = isFleetRuntimeBot(config);
365
405
  const status = readBotStatus(dir);
406
+ const legacy = fleet ? null : opts.legacy ?? legacyMoveState(dir, config, legacyConvertSeams.providerLogin(dir, config));
366
407
  const pid = fleet ? fleetLocalGatewayPid(config.agentUid) : (status?.pid ?? null);
367
408
  const processAlive = pid !== null && isPidAlive(pid) && (fleet || status?.state === "running");
368
409
  // Turns this bot has taken and not finished: what "thinking" means.
@@ -409,6 +450,8 @@ function rowFor(name, opts = {}) {
409
450
  ...(fleet && config.fleetAuthPending === true ? { needsSignIn: true, authCommand: fleetAuthCommand(name, config) } : {}),
410
451
  ...(fleet && config.fleetServiceInstallPending === true ? { needsSignIn: false, serviceInstallPending: true } : {}),
411
452
  signInCommand: fleet ? fleetAuthCommand(name, config) : signInCommand(config.runtime),
453
+ brainSignIn: brainSignInFor(name, config, status, legacy),
454
+ ...(legacy ? { legacy: legacyStatusJson(legacy) } : {}),
412
455
  runtimeCrash: processAlive ? runtimeCrashSinceStart(status) : null,
413
456
  daemonInstalled,
414
457
  daemonLoaded,
@@ -515,9 +558,12 @@ async function startBot(name, dir, config, output, extra = {}, opts = {}) {
515
558
  throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
516
559
  if (isFleetRuntimeBot(config))
517
560
  return startFleetRuntimeBot(name, dir, config, output, extra);
518
- const converted = opts.keepLegacy ? null : await convertLegacyBotToFleet(name, dir, config, "start");
519
- if (converted)
520
- return reportFleetBotStart(name, dir, converted, output, { ...extra, movedToFleet: true }, true, "started by the HQ Fleet Runtime");
561
+ const move = opts.keepLegacy ? null : await convertLegacyBotToFleet(name, dir, config, "start");
562
+ if (move?.fleet)
563
+ return reportFleetBotStart(name, dir, move.fleet, output, { ...extra, movedToFleet: true }, true, "started by the HQ Fleet Runtime");
564
+ // The app reads these to say the bot is still legacy and, when a sign-in is all it waits for, to offer it.
565
+ if (move?.state)
566
+ extra = { ...extra, movedToFleet: false, legacy: legacyStatusJson(move.state), brainSignIn: brainSignInFor(name, config, readBotStatus(dir), move.state) };
521
567
  if (config.enabled === false)
522
568
  patchBotConfig(dir, { enabled: true });
523
569
  const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
@@ -594,21 +640,37 @@ async function startBot(name, dir, config, output, extra = {}, opts = {}) {
594
640
  * itself starting (launchd, hq daemon or a detached run), before it polls:
595
641
  * its own startup agent is booted out last, detached.
596
642
  */
643
+ /** Seams for the legacy move, replaced in tests. */
644
+ export const legacyConvertSeams = {
645
+ /** Whether the provider sign-in a Codex or Grok legacy bot needs is readable here, as HQ Fleet Local imports it. */
646
+ providerLogin: (dir, config) => legacyProviderLoginProbe(dir, config),
647
+ };
597
648
  async function convertLegacyBotToFleet(name, dir, config, from) {
598
- if (!legacyConversionDecision(config).convert || hasPromotionHold(dir))
599
- return null;
600
- // A launchd or daemon restart loop would otherwise retry a failing install before every start.
649
+ if (hasPromotionHold(dir))
650
+ return { fleet: null, state: null };
651
+ // A launchd or daemon restart loop would otherwise retry a failing install (and probe the sign-in) before every start.
601
652
  if (from === "run" && fleetMoveRecentlyFailed(dir))
602
- return null;
653
+ return { fleet: null, state: null };
654
+ const probe = legacyConvertSeams.providerLogin(dir, config);
655
+ const decision = legacyConversionDecision(config, process.env, probe);
656
+ if (!decision.convert) {
657
+ if (legacyHoldNotifiesOwner(decision.reason))
658
+ await holdLegacyBot(name, dir, config, decision);
659
+ return { fleet: null, state: { decision, notice: legacyHoldNotice(name, config.runtime, decision), lastFailure: readFleetMoveFailure(dir), hold: readFleetMoveHold(dir) } };
660
+ }
661
+ // The hold is over: whatever stops the move from here is a failure, not a hold.
662
+ fs.rmSync(fleetMoveHoldPath(dir), { force: true });
663
+ const ready = () => ({ fleet: null, state: { decision, notice: null, lastFailure: readFleetMoveFailure(dir), hold: null } });
603
664
  if (from === "start") {
604
665
  const running = readBotStatus(dir);
605
666
  if (running && isPidAlive(running.pid) && selfStopProbe(running.pid))
606
- return null;
667
+ return ready();
607
668
  }
608
669
  const log = createBotLogger(dir);
609
670
  const keepLegacy = (step, err) => {
610
- writeFleetMoveFailure(dir);
611
- log("warn", `could not ${step} ${name} on the HQ Fleet Runtime (${err instanceof Error ? err.message : String(err)}); it runs on ${config.runtime} this time and tries again at its next start`);
671
+ const error = err instanceof Error ? err.message : String(err);
672
+ writeFleetMoveFailure(dir, new Date(), { step, error });
673
+ log("warn", `could not ${step} ${name} on the HQ Fleet Runtime (${error}); it runs on ${config.runtime} this time and tries again at its next start`);
612
674
  };
613
675
  let engine;
614
676
  try {
@@ -618,9 +680,27 @@ async function convertLegacyBotToFleet(name, dir, config, from) {
618
680
  }
619
681
  catch (err) {
620
682
  keepLegacy("install", err);
621
- return null;
683
+ return ready();
622
684
  }
623
- const fleet = { ...(readBotConfig(dir) ?? config), engine, enabled: true, fleetServiceInstallPending: false };
685
+ if (decision.signIn) {
686
+ try {
687
+ importFleetLocalProviderLogin(config.agentUid, decision.signIn.runtime, {}, decision.signIn.env ? { env: decision.signIn.env } : {});
688
+ log("info", `imported ${name}'s ${config.runtime} sign-in from this computer for the HQ Fleet Runtime`);
689
+ }
690
+ catch (err) {
691
+ keepLegacy("sign in", err);
692
+ return ready();
693
+ }
694
+ }
695
+ const current = readBotConfig(dir) ?? config;
696
+ const fleet = {
697
+ ...current,
698
+ engine,
699
+ enabled: true,
700
+ fleetServiceInstallPending: false,
701
+ // The imported login is the bot's Fleet sign-in now; the legacy own-login setting no longer applies.
702
+ ...(decision.signIn?.env ? { runtimeAuth: withRuntimeAuth(current.runtimeAuth, decision.signIn.runtime, null) } : {}),
703
+ };
624
704
  if (from === "start")
625
705
  await stopBot(name, dir, config, "silent");
626
706
  let failure = null;
@@ -641,16 +721,77 @@ async function convertLegacyBotToFleet(name, dir, config, from) {
641
721
  /* nothing was running */
642
722
  }
643
723
  keepLegacy("start", failure);
644
- return null;
724
+ return ready();
645
725
  }
646
726
  writeBotConfig(dir, fleet);
647
- fs.rmSync(fleetMoveFailurePath(dir), { force: true });
727
+ clearFleetMoveRecords(dir);
648
728
  // The legacy runner's copy of the machine credentials goes only now: a failed move above still needs it.
649
729
  fs.rmSync(botCredsPath(dir), { force: true });
650
730
  log("info", `moved ${name} to the HQ Fleet Runtime under the same identity; its ${config.runtime} runner is retired`);
651
731
  syncFleetRuntimeJobs(name, dir, fleet);
652
732
  retireLegacyBotAgent(name, dir, config, from === "run" ? "detached" : "inline");
653
- return fleet;
733
+ return { fleet, state: null };
734
+ }
735
+ /**
736
+ * A legacy bot that cannot move yet: record why, and tell the owner once per
737
+ * reason through the bot's own DM, the way a Fleet bot reports a failed turn.
738
+ * The record is what `hq bot status` and `hq doctor` show.
739
+ */
740
+ async function holdLegacyBot(name, dir, config, decision) {
741
+ const log = createBotLogger(dir);
742
+ const notice = legacyHoldNotice(name, config.runtime, decision);
743
+ const previous = readFleetMoveHold(dir);
744
+ const now = new Date().toISOString();
745
+ const hold = {
746
+ reason: decision.reason,
747
+ detail: decision.detail,
748
+ at: now,
749
+ ...(previous?.notifiedAt ? { notifiedAt: previous.notifiedAt } : {}),
750
+ ...(previous?.notifiedReason ? { notifiedReason: previous.notifiedReason } : {}),
751
+ ...(previous?.reportId ? { reportId: previous.reportId } : {}),
752
+ ...(previous?.reportedReason ? { reportedReason: previous.reportedReason } : {}),
753
+ };
754
+ writeFleetMoveHold(dir, hold);
755
+ log("warn", `${name} stays on its ${config.runtime} runner: ${notice.cause} ${notice.fix}`);
756
+ const reportDue = holdReportDue(previous, decision.reason);
757
+ const noticeDue = ownerHoldNoticeDue(previous, decision.reason);
758
+ if (!reportDue && !noticeDue)
759
+ return;
760
+ const api = new BotApi({ token: botTokenSupplier(dir) });
761
+ // One failure report per reason, the same route a Fleet bot files a failed turn through, so stuck legacy bots are visible in HQ.
762
+ if (reportDue) {
763
+ try {
764
+ const report = legacyHoldFailureReport({
765
+ reportId: randomUUID(),
766
+ createdAt: now,
767
+ name,
768
+ config,
769
+ decision,
770
+ notice,
771
+ machine: os.hostname().trim(),
772
+ hqCliVersion: CLI_VERSION,
773
+ os: `${process.platform} ${os.release()}`,
774
+ logPath: botLogPath(dir),
775
+ });
776
+ const { reportId } = await api.reportFailure(config.agentUid, report);
777
+ Object.assign(hold, { reportedReason: decision.reason, ...(reportId ? { reportId } : {}) });
778
+ writeFleetMoveHold(dir, hold);
779
+ log("info", `filed a failure report with HQ for ${name}'s held move (${decision.reason}${reportId ? `, report ${reportId}` : ""})`);
780
+ }
781
+ catch (err) {
782
+ log("warn", `could not file ${name}'s held move with HQ (${err instanceof Error ? err.message : String(err)}); the next start tries again`);
783
+ }
784
+ }
785
+ if (!noticeDue)
786
+ return;
787
+ try {
788
+ await api.sendDm({ toPersonUid: config.ownerUid, body: `${notice.cause} ${notice.fix}` });
789
+ writeFleetMoveHold(dir, { ...hold, notifiedAt: now, notifiedReason: decision.reason });
790
+ log("info", `told the owner once that ${name} has not moved to the HQ Fleet Runtime (${decision.reason})`);
791
+ }
792
+ catch (err) {
793
+ log("warn", `could not tell the owner why ${name} has not moved to the HQ Fleet Runtime (${err instanceof Error ? err.message : String(err)}); the next start tries again`);
794
+ }
654
795
  }
655
796
  /** Remove a legacy bot's own startup agent once the bot runs on the HQ Fleet Runtime. True when there was one. */
656
797
  function retireLegacyBotAgent(name, dir, config, bootout) {
@@ -735,9 +876,10 @@ async function installFleetRuntimeProfile(name, dir, config, opts = {}) {
735
876
  return null;
736
877
  if (!opts.force && !fleetLocalProfileNeedsAdoption(config.agentUid, { release: target.release }))
737
878
  return null;
879
+ const progress = opts.progress ?? fleetInstallProgressReporter("silent");
738
880
  let installation;
739
881
  try {
740
- installation = await installTargetFleetLocalDistribution({ target });
882
+ installation = await installTargetFleetLocalDistribution({ target, onProgress: progress });
741
883
  }
742
884
  catch (err) {
743
885
  if (target.source !== "pointer")
@@ -749,8 +891,9 @@ async function installFleetRuntimeProfile(name, dir, config, opts = {}) {
749
891
  createBotLogger(dir)("warn", `could not install HQ Fleet Local ${target.release.version} (${err instanceof Error ? err.message : String(err)}); staying on ${HQ_FLEET_LOCAL_RELEASE.version}`);
750
892
  if (!opts.force && !fleetLocalProfileNeedsAdoption(config.agentUid, { release: HQ_FLEET_LOCAL_RELEASE }))
751
893
  return null;
752
- installation = await installPinnedFleetLocalDistribution();
894
+ installation = await installPinnedFleetLocalDistribution({ onProgress: progress });
753
895
  }
896
+ progress({ phase: "python" });
754
897
  const python = await ensureFleetPython();
755
898
  const companyScope = effectiveBotKind(config) === "company"
756
899
  ? effectiveBotCompanies(config)
@@ -765,7 +908,7 @@ async function installFleetRuntimeProfile(name, dir, config, opts = {}) {
765
908
  provider: fleetProviderForRuntime(config.runtime, config.model),
766
909
  ownerCompanies: companyScope,
767
910
  ...(config.autoApprove === true ? { autoApprove: true } : {}),
768
- }, { python, start: false });
911
+ }, { python, start: false, onProgress: progress });
769
912
  const credentials = botCredsPath(dir);
770
913
  if (fs.existsSync(credentials))
771
914
  importFleetLocalMachineCredential(config.agentUid, credentials, {}, { keepSource: opts.keepCredentials === true });
@@ -1228,7 +1371,9 @@ async function runBotCreateLocked(name, opts) {
1228
1371
  // of sending another create that the entity store will reject.
1229
1372
  if (isSetupBot && (await adoptOwnedSetupBot(true)))
1230
1373
  return;
1231
- const fleetInstallation = await installTargetFleetLocalDistribution();
1374
+ const progress = fleetInstallProgressReporter(opts.json ? "silent" : "text");
1375
+ const fleetInstallation = await installTargetFleetLocalDistribution({ onProgress: progress });
1376
+ progress({ phase: "python" });
1232
1377
  const fleetPython = await ensureFleetPython();
1233
1378
  const ownerCompanies = kind === "personal" ? await fleetOwnerCompanies() : companies;
1234
1379
  // A company bot's companies are resolved first (a typo must not cost an
@@ -1388,7 +1533,7 @@ async function runBotCreateLocked(name, opts) {
1388
1533
  firstStart: toolOffer
1389
1534
  ? { intro: setupToolOfferText(toolOffer, setupWelcomeOpening(intro)) }
1390
1535
  : { intro: introText, ...(kickoff ? { kickoff } : {}) },
1391
- }, { start: opts.start !== false && !fleetAuthPending, python: fleetPython });
1536
+ }, { start: opts.start !== false && !fleetAuthPending, python: fleetPython, onProgress: progress });
1392
1537
  syncFleetRuntimeJobs(name, dir, config);
1393
1538
  const fleetProfile = fleetBootstrap.profile;
1394
1539
  const runtimeLabel = HQ_FLEET_RUNTIME_ENGINE_ID;
@@ -1562,7 +1707,7 @@ async function runBotResident(nameArg) {
1562
1707
  console.log(orphanBotRunMessage(name, outcome));
1563
1708
  return;
1564
1709
  }
1565
- if (await convertLegacyBotToFleet(name, dir, config, "run"))
1710
+ if ((await convertLegacyBotToFleet(name, dir, config, "run")).fleet)
1566
1711
  return;
1567
1712
  if (healNodePathFromJob(name, dir, config))
1568
1713
  return;
@@ -1680,18 +1825,28 @@ async function runBotList(opts) {
1680
1825
  async function runBotStatus(nameArg, opts) {
1681
1826
  const { name, dir, config } = requireBot(nameArg);
1682
1827
  const fleet = isFleetRuntimeBot(config);
1683
- const row = rowFor(name);
1828
+ const legacy = fleet ? null : legacyMoveState(dir, config, legacyConvertSeams.providerLogin(dir, config));
1829
+ const row = rowFor(name, { legacy });
1684
1830
  await decorateOnline([row]);
1685
1831
  const status = readBotStatus(dir);
1686
1832
  const creds = readBotCredsIdentity(dir);
1687
1833
  const channels = fleet ? fleetLocalChannels(readFleetLocalGatewayState(config.agentUid), row.processAlive) : null;
1688
1834
  if (opts.json) {
1689
- console.log(JSON.stringify({ ...row, executive: executiveStatus(config, dir), ...(fleet ? { engine: config.engine, channels } : {}), credsPresent: fleet || !!creds, login: describeBotAuth(dir, config), status }, null, 2));
1835
+ console.log(JSON.stringify({
1836
+ ...row,
1837
+ executive: executiveStatus(config, dir),
1838
+ ...(fleet ? { engine: config.engine, channels } : {}),
1839
+ credsPresent: fleet || !!creds,
1840
+ login: describeBotAuth(dir, config),
1841
+ status,
1842
+ }, null, 2));
1690
1843
  return;
1691
1844
  }
1692
1845
  console.log(`${chalk.bold(name)} ${row.agentUid}`);
1693
1846
  if (fleet)
1694
1847
  console.log(` engine: HQ Fleet Runtime ${config.engine?.version ?? ""} ${chalk.dim(`logs ~/.hq-fleet/profiles/${config.agentUid}/logs`)}`);
1848
+ else
1849
+ console.log(` engine: ${chalk.yellow(`legacy ${runtimeDisplayName(config.runtime)} runner (not on the HQ Fleet Runtime)`)}`);
1695
1850
  console.log(` kind: ${describeKind(row.kind, row.companies)}`);
1696
1851
  const login = describeBotAuth(dir, config);
1697
1852
  console.log(` runtime: ${row.runtime} model ${row.model ?? "default"} thinking ${row.effort}${row.effortIsDefault ? " (default)" : ""}`);
@@ -1718,6 +1873,10 @@ async function runBotStatus(nameArg, opts) {
1718
1873
  else {
1719
1874
  console.log(` launchd: ${row.daemonInstalled ? (row.daemonLoaded ? "installed, loaded" : "installed, not loaded") : "not installed"}`);
1720
1875
  }
1876
+ if (legacy) {
1877
+ for (const line of legacyStatusLines(name, legacy))
1878
+ console.log(` ${line}`);
1879
+ }
1721
1880
  if (row.daemonNodeMissing) {
1722
1881
  console.log(` ${chalk.red(`node missing: ${row.daemonNodeMissing}`)} — launchd cannot start ${name}; fix with: hq bot start ${name}`);
1723
1882
  }
@@ -1767,6 +1926,37 @@ async function runBotStatus(nameArg, opts) {
1767
1926
  }
1768
1927
  console.log(chalk.dim(` state dir: ${dir}`));
1769
1928
  }
1929
+ /** The `legacy` object in `hq bot status --json` for a bot still on its legacy runner. */
1930
+ export function legacyStatusJson(state) {
1931
+ const { decision } = state;
1932
+ return {
1933
+ moves: decision.convert,
1934
+ ...(decision.convert ? {} : { reason: decision.reason, detail: decision.detail, cause: state.notice?.cause ?? null, fix: state.notice?.fix ?? null }),
1935
+ lastMoveFailure: state.lastFailure,
1936
+ ownerNotifiedAt: state.hold?.notifiedAt ?? null,
1937
+ reportId: state.hold?.reportId ?? null,
1938
+ };
1939
+ }
1940
+ /** The `fleet move:` lines of `hq bot status` for a bot still on its legacy runner. */
1941
+ export function legacyStatusLines(name, state) {
1942
+ const lines = [];
1943
+ const { decision } = state;
1944
+ if (decision.convert) {
1945
+ lines.push(`fleet move: ready; it moves to the HQ Fleet Runtime at its next start (hq bot start ${name})`);
1946
+ }
1947
+ else {
1948
+ lines.push(`fleet move: ${chalk.yellow(`held (${decision.reason})`)}: ${state.notice?.cause ?? decision.detail}`);
1949
+ if (state.notice?.fix)
1950
+ lines.push(` fix: ${state.notice.fix}`);
1951
+ if (legacyHoldNotifiesOwner(decision.reason))
1952
+ lines.push(chalk.dim(` owner DM: ${state.hold?.notifiedAt ? `sent ${state.hold.notifiedAt}` : "not sent yet"}`));
1953
+ }
1954
+ if (state.lastFailure) {
1955
+ const f = state.lastFailure;
1956
+ lines.push(`last move: ${chalk.yellow(`failed ${f.at}${f.step ? ` (could not ${f.step})` : ""}${f.error ? `: ${f.error}` : ""}`)}; it tries again at its next start`);
1957
+ }
1958
+ return lines;
1959
+ }
1770
1960
  async function runBotLogs(nameArg, opts) {
1771
1961
  const { dir } = requireBot(nameArg);
1772
1962
  const file = botLogPath(dir);
@@ -2489,6 +2679,7 @@ async function runBotAuth(nameArg, opts) {
2489
2679
  if (legacy && engine) {
2490
2680
  const moved = { ...(readBotConfig(b.dir) ?? legacy), engine };
2491
2681
  writeBotConfig(b.dir, moved);
2682
+ clearFleetMoveRecords(b.dir);
2492
2683
  fs.rmSync(botCredsPath(b.dir), { force: true });
2493
2684
  createBotLogger(b.dir)("info", `moved ${b.name} to the HQ Fleet Runtime with its ${opts.provider} sign-in; its ${legacy.runtime} runner is retired`);
2494
2685
  syncFleetRuntimeJobs(b.name, b.dir, moved);
@@ -2496,8 +2687,11 @@ async function runBotAuth(nameArg, opts) {
2496
2687
  }
2497
2688
  const signedInRuntime = result.runtimeProvider === "anthropic" ? "claude" : result.runtimeProvider === "xai-oauth" ? "grok" : result.runtimeProvider === "openai-codex" ? "codex" : null;
2498
2689
  const pending = b.config.fleetAuthPending === true || legacy !== null;
2690
+ // A moved bot answers with the login it just signed in with; its legacy own-login setting no longer applies.
2691
+ const runtimeAuth = legacy && signedInRuntime && legacy.runtimeAuth?.[signedInRuntime] !== undefined ? withRuntimeAuth(legacy.runtimeAuth, signedInRuntime, null) : undefined;
2499
2692
  patchBotConfig(b.dir, {
2500
2693
  ...(signedInRuntime ? { runtime: signedInRuntime } : {}),
2694
+ ...(legacy && signedInRuntime && legacy.runtimeAuth?.[signedInRuntime] !== undefined ? { runtimeAuth } : {}),
2501
2695
  model: result.model,
2502
2696
  ...(pending ? { fleetAuthPending: false, fleetServiceInstallPending: !result.restarted, enabled: result.restarted } : {}),
2503
2697
  });
@@ -2776,7 +2970,10 @@ async function adoptRemoteBotLocked(input) {
2776
2970
  ...(input.preserveFirstStart ? { preserveFirstStart: true } : {}),
2777
2971
  });
2778
2972
  // A bot made on the HQ Fleet Runtime comes back on it, on the release this CLI pins.
2779
- const fleetInstallation = fleetEngine ? await installPinnedFleetLocalDistribution() : null;
2973
+ const progress = input.progress ?? fleetInstallProgressReporter("silent");
2974
+ const fleetInstallation = fleetEngine ? await installPinnedFleetLocalDistribution({ onProgress: progress }) : null;
2975
+ if (fleetInstallation)
2976
+ progress({ phase: "python" });
2780
2977
  const fleetPython = fleetInstallation ? await ensureFleetPython() : null;
2781
2978
  if (fleetInstallation)
2782
2979
  config.engine = fleetInstallation.engine;
@@ -2805,7 +3002,7 @@ async function adoptRemoteBotLocked(input) {
2805
3002
  },
2806
3003
  }
2807
3004
  : {}),
2808
- }, { python: fleetPython, start: false });
3005
+ }, { python: fleetPython, start: false, onProgress: progress });
2809
3006
  }
2810
3007
  createBotLogger(dir)("info", `adopted ${name} (${bot.agentUid}) on ${localMachineId()} kind=${config.kind}${local ? "" : " (no saved settings; defaults)"} runtime=${config.runtime}`);
2811
3008
  let daemonMessage = null;
@@ -2870,6 +3067,7 @@ export async function runBotAdopt(nameArg, opts) {
2870
3067
  name,
2871
3068
  api,
2872
3069
  hqRoot,
3070
+ progress: fleetInstallProgressReporter(opts.json ? "silent" : "text"),
2873
3071
  overrides: {
2874
3072
  ...(isBotRuntimeId(opts.runtime) ? { runtime: opts.runtime } : {}),
2875
3073
  ...(model ? { model } : {}),
@@ -2983,7 +3181,7 @@ export async function runBotRestore(opts) {
2983
3181
  if (here) {
2984
3182
  return { name, agentUid: bot.agentUid, action: "repaired", detail: await repairLocalBot(name, api, bot, opts.daemon !== false), ...shown };
2985
3183
  }
2986
- const { result, config } = await adoptRemoteBot({ bot, name, api, hqRoot, daemon: opts.daemon !== false });
3184
+ const { result, config } = await adoptRemoteBot({ bot, name, api, hqRoot, daemon: opts.daemon !== false, progress: fleetInstallProgressReporter(opts.json ? "silent" : "text") });
2987
3185
  // Silent: this command's own JSON has to be the only document on stdout.
2988
3186
  const started = opts.start === false ? null : await startBot(name, result.dir, config, "silent").catch(() => null);
2989
3187
  return {
@@ -3173,7 +3371,7 @@ export function registerBotCommand(program) {
3173
3371
  const b = requireBot(name);
3174
3372
  if (isFleetRuntimeBot(b.config)) {
3175
3373
  refuseHeldFleetBot(b.dir);
3176
- const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config);
3374
+ const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config, { progress: fleetInstallProgressReporter(opts.json ? "silent" : "text") });
3177
3375
  runFleetRuntimeControl(b.name, b.config, adopted ? "install" : "start", opts.json === true);
3178
3376
  return;
3179
3377
  }
@@ -3271,6 +3469,17 @@ export function registerBotCommand(program) {
3271
3469
  .command("intro <name>")
3272
3470
  .description("Have the bot re-send its introduction DM to you")
3273
3471
  .action((name) => wrap(() => runBotIntro(name))());
3472
+ const runtime = bot.command("runtime").description("The HQ Fleet Runtime every local bot on this computer shares");
3473
+ runtime
3474
+ .command("install")
3475
+ .description("Download and build the runtime once for this computer, with progress, so creating a bot takes seconds. Safe to run again: a finished install returns at once, an interrupted download continues where it stopped")
3476
+ .option("--json", "Print one JSON object per line: progress events, then { ok, event: \"done\", release, runtimeDir }")
3477
+ .action((opts) => wrap(() => runBotRuntimeInstall(opts))());
3478
+ runtime
3479
+ .command("status")
3480
+ .description("Say whether the runtime is installed on this computer, step by step")
3481
+ .option("--json", "Print machine-readable JSON")
3482
+ .action((opts) => wrap(() => runBotRuntimeStatus(opts))());
3274
3483
  const daemon = bot.command("daemon").description("Manage the bot's launchd user agent (macOS)");
3275
3484
  daemon
3276
3485
  .command("install <name>")
@@ -59,10 +59,49 @@ export declare class SyncSessionExpiredError extends Error {
59
59
  readonly reason?: string | undefined;
60
60
  constructor(kind: "reauth" | "unavailable", reason?: string | undefined);
61
61
  }
62
+ /** Shown when the vault rejected the session even after a forced refresh. */
63
+ export declare const SYNC_SESSION_REJECTED_MESSAGE = "The HQ server rejected your session even after it was refreshed, so the saved sign-in is no longer accepted. `hq auth status` only checks the saved expiry, not the server. Run `hq login`, then rerun the sync.";
64
+ /** Reason text when token acquisition does not settle within the bound. */
65
+ export declare const SYNC_TOKEN_TIMEOUT_REASON = "timed out waiting for the sign-in service";
66
+ /** Longest a single token acquisition or refresh may take before the sync fails. */
67
+ export declare const SYNC_TOKEN_TIMEOUT_MS = 45000;
68
+ /**
69
+ * The vault rejected the session after a forced refresh had already replaced
70
+ * the saved token. Retrying again cannot help; the user has to sign in again.
71
+ */
72
+ export declare class SyncSessionRejectedError extends Error {
73
+ constructor();
74
+ }
62
75
  export interface SyncVaultConfigDeps {
63
76
  /** Non-interactive token resolver: returns the cached token or refreshes it. */
64
77
  ensureToken?: () => Promise<string>;
78
+ /** Bound on one token acquisition (default {@link SYNC_TOKEN_TIMEOUT_MS}). */
79
+ tokenTimeoutMs?: number;
65
80
  }
81
+ export interface RejectedSessionRecoveryDeps {
82
+ /** Refresh the saved session even though it looks valid. */
83
+ forceRefresh?: () => Promise<{
84
+ refreshed: boolean;
85
+ reason?: string;
86
+ }>;
87
+ /**
88
+ * The token-getter failure recorded for the latest attempt, if any. hq-cloud
89
+ * turns a getter error (network, refresh failure, identity swap) into a plain
90
+ * 401, so a 401 with a recorded failure is not a server rejection of a token.
91
+ */
92
+ recordedFailure?: () => Error | null;
93
+ /** Runs once, after a recoverable 401 and before the refresh and rerun. */
94
+ onRetry?: () => void | Promise<void>;
95
+ }
96
+ /**
97
+ * feedback_3d752910: the vault can reject a token that still looks valid in the
98
+ * local cache (revoked or replaced server-side). The cached expiry check never
99
+ * notices, and `hq auth refresh` is a no-op for a valid-looking token, so every
100
+ * sync kept presenting the same rejected token. On a vault 401, force one real
101
+ * refresh and rerun the sync once. A second rejection is reported as a rejected
102
+ * session (sign in again), never retried again.
103
+ */
104
+ export declare function runWithRejectedSessionRecovery<T>(run: () => Promise<T>, deps?: RejectedSessionRecoveryDeps): Promise<T>;
66
105
  /** The last token failure recorded by a {@link buildSyncVaultConfig} config. */
67
106
  export declare function syncAuthFailureFor(config: VaultServiceConfig | undefined): Error | null;
68
107
  /**