@indigoai-us/hq-cli 5.175.0 → 5.177.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.177.0] — 2026-09-24
6
+
7
+ ### Added
8
+
9
+ - `hq lanes create --hire-grant`, `hq lanes grant`, and `hq lanes backfill-grants`
10
+ manage alive child-lane grants. `lanes.hire-grants` defaults off; lane list and
11
+ show include grant and alive-child counts. Terminal child resume, message, and
12
+ account-move paths recheck occupancy and remaining grant allocation. The
13
+ backfill defaults to dry-run.
14
+
15
+ - When `lanes.hire-grants` is enabled, `hq lanes list` and `hq lanes show` use counts
16
+ from the owning Outpost for active remote lanes. If the Outpost does not return
17
+ counts, both commands show local counts and a warning.
18
+
19
+ ## [5.176.0] — 2026-09-24
20
+
21
+ ### Fixed
22
+
23
+ - `hq bot create setup` restores the account's existing setup bot when the local copy is missing.
24
+
5
25
  ## [5.175.0] — 2026-09-24
6
26
 
7
27
  ### Fixed
@@ -26,6 +46,7 @@
26
46
  - Bot LaunchAgents and systemd units, the Work Mesh daemon unit, the `hq daemon` unit, and the lanes and agent-kit units now name node by its upgrade-stable path (`/opt/homebrew/bin/node` or `/opt/homebrew/opt/<formula>/bin/node`, or nvm's `current` link) instead of the versioned `/opt/homebrew/Cellar/node/<version>/bin/node`. Before, `brew upgrade node` deleted that path and launchd could no longer start the bots. The stable path is used only when it resolves to the same node binary; otherwise the unit keeps the resolved path as before.
27
47
  - Installed bot LaunchAgents that still name a versioned or missing node are repaired: `hq bot start` rewrites and reloads the agent, a bot started by launchd rewrites its own agent and reloads it from a detached shell before it takes any message, and `hq doctor` reports it (`bots.<name>.daemon-node`), with `hq doctor --fix` rewriting only the plist file and not reloading launchd. `hq bot list` and `hq bot status` flag a missing node binary. `hq mesh daemon install --if-stale` also reinstalls a same-version unit whose node path is missing or versioned.
28
48
 
49
+
29
50
  ## [5.172.0] — 2026-09-24
30
51
 
31
52
  ### Changed
@@ -6886,6 +6886,9 @@ export declare const COMMAND_CATALOG: readonly [{
6886
6886
  }, {
6887
6887
  readonly flags: "--senior <ref>";
6888
6888
  readonly description: "session:<id> | lane:<id> | user";
6889
+ }, {
6890
+ readonly flags: "--hire-grant <n>";
6891
+ readonly description: "Maximum alive direct child lanes assigned to this lane; set by its senior";
6889
6892
  }, {
6890
6893
  readonly flags: "--timeout <secs>";
6891
6894
  readonly description: "Deadline in seconds (warns, does not kill)";
@@ -6906,6 +6909,43 @@ export declare const COMMAND_CATALOG: readonly [{
6906
6909
  readonly description: "JSON on stdout (always)";
6907
6910
  }];
6908
6911
  readonly subcommands: readonly [];
6912
+ }, {
6913
+ readonly name: "grant";
6914
+ readonly description: "Set a lane's alive child-lane grant as its senior";
6915
+ readonly aliases: readonly [];
6916
+ readonly hidden: false;
6917
+ readonly usage: "[options] <lane-id>";
6918
+ readonly arguments: readonly [{
6919
+ readonly name: "lane-id";
6920
+ readonly required: true;
6921
+ readonly variadic: false;
6922
+ }];
6923
+ readonly options: readonly [{
6924
+ readonly flags: "--hire <n>";
6925
+ readonly description: "Non-negative safe integer hire grant";
6926
+ }, {
6927
+ readonly flags: "--json";
6928
+ readonly description: "JSON on stdout (always)";
6929
+ }];
6930
+ readonly subcommands: readonly [];
6931
+ }, {
6932
+ readonly name: "backfill-grants";
6933
+ readonly description: "Preview or apply missing hire grants for alive manager lanes";
6934
+ readonly aliases: readonly [];
6935
+ readonly hidden: false;
6936
+ readonly usage: "[options]";
6937
+ readonly arguments: readonly [];
6938
+ readonly options: readonly [{
6939
+ readonly flags: "--dry-run";
6940
+ readonly description: "Print proposed values without writing (default)";
6941
+ }, {
6942
+ readonly flags: "--apply";
6943
+ readonly description: "Write missing hire grants; only run before enforcement is enabled";
6944
+ }, {
6945
+ readonly flags: "--json";
6946
+ readonly description: "JSON on stdout (always)";
6947
+ }];
6948
+ readonly subcommands: readonly [];
6909
6949
  }, {
6910
6950
  readonly name: "list";
6911
6951
  readonly description: "List lanes";
@@ -8898,6 +8898,10 @@ export const COMMAND_CATALOG = [
8898
8898
  "flags": "--senior <ref>",
8899
8899
  "description": "session:<id> | lane:<id> | user"
8900
8900
  },
8901
+ {
8902
+ "flags": "--hire-grant <n>",
8903
+ "description": "Maximum alive direct child lanes assigned to this lane; set by its senior"
8904
+ },
8901
8905
  {
8902
8906
  "flags": "--timeout <secs>",
8903
8907
  "description": "Deadline in seconds (warns, does not kill)"
@@ -8925,6 +8929,54 @@ export const COMMAND_CATALOG = [
8925
8929
  ],
8926
8930
  "subcommands": []
8927
8931
  },
8932
+ {
8933
+ "name": "grant",
8934
+ "description": "Set a lane's alive child-lane grant as its senior",
8935
+ "aliases": [],
8936
+ "hidden": false,
8937
+ "usage": "[options] <lane-id>",
8938
+ "arguments": [
8939
+ {
8940
+ "name": "lane-id",
8941
+ "required": true,
8942
+ "variadic": false
8943
+ }
8944
+ ],
8945
+ "options": [
8946
+ {
8947
+ "flags": "--hire <n>",
8948
+ "description": "Non-negative safe integer hire grant"
8949
+ },
8950
+ {
8951
+ "flags": "--json",
8952
+ "description": "JSON on stdout (always)"
8953
+ }
8954
+ ],
8955
+ "subcommands": []
8956
+ },
8957
+ {
8958
+ "name": "backfill-grants",
8959
+ "description": "Preview or apply missing hire grants for alive manager lanes",
8960
+ "aliases": [],
8961
+ "hidden": false,
8962
+ "usage": "[options]",
8963
+ "arguments": [],
8964
+ "options": [
8965
+ {
8966
+ "flags": "--dry-run",
8967
+ "description": "Print proposed values without writing (default)"
8968
+ },
8969
+ {
8970
+ "flags": "--apply",
8971
+ "description": "Write missing hire grants; only run before enforcement is enabled"
8972
+ },
8973
+ {
8974
+ "flags": "--json",
8975
+ "description": "JSON on stdout (always)"
8976
+ }
8977
+ ],
8978
+ "subcommands": []
8979
+ },
8928
8980
  {
8929
8981
  "name": "list",
8930
8982
  "description": "List lanes",
@@ -209,6 +209,7 @@ export declare function configFromLocalConfig(input: {
209
209
  runtime?: BotRuntimeId;
210
210
  model?: string;
211
211
  };
212
+ preserveFirstStart?: boolean;
212
213
  now?: () => Date;
213
214
  }): {
214
215
  config: BotConfig;
@@ -481,9 +481,6 @@ export async function runBotCreate(nameArg, opts) {
481
481
  const kickoff = opts.kickoff !== undefined ? validateBotKickoff(opts.kickoff) : undefined;
482
482
  const model = opts.model !== undefined ? validateBotModel(opts.model) : undefined;
483
483
  const effort = opts.effort !== undefined ? validateBotEffort(runtime, opts.effort) : undefined;
484
- if (name === RESERVED_SETUP_BOT_NAME && opts.worker?.trim() !== RESERVED_SETUP_BOT_NAME) {
485
- throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
486
- }
487
484
  const dir = botDir(name);
488
485
  if (readBotConfig(dir)) {
489
486
  throw Object.assign(new Error(`Bot "${name}" already exists (${dir}). Use hq bot start ${name} or hq bot rm ${name}.`), {
@@ -495,11 +492,84 @@ export async function runBotCreate(nameArg, opts) {
495
492
  // cost an identity).
496
493
  const worker = opts.worker?.trim() ? resolveBotWorker(hqRoot, opts.worker) : null;
497
494
  const { kind, companies } = resolveCreateKind(opts, worker);
495
+ const isSetupBot = name === RESERVED_SETUP_BOT_NAME && kind === "personal" && worker?.id === SETUP_BOT_WORKER_ID;
496
+ if (name === RESERVED_SETUP_BOT_NAME && kind === "personal" && !isSetupBot) {
497
+ throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
498
+ }
498
499
  const token = await ownerToken();
499
500
  const owner = await resolveCallerPersonUid(token);
500
501
  if (!owner)
501
502
  throw Object.assign(new Error("Your HQ login has no person entity yet; open the HQ desktop app once, then retry."), { expected: true });
502
503
  const api = new BotApi({ token: async () => token });
504
+ const adoptOwnedSetupBot = async (allowUnsupportedListing) => {
505
+ let mine;
506
+ try {
507
+ mine = await api.listMine({ local: true });
508
+ }
509
+ catch (err) {
510
+ const failure = remoteBotsUnavailable(err);
511
+ if (allowUnsupportedListing && failure.reason === "server-unsupported")
512
+ return false;
513
+ if (opts.json)
514
+ console.log(JSON.stringify({ ok: false, ...failure }, null, 2));
515
+ throw Object.assign(new Error(failure.message), { expected: true });
516
+ }
517
+ const matches = matchRemoteBots(mine, name);
518
+ if (matches.length > 1) {
519
+ throw Object.assign(new Error(`You own more than one bot called "${name}": ${matches.map((bot) => bot.agentUid).join(", ")}. Name the one you mean by its id.`), { expected: true });
520
+ }
521
+ if (matches.length === 0)
522
+ return false;
523
+ const remote = matches[0];
524
+ if (!isReservedSetupBotRecord(remote, owner))
525
+ return false;
526
+ const { result, config } = await adoptRemoteBot({
527
+ bot: remote,
528
+ name,
529
+ api,
530
+ hqRoot,
531
+ overrides: { ...(model ? { model } : {}) },
532
+ daemon: opts.daemon !== false,
533
+ preserveFirstStart: true,
534
+ });
535
+ const recovered = {
536
+ agentUid: result.agentUid,
537
+ kind: effectiveBotKind(config),
538
+ companies: effectiveBotCompanies(config),
539
+ runtime: config.runtime,
540
+ dir: result.dir,
541
+ worker: config.workerSource === "worker" ? path.join(hqRoot, config.workerDir) : result.worker,
542
+ workerId: config.workerId ?? null,
543
+ companySlug: config.companySlug ?? null,
544
+ memory: botMemoryMode(config),
545
+ memoryDir: result.memoryDir,
546
+ intro: config.intro ?? null,
547
+ kickoff: config.kickoff ?? null,
548
+ model: config.model ?? null,
549
+ effort: config.effort ?? DEFAULT_BOT_EFFORT,
550
+ };
551
+ if (!opts.json) {
552
+ console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${result.agentUid}, ${result.runtime})`));
553
+ console.log(chalk.dim(` kind: ${describeKind(recovered.kind, recovered.companies)}`));
554
+ console.log(chalk.dim(` worker: ${result.worker}`));
555
+ console.log(chalk.dim(` memory: ${result.memoryDir} (${result.memory})`));
556
+ console.log(chalk.dim(` state: ${result.dir}`));
557
+ if (result.daemon)
558
+ console.log(chalk.dim(` ${result.daemon}`));
559
+ }
560
+ if (opts.start !== false) {
561
+ await startBot(name, result.dir, config, opts.json ? "json" : "text", { ...recovered, daemon: result.daemon });
562
+ return true;
563
+ }
564
+ if (opts.json)
565
+ console.log(JSON.stringify({ ok: true, name, ...recovered, daemon: result.daemon, running: false }, null, 2));
566
+ return true;
567
+ };
568
+ // HQ adds the owner suffix to the setup bot slug. If a create reached the
569
+ // server but its response was lost, recover the caller-owned bot instead
570
+ // of sending another create that the entity store will reject.
571
+ if (isSetupBot && (await adoptOwnedSetupBot(true)))
572
+ return;
503
573
  // A company bot's companies are resolved first (a typo must not cost an
504
574
  // identity) and sent with the create: the server makes the bot a member of
505
575
  // each and refuses the whole create if the owner cannot add agents there.
@@ -549,6 +619,12 @@ export async function runBotCreate(nameArg, opts) {
549
619
  });
550
620
  }
551
621
  if (err instanceof BotApiError && err.status === 409) {
622
+ if (isSetupBot) {
623
+ if (await adoptOwnedSetupBot(false))
624
+ return;
625
+ throw Object.assign(new Error(`HQ could not create the reserved ${name} bot. If you created it on another computer, bring it back with: ` +
626
+ `hq bot adopt ${name}. Otherwise, choose a different name or ask your HQ admin.`), { expected: true });
627
+ }
552
628
  // The cloud already has this bot; it is simply not set up on this
553
629
  // computer (a reinstall, or a second Mac). Re-creating it is the wrong
554
630
  // move — adopting brings the existing one back, memory and all.
@@ -1223,6 +1299,18 @@ export function matchRemoteBots(bots, wanted) {
1223
1299
  (b.name ?? "").trim().toLowerCase() === needle ||
1224
1300
  localNameForRemoteBot(b) === needle);
1225
1301
  }
1302
+ /** A name match alone does not prove that a remote record is HQ's setup bot. */
1303
+ function isReservedSetupBotRecord(bot, ownerUid) {
1304
+ const owner = ownerUid.trim();
1305
+ const local = parseBotLocalConfig(bot.localConfig);
1306
+ return (!!owner &&
1307
+ bot.ownerUid === owner &&
1308
+ bot.slug === `${RESERVED_SETUP_BOT_NAME}-${owner.slice(-8).toLowerCase()}` &&
1309
+ bot.botKind === "personal" &&
1310
+ bot.computeMode === "local" &&
1311
+ local?.workerSource === "worker" &&
1312
+ local.workerId === SETUP_BOT_WORKER_ID);
1313
+ }
1226
1314
  export function resolveRemoteBot(bots, wanted) {
1227
1315
  const hits = matchRemoteBots(bots, wanted);
1228
1316
  if (hits.length === 1)
@@ -1392,8 +1480,10 @@ export function configFromLocalConfig(input) {
1392
1480
  enabled: true,
1393
1481
  // A restore is not a first start: the bot's one-off kickoff task already
1394
1482
  // ran on the machine it was created on, and `runs once` has to keep
1395
- // meaning once. `hq bot intro <name>` has it say hello again.
1396
- introSentAt: (input.now ?? (() => new Date()))().toISOString(),
1483
+ // meaning once. Recovery after a create response was lost is different:
1484
+ // this machine has not started the bot yet, so its first intro/kickoff
1485
+ // must still run.
1486
+ ...(input.preserveFirstStart ? {} : { introSentAt: (input.now ?? (() => new Date()))().toISOString() }),
1397
1487
  };
1398
1488
  return { config, notes };
1399
1489
  }
@@ -1440,6 +1530,7 @@ async function adoptRemoteBot(input) {
1440
1530
  dir,
1441
1531
  local,
1442
1532
  ...(input.overrides ? { overrides: input.overrides } : {}),
1533
+ ...(input.preserveFirstStart ? { preserveFirstStart: true } : {}),
1443
1534
  });
1444
1535
  writeBotConfig(dir, config);
1445
1536
  const scaffoldOpts = { memoryDir: config.memoryDir };
@@ -9,9 +9,10 @@ import { Command } from "commander";
9
9
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
10
10
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
11
11
  export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
12
+ export { LANE_HIRE_GRANTS_FLAG };
12
13
  import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
13
14
  import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
14
- import { MAX_LANE_DEPTH, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
15
+ import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
15
16
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
16
17
  import { type RunCompactionFn } from "../lib/lanes/compaction.js";
17
18
  import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
@@ -77,6 +78,8 @@ export interface LanesCommandDeps {
77
78
  monitorEvidence?: MonitorEvidenceReader;
78
79
  /** Test seam for the default-off outpost placement rollout flag. */
79
80
  lanePlacementEnabled?: () => boolean;
81
+ /** Test seam for the default-off lane hire-grants rollout gate. */
82
+ laneHireGrantsEnabled?: () => boolean;
80
83
  }
81
84
  export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
82
85
  export declare function parseSeniorRef(raw: string): SeniorRef;