@indigoai-us/hq-cli 5.345.170 → 5.345.171

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 (111) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +132 -5
  4. package/dist/command-catalog.generated.js +164 -6
  5. package/dist/commands/access.d.ts +32 -1
  6. package/dist/commands/access.js +112 -3
  7. package/dist/commands/agent-kit.d.ts +1 -1
  8. package/dist/commands/agent-kit.js +3 -1
  9. package/dist/commands/agents.js +2 -1
  10. package/dist/commands/bot.d.ts +3 -0
  11. package/dist/commands/bot.js +137 -14
  12. package/dist/commands/cloud.d.ts +58 -1
  13. package/dist/commands/cloud.js +219 -5
  14. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  15. package/dist/commands/core-company-index-scope-flag.js +9 -2
  16. package/dist/commands/core-delegate-w1b.js +5 -2
  17. package/dist/commands/core-dm-bind-native.js +19 -4
  18. package/dist/commands/core-rebuild-index-fast.js +24 -6
  19. package/dist/commands/core.js +18 -6
  20. package/dist/commands/dm.d.ts +17 -3
  21. package/dist/commands/dm.js +121 -26
  22. package/dist/commands/integrations-api.d.ts +4 -0
  23. package/dist/commands/integrations-api.js +18 -2
  24. package/dist/commands/integrations-connect.d.ts +10 -3
  25. package/dist/commands/integrations-connect.js +70 -6
  26. package/dist/commands/integrations-core.d.ts +23 -0
  27. package/dist/commands/integrations-core.js +100 -0
  28. package/dist/commands/integrations-manage.js +8 -1
  29. package/dist/commands/integrations.js +49 -5
  30. package/dist/commands/lanes-loop.d.ts +20 -0
  31. package/dist/commands/lanes-loop.js +69 -11
  32. package/dist/commands/lanes.js +9 -1
  33. package/dist/commands/rescue.d.ts +9 -0
  34. package/dist/commands/rescue.js +31 -0
  35. package/dist/commands/run.js +115 -40
  36. package/dist/commands/scan.d.ts +5 -1
  37. package/dist/commands/scan.js +16 -4
  38. package/dist/commands/secrets.js +50 -9
  39. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
  40. package/dist/hq-cloud-sync-runner/package.json +1 -1
  41. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  42. package/dist/lib/agent-kit/inbox-state.js +41 -20
  43. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  44. package/dist/lib/agent-kit/kit-config.js +6 -0
  45. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  46. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  47. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  48. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  49. package/dist/lib/agent-kit/run/inbox.js +42 -10
  50. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  51. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  52. package/dist/lib/bot/api.d.ts +1 -1
  53. package/dist/lib/bot/config.d.ts +10 -0
  54. package/dist/lib/bot/config.js +17 -1
  55. package/dist/lib/bot/fleet-local.d.ts +127 -0
  56. package/dist/lib/bot/fleet-local.js +638 -0
  57. package/dist/lib/bot/index.d.ts +1 -0
  58. package/dist/lib/bot/index.js +1 -0
  59. package/dist/lib/bot/local-config.d.ts +3 -2
  60. package/dist/lib/bot/local-config.js +5 -2
  61. package/dist/lib/bot/promote.d.ts +10 -0
  62. package/dist/lib/bot/promote.js +3 -2
  63. package/dist/lib/bot/runtime/index.js +8 -4
  64. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  65. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  66. package/dist/lib/caller-principal.d.ts +4 -0
  67. package/dist/lib/caller-principal.js +40 -0
  68. package/dist/lib/capacity/collectors/grok.js +8 -2
  69. package/dist/lib/cli-flag-keys.d.ts +2 -0
  70. package/dist/lib/cli-flag-keys.js +2 -0
  71. package/dist/lib/daemon/catalog.js +4 -1
  72. package/dist/lib/daemon/sync-core-canary.d.ts +3 -0
  73. package/dist/lib/daemon/sync-core-canary.js +8 -3
  74. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  75. package/dist/lib/doctor/checks/release-links.js +188 -2
  76. package/dist/lib/doctor/fix/apply.js +54 -0
  77. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  78. package/dist/lib/doctor/fix/remediation.js +22 -0
  79. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  80. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  81. package/dist/lib/index-render/index.d.ts +9 -0
  82. package/dist/lib/index-render/index.js +65 -5
  83. package/dist/lib/index-render/session-scope.d.ts +69 -0
  84. package/dist/lib/index-render/session-scope.js +279 -0
  85. package/dist/lib/lanes/admission.js +7 -20
  86. package/dist/lib/lanes/reconcile.js +13 -15
  87. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  88. package/dist/lib/lanes/stale-lock.js +105 -0
  89. package/dist/lib/lanes/store.js +11 -76
  90. package/dist/lib/notify-recipient.d.ts +10 -4
  91. package/dist/lib/notify-recipient.js +16 -4
  92. package/dist/lib/secret-input-link.d.ts +42 -0
  93. package/dist/lib/secret-input-link.js +93 -0
  94. package/dist/lib/secret-scan/range.d.ts +57 -0
  95. package/dist/lib/secret-scan/range.js +266 -0
  96. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  97. package/dist/lib/secret-scan/repo-config.js +135 -2
  98. package/dist/run/script-bound-schema.d.ts +27 -0
  99. package/dist/run/script-bound-schema.js +97 -0
  100. package/dist/secrets-env-action.d.ts +2 -0
  101. package/dist/secrets-env-action.js +7 -2
  102. package/dist/utils/cognito-session.d.ts +3 -30
  103. package/dist/utils/cognito-session.js +2 -100
  104. package/dist/utils/machine-principal-state.d.ts +38 -0
  105. package/dist/utils/machine-principal-state.js +110 -0
  106. package/dist/utils/safe-link.js +9 -0
  107. package/dist/utils/script-bound-exec.d.ts +158 -0
  108. package/dist/utils/script-bound-exec.js +568 -0
  109. package/dist/utils/sync-watchdog.d.ts +92 -0
  110. package/dist/utils/sync-watchdog.js +250 -0
  111. package/package.json +8 -8
@@ -16,7 +16,7 @@
16
16
  import { Command } from "commander";
17
17
  import { type CompanyBrowseClientFactory, type FilesBrowseVaultClient, type RunGetResult } from "./files-browse.js";
18
18
  import { type AclLock } from "./acl-lock.js";
19
- export type AccessOutcome = "never-existed" | "local" | "not-synced" | "no-access" | "ambiguous" | "pending-confirmation";
19
+ export type AccessOutcome = "never-existed" | "local" | "not-synced" | "no-access" | "no-write-access" | "ambiguous" | "pending-confirmation";
20
20
  export interface AccessAclTree {
21
21
  prefix: string;
22
22
  direct: Array<{
@@ -151,6 +151,15 @@ export interface RunAccessResult {
151
151
  * grants (including a root `*` company-wide grant) do not apply.
152
152
  */
153
153
  lockedBy?: AclLock;
154
+ /**
155
+ * Set with outcome "no-write-access": the ACL row that blocks writes to a
156
+ * path that is on disk but not in the vault, so a push cannot upload it.
157
+ * Anchored as companies/<slug>/<prefix>; absent when only the effective
158
+ * permission is known.
159
+ */
160
+ writeBlockedBy?: string;
161
+ /** Effective permission on the path when outcome is "no-write-access". */
162
+ permission?: string | null;
154
163
  }
155
164
  export interface AccessAsResult {
156
165
  personUid: string;
@@ -164,6 +173,28 @@ export interface AccessAsResult {
164
173
  export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
165
174
  export declare function formatAccessOutcome(result: RunAccessResult): string;
166
175
  export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
176
+ /**
177
+ * ACL evidence for one key. `tree` is the full acl/tree response (which keeps
178
+ * the deny / no-record distinction and the ACL creator for grantor lookup).
179
+ * `resolved` is a non-null permission from the batch acl/resolve route.
180
+ */
181
+ export type AclVerdict = {
182
+ kind: "tree";
183
+ tree: AccessAclTree | null;
184
+ } | {
185
+ kind: "resolved";
186
+ permission: string;
187
+ };
188
+ /**
189
+ * Why the caller cannot write `relative`, or null when it can or the ACL says
190
+ * nothing (no record: membership-distributed access decides, which this
191
+ * command cannot see). The tree is fetched when the fast resolve path gave
192
+ * only a permission, so the blocking row can be named.
193
+ */
194
+ export declare function writeBlocker(verdict: AclVerdict, relative: string, deps: Pick<AccessDeps, "fetchAclTree" | "companySlug">): Promise<{
195
+ permission: string | null;
196
+ blockedBy?: string;
197
+ } | null>;
167
198
  export declare function runAccess(input: RunAccessInput): Promise<RunAccessResult>;
168
199
  export interface RunAccessBatchInput {
169
200
  targets: string[];
@@ -26,6 +26,7 @@ import { listActiveMembers } from "./members.js";
26
26
  import { aclLockedBy } from "./acl-lock.js";
27
27
  import { buildDmBody } from "./dm.js";
28
28
  import { defaultAudienceFor, recipientKindOf } from "../lib/notify-recipient.js";
29
+ import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
29
30
  import { peekIdToken } from "../utils/id-token.js";
30
31
  import { findRecentAccessRequest, formatTimeAgo, recordAccessRequest, } from "../utils/access-requests.js";
31
32
  import { recordAccessOutcome } from "../utils/access-outcomes.js";
@@ -34,8 +35,11 @@ export function outcomeExitCode(outcome) {
34
35
  return 0;
35
36
  if (outcome === "never-existed")
36
37
  return 2;
37
- if (outcome === "no-access" || outcome === "pending-confirmation")
38
+ if (outcome === "no-access" ||
39
+ outcome === "no-write-access" ||
40
+ outcome === "pending-confirmation") {
38
41
  return 3;
42
+ }
39
43
  return 4;
40
44
  }
41
45
  export function formatAccessOutcome(result) {
@@ -57,6 +61,16 @@ export function formatAccessOutcome(result) {
57
61
  lines.push(`You do not have read access to this path.`);
58
62
  lines.push(result.path);
59
63
  break;
64
+ case "no-write-access":
65
+ lines.push(`The file is on this computer but not in the ${result.company} vault, and you cannot write to it, so a push cannot upload it.`);
66
+ lines.push(result.path);
67
+ if (result.writeBlockedBy) {
68
+ lines.push(`Write is blocked by the ACL on ${result.writeBlockedBy} (your permission: ${result.permission ?? "none"}). Ask its creator or a company admin for write access.`);
69
+ }
70
+ else {
71
+ lines.push(`Your permission on this path is ${result.permission ?? "none"}. Ask a company admin for write access.`);
72
+ }
73
+ break;
60
74
  case "pending-confirmation":
61
75
  lines.push(`Ask the grantor for read access to this path.`);
62
76
  lines.push(result.path);
@@ -243,6 +257,69 @@ function startEarly(promise) {
243
257
  promise.catch(() => undefined);
244
258
  return promise;
245
259
  }
260
+ const WRITE_PERMISSIONS = new Set(["write", "admin", "owner"]);
261
+ /**
262
+ * Why the caller cannot write `relative`, or null when it can or the ACL says
263
+ * nothing (no record: membership-distributed access decides, which this
264
+ * command cannot see). The tree is fetched when the fast resolve path gave
265
+ * only a permission, so the blocking row can be named.
266
+ */
267
+ export async function writeBlocker(verdict, relative, deps) {
268
+ const resolvedPermission = verdict.kind === "resolved" ? verdict.permission : undefined;
269
+ if (resolvedPermission && WRITE_PERMISSIONS.has(resolvedPermission))
270
+ return null;
271
+ let tree = verdict.kind === "tree" ? verdict.tree : null;
272
+ if (verdict.kind === "resolved") {
273
+ try {
274
+ tree = await deps.fetchAclTree(relative);
275
+ }
276
+ catch {
277
+ tree = null;
278
+ }
279
+ }
280
+ const permission = resolvedPermission ?? tree?.effectivePermission ?? null;
281
+ if (permission && WRITE_PERMISSIONS.has(permission))
282
+ return null;
283
+ const covering = tree?.coveringAclPath;
284
+ if (typeof covering === "string" && covering !== "") {
285
+ return {
286
+ permission,
287
+ blockedBy: toBucketRelative(covering, deps.companySlug),
288
+ };
289
+ }
290
+ // A row on the path itself with a known non-write permission blocks it.
291
+ // With no permission reported, nothing proves the path is blocked.
292
+ if (permission && tree?.directRow) {
293
+ return { permission, blockedBy: toBucketRelative(tree.prefix, deps.companySlug) };
294
+ }
295
+ // No ACL row covers the path. A known non-write permission still blocks.
296
+ if (resolvedPermission)
297
+ return { permission };
298
+ return null;
299
+ }
300
+ /**
301
+ * Owners and admins write past ACL rows on the server (role bypass), which
302
+ * the ACL tree does not always show. Treat a lookup failure as no bypass only
303
+ * when the member list loads; if it cannot be read, assume a bypass so the
304
+ * command keeps its old "never-existed" answer instead of a wrong blocker.
305
+ */
306
+ async function callerHasRoleBypass(deps) {
307
+ try {
308
+ const [members, requester] = await Promise.all([
309
+ deps.listMembers(),
310
+ resolveRequester(deps.requester),
311
+ ]);
312
+ const email = (requester.email ?? "").trim().toLowerCase();
313
+ // An unknown caller cannot be matched; keep the old answer.
314
+ if (email === "")
315
+ return true;
316
+ return members.some((m) => (m.personEmail ?? "").trim().toLowerCase() === email &&
317
+ (m.role === "owner" || m.role === "admin"));
318
+ }
319
+ catch {
320
+ return true;
321
+ }
322
+ }
246
323
  function verdictDenied(verdict) {
247
324
  return verdict.kind === "tree" ? aclDenied(verdict.tree) : false;
248
325
  }
@@ -388,6 +465,31 @@ async function locateTarget(target, deps, ctx) {
388
465
  },
389
466
  };
390
467
  }
468
+ // feedback_5baccb9e: a file that is on disk but absent from the vault
469
+ // is usually one a push could not upload. When the ACL blocks writes,
470
+ // say so instead of "never existed".
471
+ if (relative !== "" &&
472
+ fs.existsSync(path.join(deps.hqRoot, "companies", slug, relative))) {
473
+ steps.push("disk: present");
474
+ const blocker = await writeBlocker(verdict, relative, deps);
475
+ if (blocker && !(await callerHasRoleBypass(deps))) {
476
+ steps.push(blocker.blockedBy
477
+ ? `acl: write blocked by ${anchored(blocker.blockedBy)} (${blocker.permission ?? "none"})`
478
+ : `acl: no write (${blocker.permission ?? "none"})`);
479
+ return {
480
+ kind: "terminal",
481
+ outcome: "no-write-access",
482
+ fields: {
483
+ path: anchored(relative),
484
+ company: slug,
485
+ exists: false,
486
+ steps,
487
+ permission: blocker.permission,
488
+ ...(blocker.blockedBy ? { writeBlockedBy: anchored(blocker.blockedBy) } : {}),
489
+ },
490
+ };
491
+ }
492
+ }
391
493
  return {
392
494
  kind: "terminal",
393
495
  outcome: "never-existed",
@@ -857,6 +959,11 @@ function jsonPayload(result) {
857
959
  payload.alreadyAskedAt = result.alreadyAskedAt;
858
960
  if (result.lockedBy)
859
961
  payload.lockedBy = result.lockedBy;
962
+ if (result.outcome === "no-write-access") {
963
+ payload.permission = result.permission ?? null;
964
+ if (result.writeBlockedBy)
965
+ payload.writeBlockedBy = result.writeBlockedBy;
966
+ }
860
967
  return payload;
861
968
  }
862
969
  function emit(input, result, log) {
@@ -933,7 +1040,9 @@ export function registerAccessCommand(program) {
933
1040
  Exit codes:
934
1041
  0 local or not-synced (file exists and you can read it)
935
1042
  2 never-existed (the path was never created)
936
- 3 no-access (the file exists but you cannot read it)
1043
+ 3 no-access (the file exists but you cannot read it), or
1044
+ no-write-access (the file is on disk, not in the vault, and you cannot
1045
+ write to it, so a push cannot upload it)
937
1046
  4 ambiguous (several keys matched; pick one)
938
1047
 
939
1048
  With several paths, each gets its own result block and the exit code is the
@@ -1161,7 +1270,7 @@ async function buildLiveAccessDeps(slug, hqRoot) {
1161
1270
  prompt: msg.prompt,
1162
1271
  details: msg.details,
1163
1272
  now: Date.now(),
1164
- audience: defaultAudienceFor(recipientKindOf(msg.recipient)),
1273
+ audience: defaultAudienceFor(recipientKindOf(msg.recipient), callerIsMachinePrincipal()),
1165
1274
  });
1166
1275
  const res = await vaultApiFetch({
1167
1276
  token,
@@ -75,7 +75,7 @@ export interface KitRuntime {
75
75
  paths: AgentKitPaths;
76
76
  creds: ExternalMachineCreds;
77
77
  config: KitConfig;
78
- getToken: () => Promise<string>;
78
+ getToken: (signal?: AbortSignal) => Promise<string>;
79
79
  log: KitLogger;
80
80
  credsSource?: HeartbeatCredsSource;
81
81
  }
@@ -153,7 +153,7 @@ export function buildKitRuntime(service) {
153
153
  process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
154
154
  process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
155
155
  const log = createKitLogger(paths, service, { echo: process.stdout.isTTY === true });
156
- const getToken = () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
156
+ const getToken = (signal) => ensureCognitoToken({ tokenSource: "machine", interactive: false, ...(signal ? { signal } : {}) });
157
157
  return { paths, creds, config, getToken, log, credsSource };
158
158
  }
159
159
  export async function runKitService(service, rt) {
@@ -177,6 +177,7 @@ export async function runKitService(service, rt) {
177
177
  },
178
178
  apiBaseUrl: rt.creds.apiBaseUrl,
179
179
  intervalMs: rt.config.heartbeatIntervalMs,
180
+ requestTimeoutMs: rt.config.requestTimeoutMs,
180
181
  getToken: rt.getToken,
181
182
  log: rt.log,
182
183
  components: enabledKitComponents(rt.config),
@@ -192,6 +193,7 @@ export async function runKitService(service, rt) {
192
193
  agentUid: rt.creds.entityUid,
193
194
  apiBaseUrl: rt.creds.apiBaseUrl,
194
195
  pollMs: rt.config.inboxPollMs,
196
+ requestTimeoutMs: rt.config.requestTimeoutMs,
195
197
  ack: rt.config.inboxAck,
196
198
  getToken: rt.getToken,
197
199
  log: rt.log,
@@ -43,6 +43,7 @@ import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSe
43
43
  import { peekIdToken } from "../utils/id-token.js";
44
44
  import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, } from "../utils/plan-gate-error.js";
45
45
  import { AGENT_UID_PATTERN, defaultAudienceFor, isAgentUid } from "../lib/notify-recipient.js";
46
+ import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
46
47
  import { AGENT_MODEL_PROVIDERS, modelSelectionErrorMessage, readAgentModelCatalog, validateAgentModelSelection, } from "../lib/agents/model-catalog.js";
47
48
  import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyBoxSentence, usageOnlyCardRequiredLines, } from "../lib/billing/usage-only.js";
48
49
  import { BUNDLED_USAGE_CARD, fetchUsageCard, } from "../lib/billing/usage-card.js";
@@ -1139,7 +1140,7 @@ export async function sendAgentDm(token, agentUid, message) {
1139
1140
  token,
1140
1141
  path: "/v1/notify/dm",
1141
1142
  method: "POST",
1142
- body: { toPersonUid: agentUid, body: message, audience: defaultAudienceFor("agent") },
1143
+ body: { toPersonUid: agentUid, body: message, audience: defaultAudienceFor("agent", callerIsMachinePrincipal()) },
1143
1144
  });
1144
1145
  }
1145
1146
  /** Read the two-way DM conversation with an agent (most recent `limit`). */
@@ -109,6 +109,7 @@ export declare function resolveCreateKind(opts: {
109
109
  };
110
110
  export interface BotCreateOptions {
111
111
  runtime?: string;
112
+ hqFleetRuntime?: boolean;
112
113
  model?: string;
113
114
  effort?: string;
114
115
  autoApprove?: boolean;
@@ -343,6 +344,8 @@ export interface RemoteBotRow {
343
344
  displayName: string | null;
344
345
  /** "assistant" for HQ's setup assistant, else null. */
345
346
  role: BotRole;
347
+ /** The owner's person uid exactly as GET /v1/agents/mine sends it; null when the server omits it. */
348
+ ownerUid: string | null;
346
349
  }
347
350
  export declare function registerBotCommand(program: Command): void;
348
351
  //# sourceMappingURL=bot.d.ts.map
@@ -52,7 +52,7 @@ import { isAncestorProcess } from "../lib/bot/self-process.js";
52
52
  import { runtimeDisplayName } from "../lib/bot/runtime-sign-in.js";
53
53
  import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
54
54
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
55
- 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, 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, } from "../lib/bot/index.js";
55
+ 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, 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, authenticateFleetLocalBot, bootstrapFleetLocalBot, controlFleetLocalBot, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, removeFleetLocalBot, } from "../lib/bot/index.js";
56
56
  function collectRepeated(value, previous) {
57
57
  return [...previous, value];
58
58
  }
@@ -114,6 +114,18 @@ function requireBot(name) {
114
114
  }
115
115
  return { name: valid, dir, config };
116
116
  }
117
+ function isFleetRuntimeBot(config) {
118
+ return config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID;
119
+ }
120
+ function runFleetRuntimeControl(name, config, action, json) {
121
+ const result = controlFleetLocalBot(config.agentUid, action);
122
+ if (json) {
123
+ console.log(JSON.stringify({ name, runtime: HQ_FLEET_RUNTIME_ENGINE_ID, ...result }, null, 2));
124
+ return;
125
+ }
126
+ const summary = typeof result.summary === "string" ? result.summary : `${action} completed`;
127
+ console.log(chalk.green(`${name}: ${summary}`));
128
+ }
117
129
  /** Bot-identity token supplier: per-bot creds + per-bot token cache dir. */
118
130
  export function botTokenSupplier(dir) {
119
131
  process.env.HQ_MACHINE_CREDS_FILE = botCredsPath(dir);
@@ -605,6 +617,10 @@ async function finishExistingSetupBot(name, dir, config, displayName, opts) {
605
617
  console.log(chalk.yellow(`${name} did not report running within 15s`) + chalk.dim(` logs: hq bot logs ${name}`));
606
618
  }
607
619
  async function runBotCreateLocked(name, opts) {
620
+ const useFleetRuntime = opts.hqFleetRuntime === true;
621
+ if (useFleetRuntime && (opts.runtime || opts.model || opts.effort || opts.lean)) {
622
+ throw Object.assign(new Error("--hq-fleet-runtime selects the local HQ runtime. Do not combine it with --runtime, --model, --effort, or --lean."), { expected: true });
623
+ }
608
624
  const runtime = opts.runtime ?? "claude";
609
625
  if (!isBotRuntimeId(runtime)) {
610
626
  throw Object.assign(new Error(`--runtime must be one of ${BOT_RUNTIMES.join(", ")}`), { expected: true });
@@ -640,6 +656,11 @@ async function runBotCreateLocked(name, opts) {
640
656
  // cost an identity).
641
657
  const worker = opts.worker?.trim() ? resolveBotWorker(hqRoot, opts.worker) : null;
642
658
  const { kind, companies } = resolveCreateKind(opts, worker);
659
+ if (useFleetRuntime && kind !== "personal") {
660
+ throw Object.assign(new Error("--hq-fleet-runtime creates a personal bot. Drop --kind company and --company."), {
661
+ expected: true,
662
+ });
663
+ }
643
664
  const isSetupBot = name === RESERVED_SETUP_BOT_NAME && kind === "personal" && worker?.id === SETUP_BOT_WORKER_ID;
644
665
  if (name === RESERVED_SETUP_BOT_NAME && kind === "personal" && !isSetupBot) {
645
666
  throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's assistant; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
@@ -654,6 +675,9 @@ async function runBotCreateLocked(name, opts) {
654
675
  expected: true,
655
676
  });
656
677
  }
678
+ const fleetInstallation = useFleetRuntime
679
+ ? await installPinnedFleetLocalDistribution()
680
+ : null;
657
681
  const token = await ownerToken();
658
682
  const owner = await resolveCallerPersonUid(token, undefined, { command: "hq bot create" });
659
683
  if (!owner)
@@ -763,6 +787,7 @@ async function runBotCreateLocked(name, opts) {
763
787
  // instead of creating a new one. The machine secret never travels.
764
788
  const localConfig = buildBotLocalConfig({
765
789
  runtime,
790
+ ...(fleetInstallation ? { engine: fleetInstallation.engine } : {}),
766
791
  kind,
767
792
  ...(kind === "company" ? { companies } : {}),
768
793
  ...(model ? { model } : {}),
@@ -829,6 +854,7 @@ async function runBotCreateLocked(name, opts) {
829
854
  agentUid,
830
855
  ownerUid: created.agent.ownerUid ?? owner,
831
856
  runtime,
857
+ ...(fleetInstallation ? { engine: fleetInstallation.engine } : {}),
832
858
  kind,
833
859
  // A company bot's list is filled in below, with the invites that went through.
834
860
  ...(kind === "company" ? { companies: [] } : {}),
@@ -885,8 +911,20 @@ async function runBotCreateLocked(name, opts) {
885
911
  const scaffoldOpts = { memoryDir: config.memoryDir };
886
912
  const scaffold = worker
887
913
  ? scaffoldBotMemory(hqRoot, name, undefined, scaffoldOpts)
888
- : scaffoldBotWorker(hqRoot, name, agentUid, runtime, undefined, scaffoldOpts);
889
- createBotLogger(dir)("info", `created ${name} (${agentUid}) kind=${kind}${companies.length ? ` companies=${companies.join(",")}` : ""} runtime=${runtime}${worker ? ` worker=${worker.id} company=${worker.company ?? "-"}` : ""} memory=${memoryMode}`);
914
+ : scaffoldBotWorker(hqRoot, name, agentUid, fleetInstallation ? HQ_FLEET_RUNTIME_ENGINE_ID : runtime, undefined, scaffoldOpts);
915
+ const fleetBootstrap = fleetInstallation
916
+ ? bootstrapFleetLocalBot({
917
+ agentUid,
918
+ ownerUid: config.ownerUid,
919
+ hqRoot,
920
+ botDir: dir,
921
+ installation: fleetInstallation,
922
+ machineCredentials: botCredsPath(dir),
923
+ }, { start: opts.start !== false })
924
+ : null;
925
+ const fleetProfile = fleetBootstrap?.profile ?? null;
926
+ const runtimeLabel = fleetInstallation ? HQ_FLEET_RUNTIME_ENGINE_ID : runtime;
927
+ createBotLogger(dir)("info", `created ${name} (${agentUid}) kind=${kind}${companies.length ? ` companies=${companies.join(",")}` : ""} runtime=${runtimeLabel}${worker ? ` worker=${worker.id} company=${worker.company ?? "-"}` : ""} memory=${memoryMode}`);
890
928
  const created_ = {
891
929
  outcome: "created",
892
930
  role: localBotRole(config),
@@ -895,7 +933,9 @@ async function runBotCreateLocked(name, opts) {
895
933
  agentUid,
896
934
  kind,
897
935
  companies,
898
- runtime,
936
+ runtime: runtimeLabel,
937
+ engine: fleetInstallation?.engine ?? null,
938
+ profile: fleetProfile,
899
939
  dir,
900
940
  worker: worker ? path.join(hqRoot, worker.path) : scaffold.dir,
901
941
  workerId: worker?.id ?? null,
@@ -906,15 +946,15 @@ async function runBotCreateLocked(name, opts) {
906
946
  intro: intro ?? null,
907
947
  kickoff: kickoff ?? null,
908
948
  model: model ?? null,
909
- effort: effort ?? DEFAULT_BOT_EFFORT,
949
+ effort: fleetInstallation ? null : effort ?? DEFAULT_BOT_EFFORT,
910
950
  };
911
951
  let daemonMessage = null;
912
- if (opts.daemon !== false && process.platform === "darwin") {
952
+ if (!fleetInstallation && opts.daemon !== false && process.platform === "darwin") {
913
953
  const r = installBotAgent(buildBotDaemonPaths({ name, botDir: dir, hqRoot }));
914
954
  daemonMessage = r.message;
915
955
  }
916
956
  if (!opts.json) {
917
- console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${agentUid}, ${runtime})`));
957
+ console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${agentUid}, ${runtimeLabel})`));
918
958
  console.log(chalk.dim(` kind: ${describeKind(kind, companies)}`));
919
959
  if (worker)
920
960
  console.log(chalk.dim(` worker: ${worker.id}${worker.company ? ` (${worker.company})` : ""} — ${path.join(hqRoot, worker.path)}`));
@@ -922,9 +962,17 @@ async function runBotCreateLocked(name, opts) {
922
962
  console.log(chalk.dim(` worker: ${scaffold.dir}`));
923
963
  console.log(chalk.dim(` memory: ${scaffold.memoryDir} (${memoryMode})`));
924
964
  console.log(chalk.dim(` state: ${dir}`));
965
+ if (fleetProfile)
966
+ console.log(chalk.dim(` profile: ${fleetProfile}`));
925
967
  if (daemonMessage)
926
968
  console.log(chalk.dim(` ${daemonMessage}`));
927
969
  }
970
+ if (fleetInstallation) {
971
+ if (opts.json) {
972
+ console.log(JSON.stringify({ ok: true, name, ...created_, daemon: null, running: fleetBootstrap?.running ?? false }, null, 2));
973
+ }
974
+ return;
975
+ }
928
976
  if (opts.start !== false) {
929
977
  if (daemonMessage && /loaded\)/.test(daemonMessage)) {
930
978
  // RunAtLoad already started it.
@@ -1127,7 +1175,11 @@ async function runBotList(opts) {
1127
1175
  }
1128
1176
  }
1129
1177
  async function runBotStatus(nameArg, opts) {
1130
- const { name, dir } = requireBot(nameArg);
1178
+ const { name, dir, config } = requireBot(nameArg);
1179
+ if (isFleetRuntimeBot(config)) {
1180
+ runFleetRuntimeControl(name, config, "status", opts.json === true);
1181
+ return;
1182
+ }
1131
1183
  const row = rowFor(name);
1132
1184
  await decorateOnline([row]);
1133
1185
  const status = readBotStatus(dir);
@@ -1326,6 +1378,15 @@ async function runBotRmOrphan(name, opts) {
1326
1378
  else
1327
1379
  console.log(chalk.dim(` HQ may still have a bot named "${name}". Remove that too with: hq bot rm ${name} --remote`));
1328
1380
  }
1381
+ function stopFleetRuntimeBotQuietly(agentUid) {
1382
+ try {
1383
+ controlFleetLocalBot(agentUid, "stop");
1384
+ return true;
1385
+ }
1386
+ catch {
1387
+ return false;
1388
+ }
1389
+ }
1329
1390
  async function runBotRm(nameArg, opts) {
1330
1391
  const validated = validateBotName(nameArg);
1331
1392
  // Under the bot's lock: an rm racing a create, adopt or restore of the same
@@ -1348,9 +1409,13 @@ async function runBotRmLocked(validated, opts) {
1348
1409
  return;
1349
1410
  }
1350
1411
  }
1351
- await stopBot(name, dir, config, "silent").catch(() => { });
1352
- const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
1353
- const daemon = uninstallBotDaemon(paths, daemonDeps());
1412
+ if (isFleetRuntimeBot(config))
1413
+ stopFleetRuntimeBotQuietly(config.agentUid);
1414
+ else
1415
+ await stopBot(name, dir, config, "silent").catch(() => { });
1416
+ const daemon = isFleetRuntimeBot(config)
1417
+ ? { message: "managed by HQ Fleet Runtime" }
1418
+ : uninstallBotDaemon(buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot }), daemonDeps());
1354
1419
  const token = await ownerToken();
1355
1420
  const api = new BotApi({ token: async () => token });
1356
1421
  let identity;
@@ -1380,8 +1445,11 @@ async function runBotRmLocked(validated, opts) {
1380
1445
  // re-created under the same name. Everything else keeps the folder, so the
1381
1446
  // same command can pick the job back up.
1382
1447
  const localStateRemoved = terminal || incomplete.retryable === false;
1383
- if (localStateRemoved)
1448
+ if (localStateRemoved) {
1449
+ if (isFleetRuntimeBot(config))
1450
+ removeFleetLocalBot(config.agentUid);
1384
1451
  fs.rmSync(dir, { recursive: true, force: true });
1452
+ }
1385
1453
  const message = incomplete
1386
1454
  ? `HQ removed what it could of "${name}", but ${incomplete.detail}.` +
1387
1455
  (incomplete.retryable
@@ -1572,6 +1640,10 @@ export function botExists(name) {
1572
1640
  /** `hq bot restart <name>`: stop, then start (launchd when installed). */
1573
1641
  export async function runBotRestart(nameArg, opts = {}) {
1574
1642
  const b = requireBot(nameArg);
1643
+ if (isFleetRuntimeBot(b.config)) {
1644
+ runFleetRuntimeControl(b.name, b.config, "restart", opts.json === true || opts.output === "json");
1645
+ return;
1646
+ }
1575
1647
  const running = readBotStatus(b.dir);
1576
1648
  if (running && isPidAlive(running.pid) && selfStopProbe(running.pid)) {
1577
1649
  // The stop half would end this turn (and this command) before the
@@ -1581,6 +1653,30 @@ export async function runBotRestart(nameArg, opts = {}) {
1581
1653
  await stopBot(b.name, b.dir, b.config, "silent");
1582
1654
  await startBot(b.name, b.dir, b.config, opts.output ?? (opts.json ? "json" : "text"));
1583
1655
  }
1656
+ async function runBotAuth(nameArg, opts) {
1657
+ const b = requireBot(nameArg);
1658
+ if (!isFleetRuntimeBot(b.config)) {
1659
+ throw Object.assign(new Error(`hq bot auth is for bots created with --hq-fleet-runtime. ${b.name} uses ${b.config.runtime}.`), { expected: true });
1660
+ }
1661
+ const apiKey = opts.apiKey ? fs.readFileSync(0, "utf8").trim() : undefined;
1662
+ if (opts.apiKey && !apiKey) {
1663
+ throw Object.assign(new Error("No API key was received on stdin."), { expected: true });
1664
+ }
1665
+ const result = authenticateFleetLocalBot({
1666
+ agentUid: b.config.agentUid,
1667
+ provider: opts.provider,
1668
+ ...(opts.model ? { model: opts.model } : {}),
1669
+ ...(apiKey ? { apiKey } : {}),
1670
+ ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
1671
+ ...(opts.transport ? { transport: opts.transport } : {}),
1672
+ openBrowser: opts.browser !== false,
1673
+ });
1674
+ if (opts.json) {
1675
+ console.log(JSON.stringify({ ok: true, name: b.name, runtime: HQ_FLEET_RUNTIME_ENGINE_ID, ...result }, null, 2));
1676
+ return;
1677
+ }
1678
+ console.log(chalk.green(`${b.name} is signed in to ${result.provider} and running ${result.model}.`));
1679
+ }
1584
1680
  // ── adopt / restore (bring a bot you own back to this computer) ──────────────
1585
1681
  /**
1586
1682
  * Which computer a re-issue is for. Never a secret and never an identifier the
@@ -2090,6 +2186,7 @@ async function runBotListRemote(opts) {
2090
2186
  reason: runnable.reason,
2091
2187
  displayName: remoteBotDisplayName(bot, name),
2092
2188
  role: remoteBotRole(bot),
2189
+ ownerUid: bot.ownerUid ?? null,
2093
2190
  };
2094
2191
  });
2095
2192
  if (opts.json) {
@@ -2124,7 +2221,13 @@ export function registerBotCommand(program) {
2124
2221
  const { dir, config } = requireBot(name);
2125
2222
  const token = await ownerToken(false);
2126
2223
  const api = new BotApi({ token: async () => token });
2127
- const result = await promoteLocalBot({ dir, config, companyUid: opts.company, api });
2224
+ const result = await promoteLocalBot({
2225
+ dir,
2226
+ config,
2227
+ companyUid: opts.company,
2228
+ api,
2229
+ ...(isFleetRuntimeBot(config) ? { runtime: fleetLocalPromotionRuntime(config.agentUid) } : {}),
2230
+ });
2128
2231
  const details = result.pending ? await api.promotionDetails(config.agentUid, opts.company) : null;
2129
2232
  if (opts.json)
2130
2233
  console.log(JSON.stringify({ ...result, pairing: details?.pairing ?? null, ...(details?.failure ? { failure: details.failure } : {}) }));
@@ -2140,7 +2243,8 @@ export function registerBotCommand(program) {
2140
2243
  .description("Provision a local bot identity, scaffold its worker folder, install its launchd agent, and start it")
2141
2244
  .option("--kind <kind>", `personal (acts as you, everywhere you are) or company (acts as itself, as a member of its companies). Default: personal, or company when created from a company worker`)
2142
2245
  .option("--company <slug>", "Company bots: a company the bot is a member of (repeat for several); the owner invites it on creation", collectRepeated, [])
2143
- .option("--runtime <runtime>", `Model CLI to run the bot with: ${BOT_RUNTIMES.join("|")}`, "claude")
2246
+ .option("--runtime <runtime>", `Model CLI to run the bot with: ${BOT_RUNTIMES.join("|")}`)
2247
+ .option("--hq-fleet-runtime", "Create this personal bot with the local HQ Fleet Runtime")
2144
2248
  .option("--model <model>", "Model override passed to the runtime CLI")
2145
2249
  .option("--effort <level>", `Thinking level: claude ${BOT_EFFORT_LEVELS.claude.join("|")}; codex ${BOT_EFFORT_LEVELS.codex.join("|")}; grok ${BOT_EFFORT_LEVELS.grok.join("|")} (default ${DEFAULT_BOT_EFFORT})`)
2146
2250
  .option("--no-auto-approve", "Keep the runtime's permission gates (default: everything pre-approved; a headless bot cannot answer prompts)")
@@ -2196,6 +2300,10 @@ export function registerBotCommand(program) {
2196
2300
  .option("--json", "Print machine-readable JSON")
2197
2301
  .action((name, opts) => wrap(async () => {
2198
2302
  const b = requireBot(name);
2303
+ if (isFleetRuntimeBot(b.config)) {
2304
+ runFleetRuntimeControl(b.name, b.config, "start", opts.json === true);
2305
+ return;
2306
+ }
2199
2307
  await startBot(b.name, b.dir, b.config, opts.json ? "json" : "text");
2200
2308
  })());
2201
2309
  bot
@@ -2204,6 +2312,10 @@ export function registerBotCommand(program) {
2204
2312
  .option("--json", "Print machine-readable JSON")
2205
2313
  .action((name, opts) => wrap(async () => {
2206
2314
  const b = requireBot(name);
2315
+ if (isFleetRuntimeBot(b.config)) {
2316
+ runFleetRuntimeControl(b.name, b.config, "stop", opts.json === true);
2317
+ return;
2318
+ }
2207
2319
  await stopBot(b.name, b.dir, b.config, opts.json ? "json" : "text");
2208
2320
  })());
2209
2321
  bot
@@ -2216,6 +2328,17 @@ export function registerBotCommand(program) {
2216
2328
  .description("Show the bot's process, heartbeat, launchd, and credential state")
2217
2329
  .option("--json", "Print machine-readable JSON")
2218
2330
  .action((name, opts) => wrap(() => runBotStatus(name, opts))());
2331
+ bot
2332
+ .command("auth <name>")
2333
+ .description("Sign a runtime-backed personal bot in to ChatGPT, Grok, Claude, or another supported provider")
2334
+ .requiredOption("--provider <provider>", "Provider: chatgpt, grok, claude, or a runtime provider id")
2335
+ .option("--model <model>", "Model id; required for providers without a built-in personal-bot default")
2336
+ .option("--api-key", "Read an API key from stdin instead of starting device authorization")
2337
+ .option("--base-url <url>", "Provider API base URL; required for custom runtime providers")
2338
+ .option("--transport <transport>", "Provider transport: chat_completions, anthropic_messages, or codex_responses")
2339
+ .option("--no-browser", "Do not open a browser during device authorization")
2340
+ .option("--json", "Print machine-readable JSON")
2341
+ .action((name, opts) => wrap(() => runBotAuth(name, opts))());
2219
2342
  bot
2220
2343
  .command("logs <name>")
2221
2344
  .description("Show the bot's log")