@indigoai-us/hq-cli 5.345.199 → 5.345.200

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 (41) hide show
  1. package/CHANGELOG.md +26 -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 +6 -0
  8. package/dist/commands/bot.js +39 -10
  9. package/dist/commands/creators.js +2 -1
  10. package/dist/commands/library-remove.js +2 -1
  11. package/dist/commands/library.js +5 -4
  12. package/dist/commands/mesh.d.ts +0 -7
  13. package/dist/commands/mesh.js +5 -1
  14. package/dist/commands/sync-held.js +2 -2
  15. package/dist/lib/bot/fleet-download.d.ts +27 -0
  16. package/dist/lib/bot/fleet-download.js +162 -0
  17. package/dist/lib/bot/fleet-local.d.ts +51 -2
  18. package/dist/lib/bot/fleet-local.js +177 -74
  19. package/dist/lib/bot/fleet-python.d.ts +2 -0
  20. package/dist/lib/bot/fleet-python.js +29 -0
  21. package/dist/lib/lanes/mesh-events.js +3 -1
  22. package/dist/lib/mesh/api.js +2 -1
  23. package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
  24. package/dist/lib/mesh/live/daemon/run.js +3 -0
  25. package/dist/lib/mesh/live/emit.d.ts +2 -11
  26. package/dist/lib/mesh/live/emit.js +20 -2
  27. package/dist/lib/mesh/live/flush.d.ts +2 -0
  28. package/dist/lib/mesh/live/flush.js +34 -3
  29. package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
  30. package/dist/lib/mesh/live/reporting-privacy.js +47 -0
  31. package/dist/lib/work-context/company.d.ts +2 -3
  32. package/dist/lib/work-context/company.js +4 -22
  33. package/dist/secrets-env-action.js +5 -2
  34. package/dist/utils/cognito-callback-port.d.ts +2 -0
  35. package/dist/utils/cognito-callback-port.js +10 -5
  36. package/dist/utils/cognito-config.js +6 -1
  37. package/dist/utils/cognito-session.js +6 -1
  38. package/dist/utils/interactive-caller.d.ts +8 -0
  39. package/dist/utils/interactive-caller.js +10 -0
  40. package/dist/utils/login-browser.js +2 -1
  41. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.200] — 2026-11-04
6
+ - `hq creators apply`, `hq library` (search, add, describe, link, rm),
7
+ `hq sync held`, and `hq lanes` no longer open a browser sign-in when no
8
+ person is at a terminal (a lane worker, daemon, bot, launchd task, or piped
9
+ child). They print the `hq login` remedy instead. The headless `hq login`
10
+ URL path now uses a registered callback port, and a stray
11
+ `HQ_COGNITO_CALLBACK_PORT` warning prints once per process.
12
+ - The stray callback-port warning no longer prints on commands that never
13
+ sign in; the port is read when a sign-in starts. `hq bot` and `hq secrets`
14
+ sign-ins now go through the same interactive-caller gate.
15
+ - `hq bot runtime install` downloads and builds the HQ Fleet Runtime once for
16
+ this computer, with progress, so `hq bot create` afterwards takes seconds.
17
+ `hq bot runtime status` says which of the three steps (download, Python,
18
+ environment) are done. `--json` prints one event per line for the desktop app.
19
+ - The runtime download streams to disk, continues where it stopped after a
20
+ stall or a dropped connection, and no longer has a 20 minute cap. Creates and
21
+ adopts started at the same time share one download through a machine-wide
22
+ lock instead of each fetching the same archive. A gzip runtime archive is
23
+ accepted alongside the plain tar, and a matching archive from any installed
24
+ release is reused without a download.
25
+ - `hq bot create`, `hq bot adopt` and `hq bot restore` print what the runtime
26
+ install is doing on stderr (download percentage, then each build step)
27
+ unless `--json` is given.
28
+ - Personal and unattributed conversations are no longer assigned from a device default company or sent as Work Mesh summaries. Reporting respects the HQ Anywhere off setting.
29
+ - Validate session capability documents before reading company grants.
30
+
5
31
  ## [5.345.199] — 2026-11-04
6
32
  - `hq bot status <name>` shows the bot's last reported failure: when, the
7
33
  failure class, the plain-language cause and fix the owner was sent, the HQ
@@ -53,8 +53,8 @@ session_scope_read() {
53
53
  local root="${1:-}" sid="${2:-}" aid="${3:-}" cap
54
54
  cap="$(session_scope_capability_path "$root" "$sid" "$aid")" || return 0
55
55
  [ -f "$cap" ] || return 0
56
- jq -r --arg sid "$sid" --arg aid "$aid" \
57
- 'select(.session_id == $sid and (.agent_id // "") == $aid) | .company_slug // empty' \
56
+ jq -sr --arg sid "$sid" --arg aid "$aid" \
57
+ 'if length == 1 and (.[0] | type) == "object" then .[0] else error("capability must contain one object") end | select(.session_id == $sid and (.agent_id // "") == $aid) | .company_slug // empty' \
58
58
  "$cap" 2>/dev/null || true
59
59
  }
60
60
 
@@ -66,8 +66,10 @@ session_scope_read_companies() {
66
66
  local -a slugs
67
67
  cap="$(session_scope_capability_path "$root" "$sid" "$aid")" || return 0
68
68
  [ -f "$cap" ] || return 0
69
- lock_info="$(jq -er --arg sid "$sid" --arg aid "$aid" '
70
- select(.session_id == $sid and (.agent_id // "") == $aid)
69
+ lock_info="$(jq -ser --arg sid "$sid" --arg aid "$aid" '
70
+ if length == 1 and (.[0] | type) == "object" then .[0]
71
+ else error("capability must contain one object") end
72
+ | select(.session_id == $sid and (.agent_id // "") == $aid)
71
73
  | if (.company_slug | type) != "string" or (.company_slug | test("^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$") | not) then error("invalid primary")
72
74
  elif has("company_slugs") then
73
75
  if (.company_slugs | type) == "array" and all(.company_slugs[]; type == "string" and test("^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"))
@@ -19810,6 +19810,50 @@ export declare const COMMAND_CATALOG: readonly [{
19810
19810
  }];
19811
19811
  readonly options: readonly [];
19812
19812
  readonly subcommands: readonly [];
19813
+ }, {
19814
+ readonly name: "runtime";
19815
+ readonly description: "The HQ Fleet Runtime every local bot on this computer shares";
19816
+ readonly aliases: readonly [];
19817
+ readonly hidden: false;
19818
+ readonly helpOption: true;
19819
+ readonly helpCommand: true;
19820
+ readonly usage: "[options] [command]";
19821
+ readonly helpText: readonly [];
19822
+ readonly arguments: readonly [];
19823
+ readonly options: readonly [];
19824
+ readonly subcommands: readonly [{
19825
+ readonly name: "install";
19826
+ readonly 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";
19827
+ readonly aliases: readonly [];
19828
+ readonly hidden: false;
19829
+ readonly helpOption: true;
19830
+ readonly helpCommand: false;
19831
+ readonly usage: "[options]";
19832
+ readonly helpText: readonly [];
19833
+ readonly arguments: readonly [];
19834
+ readonly options: readonly [{
19835
+ readonly flags: "--json";
19836
+ readonly description: "Print one JSON object per line: progress events, then { ok, event: \"done\", release, runtimeDir }";
19837
+ readonly hidden: false;
19838
+ }];
19839
+ readonly subcommands: readonly [];
19840
+ }, {
19841
+ readonly name: "status";
19842
+ readonly description: "Say whether the runtime is installed on this computer, step by step";
19843
+ readonly aliases: readonly [];
19844
+ readonly hidden: false;
19845
+ readonly helpOption: true;
19846
+ readonly helpCommand: false;
19847
+ readonly usage: "[options]";
19848
+ readonly helpText: readonly [];
19849
+ readonly arguments: readonly [];
19850
+ readonly options: readonly [{
19851
+ readonly flags: "--json";
19852
+ readonly description: "Print machine-readable JSON";
19853
+ readonly hidden: false;
19854
+ }];
19855
+ readonly subcommands: readonly [];
19856
+ }];
19813
19857
  }, {
19814
19858
  readonly name: "daemon";
19815
19859
  readonly description: "Manage the bot's launchd user agent (macOS)";
@@ -24159,6 +24159,58 @@ export const COMMAND_CATALOG = [
24159
24159
  "options": [],
24160
24160
  "subcommands": []
24161
24161
  },
24162
+ {
24163
+ "name": "runtime",
24164
+ "description": "The HQ Fleet Runtime every local bot on this computer shares",
24165
+ "aliases": [],
24166
+ "hidden": false,
24167
+ "helpOption": true,
24168
+ "helpCommand": true,
24169
+ "usage": "[options] [command]",
24170
+ "helpText": [],
24171
+ "arguments": [],
24172
+ "options": [],
24173
+ "subcommands": [
24174
+ {
24175
+ "name": "install",
24176
+ "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",
24177
+ "aliases": [],
24178
+ "hidden": false,
24179
+ "helpOption": true,
24180
+ "helpCommand": false,
24181
+ "usage": "[options]",
24182
+ "helpText": [],
24183
+ "arguments": [],
24184
+ "options": [
24185
+ {
24186
+ "flags": "--json",
24187
+ "description": "Print one JSON object per line: progress events, then { ok, event: \"done\", release, runtimeDir }",
24188
+ "hidden": false
24189
+ }
24190
+ ],
24191
+ "subcommands": []
24192
+ },
24193
+ {
24194
+ "name": "status",
24195
+ "description": "Say whether the runtime is installed on this computer, step by step",
24196
+ "aliases": [],
24197
+ "hidden": false,
24198
+ "helpOption": true,
24199
+ "helpCommand": false,
24200
+ "usage": "[options]",
24201
+ "helpText": [],
24202
+ "arguments": [],
24203
+ "options": [
24204
+ {
24205
+ "flags": "--json",
24206
+ "description": "Print machine-readable JSON",
24207
+ "hidden": false
24208
+ }
24209
+ ],
24210
+ "subcommands": []
24211
+ }
24212
+ ]
24213
+ },
24162
24214
  {
24163
24215
  "name": "daemon",
24164
24216
  "description": "Manage the bot's launchd user agent (macOS)",
@@ -0,0 +1,35 @@
1
+ import { type FleetInstallProgress, type FleetInstallProgressListener, type FleetLocalTargetRelease } from "../lib/bot/fleet-local.js";
2
+ export type FleetProgressMode = "text" | "json" | "silent";
3
+ interface ProgressOutput {
4
+ write(text: string): void;
5
+ isTTY: boolean;
6
+ }
7
+ /** The sentence for a phase, for people. */
8
+ export declare function describeFleetInstallPhase(event: FleetInstallProgress): string;
9
+ /**
10
+ * Progress for a terminal (stderr, one line that rewrites itself on a TTY and
11
+ * prints at each tenth otherwise), for a program (one JSON object per line on
12
+ * stdout), or nothing.
13
+ */
14
+ export declare function fleetInstallProgressReporter(mode: FleetProgressMode, output?: ProgressOutput, stdout?: (line: string) => void): FleetInstallProgressListener;
15
+ export interface FleetRuntimeStatus {
16
+ ready: boolean;
17
+ release: {
18
+ version: string;
19
+ source: FleetLocalTargetRelease["source"];
20
+ };
21
+ steps: {
22
+ distribution: boolean;
23
+ python: boolean;
24
+ runtime: boolean;
25
+ };
26
+ }
27
+ export declare function fleetRuntimeStatus(): Promise<FleetRuntimeStatus>;
28
+ export declare function runBotRuntimeInstall(opts: {
29
+ json?: boolean;
30
+ }): Promise<void>;
31
+ export declare function runBotRuntimeStatus(opts: {
32
+ json?: boolean;
33
+ }): Promise<void>;
34
+ export {};
35
+ //# sourceMappingURL=bot-runtime.d.ts.map
@@ -0,0 +1,115 @@
1
+ import chalk from "chalk";
2
+ import { ensureFleetRuntimeInstalled, fleetLocalDistributionInstalled, fleetRuntimeReady, installTargetFleetLocalDistribution, resolveFleetLocalTargetRelease, } from "../lib/bot/fleet-local.js";
3
+ import { installedFleetPython } from "../lib/bot/fleet-python.js";
4
+ function megabytes(bytes) {
5
+ return `${(bytes / 1_048_576).toFixed(1)} MB`;
6
+ }
7
+ /** The sentence for a phase, for people. */
8
+ export function describeFleetInstallPhase(event) {
9
+ switch (event.phase) {
10
+ case "download": {
11
+ const name = event.file.startsWith("hq-fleet-runtime-") ? "the HQ Fleet Runtime" : event.file;
12
+ if (event.total === null)
13
+ return `Downloading ${name}: ${megabytes(event.received)}`;
14
+ const percent = Math.min(100, Math.floor((event.received / Math.max(event.total, 1)) * 100));
15
+ return `Downloading ${name}: ${megabytes(event.received)} of ${megabytes(event.total)} (${percent}%)`;
16
+ }
17
+ case "verify":
18
+ return "Checking the download against its signed release";
19
+ case "python":
20
+ return "Preparing the runtime's private Python";
21
+ case "extract":
22
+ return "Unpacking the runtime";
23
+ case "environment":
24
+ return "Creating the runtime environment";
25
+ case "packages":
26
+ return "Installing the runtime's packages (a minute or two)";
27
+ case "ready":
28
+ if (event.step === "download")
29
+ return event.cached ? "The runtime download is already on this computer" : "The runtime download is complete";
30
+ return event.cached ? "The runtime is already installed on this computer" : "The runtime is ready";
31
+ }
32
+ }
33
+ /**
34
+ * Progress for a terminal (stderr, one line that rewrites itself on a TTY and
35
+ * prints at each tenth otherwise), for a program (one JSON object per line on
36
+ * stdout), or nothing.
37
+ */
38
+ export function fleetInstallProgressReporter(mode, output = { write: (text) => { process.stderr.write(text); }, isTTY: process.stderr.isTTY === true }, stdout = (line) => { process.stdout.write(`${line}\n`); }) {
39
+ if (mode === "silent")
40
+ return () => undefined;
41
+ if (mode === "json") {
42
+ return (event) => {
43
+ const percent = event.phase === "download" && event.total !== null
44
+ ? Math.min(100, Math.floor((event.received / Math.max(event.total, 1)) * 100))
45
+ : null;
46
+ stdout(JSON.stringify({ event: "progress", ...event, percent, message: describeFleetInstallPhase(event) }));
47
+ };
48
+ }
49
+ let lastTenth = -1;
50
+ let lineOpen = false;
51
+ return (event) => {
52
+ const text = describeFleetInstallPhase(event);
53
+ if (event.phase === "download" && event.total !== null) {
54
+ const tenth = Math.floor((event.received / Math.max(event.total, 1)) * 10);
55
+ if (output.isTTY) {
56
+ output.write(`\r${text}${event.received >= event.total ? "\n" : ""}`);
57
+ lineOpen = event.received < event.total;
58
+ return;
59
+ }
60
+ if (tenth === lastTenth && event.received < event.total)
61
+ return;
62
+ lastTenth = tenth;
63
+ output.write(`${text}\n`);
64
+ return;
65
+ }
66
+ if (lineOpen) {
67
+ output.write("\n");
68
+ lineOpen = false;
69
+ }
70
+ output.write(`${text}\n`);
71
+ };
72
+ }
73
+ export async function fleetRuntimeStatus() {
74
+ const target = await resolveFleetLocalTargetRelease();
75
+ const distribution = fleetLocalDistributionInstalled(target.release);
76
+ const python = installedFleetPython();
77
+ const runtime = distribution && python ? fleetRuntimeReady({ release: target.release }, { python }) : false;
78
+ return {
79
+ ready: distribution && python !== null && runtime,
80
+ release: { version: target.release.version, source: target.source },
81
+ steps: { distribution, python: python !== null, runtime },
82
+ };
83
+ }
84
+ export async function runBotRuntimeInstall(opts) {
85
+ const report = fleetInstallProgressReporter(opts.json ? "json" : "text");
86
+ const target = await resolveFleetLocalTargetRelease();
87
+ const installation = await installTargetFleetLocalDistribution({ target, onProgress: report });
88
+ const result = await ensureFleetRuntimeInstalled(installation, { onProgress: report });
89
+ if (opts.json) {
90
+ console.log(JSON.stringify({
91
+ ok: true,
92
+ event: "done",
93
+ release: { version: result.release.version, source: target.source },
94
+ distributionDir: result.distributionDir,
95
+ runtimeDir: result.runtimeDir,
96
+ }));
97
+ return;
98
+ }
99
+ console.log(`${chalk.green("HQ Fleet Runtime " + result.release.version + " is ready on this computer.")} ${chalk.dim(result.runtimeDir)}`);
100
+ }
101
+ export async function runBotRuntimeStatus(opts) {
102
+ const current = await fleetRuntimeStatus();
103
+ if (opts.json) {
104
+ console.log(JSON.stringify({ ok: true, ...current }, null, 2));
105
+ return;
106
+ }
107
+ const mark = (done) => (done ? chalk.green("ready") : chalk.yellow("missing"));
108
+ console.log(`HQ Fleet Runtime ${current.release.version} ${current.ready ? chalk.green("ready") : chalk.yellow("not installed yet")}`);
109
+ console.log(` download: ${mark(current.steps.distribution)}`);
110
+ console.log(` python: ${mark(current.steps.python)}`);
111
+ console.log(` environment: ${mark(current.steps.runtime)}`);
112
+ if (!current.ready)
113
+ console.log(chalk.dim(" Run `hq bot runtime install` to get it ready before creating a bot."));
114
+ }
115
+ //# sourceMappingURL=bot-runtime.js.map
@@ -101,6 +101,12 @@ export interface BotRow {
101
101
  export declare function listBotNames(root: string): string[];
102
102
  /** Bot-identity token supplier: per-bot creds + per-bot token cache dir. */
103
103
  export declare function botTokenSupplier(dir: string): () => Promise<string>;
104
+ /**
105
+ * Whether `hq bot` may open a browser sign-in for its owner: a TTY on stdin,
106
+ * and the shared interactive-caller gate (no lane, daemon, bot runtime, or
107
+ * HQ_NO_BROWSER_LOGIN child).
108
+ */
109
+ export declare function ownerSignInAllowed(): boolean;
104
110
  /**
105
111
  * The bot's word on its coding tool's sign-in. Only a running bot's report
106
112
  * counts: a stopped bot's last check may be from before a sign-in or a
@@ -71,6 +71,8 @@ import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime
71
71
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
72
72
  import { BOT_LOGIN_RUNTIMES, botAuthEnv, botRuntimeAuth, capacityAccountHome, describeBotAuth, formatBotAuth, isBotLoginRuntime, restoreBotSessions, setAsideBotSessions, signInBotRuntime, signOutBotRuntime, withRuntimeAuth, } from "../lib/bot/runtime-auth.js";
73
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";
74
+ import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
75
+ import { fleetInstallProgressReporter, runBotRuntimeInstall, runBotRuntimeStatus } from "./bot-runtime.js";
74
76
  function collectRepeated(value, previous) {
75
77
  return [...previous, value];
76
78
  }
@@ -332,7 +334,15 @@ export function botTokenSupplier(dir) {
332
334
  process.env.HQ_MACHINE_TOKEN_STATE_DIR = botTokenStateDir(dir);
333
335
  return () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
334
336
  }
335
- async function ownerToken(interactive = process.stdin.isTTY === true) {
337
+ /**
338
+ * Whether `hq bot` may open a browser sign-in for its owner: a TTY on stdin,
339
+ * and the shared interactive-caller gate (no lane, daemon, bot runtime, or
340
+ * HQ_NO_BROWSER_LOGIN child).
341
+ */
342
+ export function ownerSignInAllowed() {
343
+ return gatedInteractiveLogin(process.stdin.isTTY === true);
344
+ }
345
+ async function ownerToken(interactive = ownerSignInAllowed()) {
336
346
  if (peekHqApiKey()) {
337
347
  throw Object.assign(new Error("hq bot needs your HQ login session (HQ_API_KEY is not supported here). Run `hq login`."), {
338
348
  expected: true,
@@ -735,9 +745,10 @@ async function installFleetRuntimeProfile(name, dir, config, opts = {}) {
735
745
  return null;
736
746
  if (!opts.force && !fleetLocalProfileNeedsAdoption(config.agentUid, { release: target.release }))
737
747
  return null;
748
+ const progress = opts.progress ?? fleetInstallProgressReporter("silent");
738
749
  let installation;
739
750
  try {
740
- installation = await installTargetFleetLocalDistribution({ target });
751
+ installation = await installTargetFleetLocalDistribution({ target, onProgress: progress });
741
752
  }
742
753
  catch (err) {
743
754
  if (target.source !== "pointer")
@@ -749,8 +760,9 @@ async function installFleetRuntimeProfile(name, dir, config, opts = {}) {
749
760
  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
761
  if (!opts.force && !fleetLocalProfileNeedsAdoption(config.agentUid, { release: HQ_FLEET_LOCAL_RELEASE }))
751
762
  return null;
752
- installation = await installPinnedFleetLocalDistribution();
763
+ installation = await installPinnedFleetLocalDistribution({ onProgress: progress });
753
764
  }
765
+ progress({ phase: "python" });
754
766
  const python = await ensureFleetPython();
755
767
  const companyScope = effectiveBotKind(config) === "company"
756
768
  ? effectiveBotCompanies(config)
@@ -765,7 +777,7 @@ async function installFleetRuntimeProfile(name, dir, config, opts = {}) {
765
777
  provider: fleetProviderForRuntime(config.runtime, config.model),
766
778
  ownerCompanies: companyScope,
767
779
  ...(config.autoApprove === true ? { autoApprove: true } : {}),
768
- }, { python, start: false });
780
+ }, { python, start: false, onProgress: progress });
769
781
  const credentials = botCredsPath(dir);
770
782
  if (fs.existsSync(credentials))
771
783
  importFleetLocalMachineCredential(config.agentUid, credentials, {}, { keepSource: opts.keepCredentials === true });
@@ -1228,7 +1240,9 @@ async function runBotCreateLocked(name, opts) {
1228
1240
  // of sending another create that the entity store will reject.
1229
1241
  if (isSetupBot && (await adoptOwnedSetupBot(true)))
1230
1242
  return;
1231
- const fleetInstallation = await installTargetFleetLocalDistribution();
1243
+ const progress = fleetInstallProgressReporter(opts.json ? "silent" : "text");
1244
+ const fleetInstallation = await installTargetFleetLocalDistribution({ onProgress: progress });
1245
+ progress({ phase: "python" });
1232
1246
  const fleetPython = await ensureFleetPython();
1233
1247
  const ownerCompanies = kind === "personal" ? await fleetOwnerCompanies() : companies;
1234
1248
  // A company bot's companies are resolved first (a typo must not cost an
@@ -1388,7 +1402,7 @@ async function runBotCreateLocked(name, opts) {
1388
1402
  firstStart: toolOffer
1389
1403
  ? { intro: setupToolOfferText(toolOffer, setupWelcomeOpening(intro)) }
1390
1404
  : { intro: introText, ...(kickoff ? { kickoff } : {}) },
1391
- }, { start: opts.start !== false && !fleetAuthPending, python: fleetPython });
1405
+ }, { start: opts.start !== false && !fleetAuthPending, python: fleetPython, onProgress: progress });
1392
1406
  syncFleetRuntimeJobs(name, dir, config);
1393
1407
  const fleetProfile = fleetBootstrap.profile;
1394
1408
  const runtimeLabel = HQ_FLEET_RUNTIME_ENGINE_ID;
@@ -2776,7 +2790,10 @@ async function adoptRemoteBotLocked(input) {
2776
2790
  ...(input.preserveFirstStart ? { preserveFirstStart: true } : {}),
2777
2791
  });
2778
2792
  // 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;
2793
+ const progress = input.progress ?? fleetInstallProgressReporter("silent");
2794
+ const fleetInstallation = fleetEngine ? await installPinnedFleetLocalDistribution({ onProgress: progress }) : null;
2795
+ if (fleetInstallation)
2796
+ progress({ phase: "python" });
2780
2797
  const fleetPython = fleetInstallation ? await ensureFleetPython() : null;
2781
2798
  if (fleetInstallation)
2782
2799
  config.engine = fleetInstallation.engine;
@@ -2805,7 +2822,7 @@ async function adoptRemoteBotLocked(input) {
2805
2822
  },
2806
2823
  }
2807
2824
  : {}),
2808
- }, { python: fleetPython, start: false });
2825
+ }, { python: fleetPython, start: false, onProgress: progress });
2809
2826
  }
2810
2827
  createBotLogger(dir)("info", `adopted ${name} (${bot.agentUid}) on ${localMachineId()} kind=${config.kind}${local ? "" : " (no saved settings; defaults)"} runtime=${config.runtime}`);
2811
2828
  let daemonMessage = null;
@@ -2870,6 +2887,7 @@ export async function runBotAdopt(nameArg, opts) {
2870
2887
  name,
2871
2888
  api,
2872
2889
  hqRoot,
2890
+ progress: fleetInstallProgressReporter(opts.json ? "silent" : "text"),
2873
2891
  overrides: {
2874
2892
  ...(isBotRuntimeId(opts.runtime) ? { runtime: opts.runtime } : {}),
2875
2893
  ...(model ? { model } : {}),
@@ -2983,7 +3001,7 @@ export async function runBotRestore(opts) {
2983
3001
  if (here) {
2984
3002
  return { name, agentUid: bot.agentUid, action: "repaired", detail: await repairLocalBot(name, api, bot, opts.daemon !== false), ...shown };
2985
3003
  }
2986
- const { result, config } = await adoptRemoteBot({ bot, name, api, hqRoot, daemon: opts.daemon !== false });
3004
+ const { result, config } = await adoptRemoteBot({ bot, name, api, hqRoot, daemon: opts.daemon !== false, progress: fleetInstallProgressReporter(opts.json ? "silent" : "text") });
2987
3005
  // Silent: this command's own JSON has to be the only document on stdout.
2988
3006
  const started = opts.start === false ? null : await startBot(name, result.dir, config, "silent").catch(() => null);
2989
3007
  return {
@@ -3173,7 +3191,7 @@ export function registerBotCommand(program) {
3173
3191
  const b = requireBot(name);
3174
3192
  if (isFleetRuntimeBot(b.config)) {
3175
3193
  refuseHeldFleetBot(b.dir);
3176
- const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config);
3194
+ const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config, { progress: fleetInstallProgressReporter(opts.json ? "silent" : "text") });
3177
3195
  runFleetRuntimeControl(b.name, b.config, adopted ? "install" : "start", opts.json === true);
3178
3196
  return;
3179
3197
  }
@@ -3271,6 +3289,17 @@ export function registerBotCommand(program) {
3271
3289
  .command("intro <name>")
3272
3290
  .description("Have the bot re-send its introduction DM to you")
3273
3291
  .action((name) => wrap(() => runBotIntro(name))());
3292
+ const runtime = bot.command("runtime").description("The HQ Fleet Runtime every local bot on this computer shares");
3293
+ runtime
3294
+ .command("install")
3295
+ .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")
3296
+ .option("--json", "Print one JSON object per line: progress events, then { ok, event: \"done\", release, runtimeDir }")
3297
+ .action((opts) => wrap(() => runBotRuntimeInstall(opts))());
3298
+ runtime
3299
+ .command("status")
3300
+ .description("Say whether the runtime is installed on this computer, step by step")
3301
+ .option("--json", "Print machine-readable JSON")
3302
+ .action((opts) => wrap(() => runBotRuntimeStatus(opts))());
3274
3303
  const daemon = bot.command("daemon").description("Manage the bot's launchd user agent (macOS)");
3275
3304
  daemon
3276
3305
  .command("install <name>")
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { ensureCognitoToken } from "../utils/cognito-session.js";
3
3
  import { vaultApiFetch } from "../utils/vault-api.js";
4
+ import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
4
5
  /**
5
6
  * Testable core. Submits the application and maps the server response to a
6
7
  * single human-readable message. Throws on hard failures.
@@ -46,7 +47,7 @@ export function registerCreatorsCommand(program) {
46
47
  .action(async (options) => {
47
48
  try {
48
49
  const result = await runCreatorApply({ reason: options.reason, handle: options.handle }, {
49
- getAccessToken: () => ensureCognitoToken({ interactive: true }),
50
+ getAccessToken: () => ensureCognitoToken({ interactive: gatedInteractiveLogin() }),
50
51
  post: (token, body) => vaultApiFetch({
51
52
  token,
52
53
  path: "/v1/creators/request-access",
@@ -44,6 +44,7 @@ import * as readline from "node:readline";
44
44
  import chalk from "chalk";
45
45
  import { isCloudPlaceholderName, parseCloudPlaceholder } from "@indigoai-us/hq-cloud";
46
46
  import { OFFLOAD_PREFIX, PLACEHOLDER_SUFFIX, digestFile, resolvePlaceholder } from "./storage-offload.js";
47
+ import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
47
48
  /** A company library blob key: `.hq-offload/blobs/<64 lowercase hex>`. */
48
49
  const BLOB_KEY_PATTERN = /^\.hq-offload\/blobs\/([0-9a-f]{64})$/;
49
50
  /** hq-pro caps an asset search at 50 hits. */
@@ -637,7 +638,7 @@ export function registerLibraryRemoveCommand(library) {
637
638
  import("../utils/manifest.js"),
638
639
  ]);
639
640
  const credential = await resolveVaultCredentialForCapability("files:write", "library rm", {
640
- interactive: true,
641
+ interactive: gatedInteractiveLogin(),
641
642
  });
642
643
  const { token } = credential;
643
644
  const apiKey = credential.kind === "api-key";
@@ -41,6 +41,7 @@ import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag
41
41
  import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
42
42
  import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
43
43
  import { registerLibraryRemoveCommand } from "./library-remove.js";
44
+ import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
44
45
  /** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
45
46
  export const MAX_LIBRARY_SEARCH_TAGS = 20;
46
47
  const MAX_TAG_LENGTH = 100;
@@ -445,7 +446,7 @@ export function registerLibraryCommand(program) {
445
446
  };
446
447
  const result = await runLibrarySearch(queryParts.join(" "), searchOptions, {
447
448
  getToken: async () => (await resolveVaultCredentialForCapability("search:query", "library search", {
448
- interactive: true,
449
+ interactive: gatedInteractiveLogin(),
449
450
  })).token,
450
451
  post: (token, body) => vaultApiFetch({ token, path: "/v1/search", method: "POST", body }),
451
452
  searchLocal: (query, opts) => searchLocalAssets(query, opts, defaultLocalEnv()),
@@ -476,7 +477,7 @@ export function registerLibraryCommand(program) {
476
477
  .action(async (sources, options) => {
477
478
  try {
478
479
  const credential = await resolveVaultCredentialForCapability("files:write", "library add", {
479
- interactive: true,
480
+ interactive: gatedInteractiveLogin(),
480
481
  });
481
482
  const { token } = credential;
482
483
  const hqRoot = findHqRoot();
@@ -561,7 +562,7 @@ export function registerLibraryCommand(program) {
561
562
  .action(async (target, options) => {
562
563
  try {
563
564
  const credential = await resolveVaultCredentialForCapability("files:read", "library describe", {
564
- interactive: true,
565
+ interactive: gatedInteractiveLogin(),
565
566
  });
566
567
  const { token } = credential;
567
568
  const hqRoot = findHqRoot();
@@ -618,7 +619,7 @@ export function registerLibraryCommand(program) {
618
619
  .action(async (target, options) => {
619
620
  try {
620
621
  const credential = await resolveVaultCredentialForCapability("files:read", "library link", {
621
- interactive: true,
622
+ interactive: gatedInteractiveLogin(),
622
623
  });
623
624
  const { token } = credential;
624
625
  const result = await runLibraryLink(target, { company: options.company, expiresMinutes: options.expires }, {
@@ -1,10 +1,3 @@
1
- /**
2
- * `hq mesh` — work-mesh verbs for agents and local sessions.
3
- *
4
- * Native hq-cli: Cognito + hq-pro REST + ~/.hq/work-mesh/cache. Does not
5
- * require hq-pack-work-mesh on disk and does not start MQTT listen.
6
- * Distinct from `hq doctor` (hook guardrails).
7
- */
8
1
  import { Command } from "commander";
9
2
  import { type MeshCompany, type MeshProjectRepo, type MeshProjectStoryInput, type MeshThread } from "../lib/mesh/api.js";
10
3
  import { UnknownProjectStoryStateError } from "../lib/mesh/story-status.js";
@@ -1,3 +1,4 @@
1
+ import { reportingRuntimeAllowed } from "../lib/mesh/live/reporting-privacy.js";
1
2
  /**
2
3
  * `hq mesh` — work-mesh verbs for agents and local sessions.
3
4
  *
@@ -1123,6 +1124,7 @@ async function runPrdSync(opts) {
1123
1124
  }));
1124
1125
  const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
1125
1126
  const emitted = await emitEvents({
1127
+ reportingAllowed: () => reportingRuntimeAllowed(isMachineIdentity()),
1126
1128
  workMeshRoot: meshRoot,
1127
1129
  poster,
1128
1130
  newEvents: events,
@@ -2534,6 +2536,7 @@ async function runSessionEnqueue(cliKind, opts) {
2534
2536
  baseUrl: resolveVaultApiBase(process.env),
2535
2537
  });
2536
2538
  const summary = await emitEvents({
2539
+ reportingAllowed: () => reportingRuntimeAllowed(isMachineIdentity()),
2537
2540
  workMeshRoot: meshRoot,
2538
2541
  poster,
2539
2542
  newEvents: [event],
@@ -2617,7 +2620,7 @@ async function runMeshEmit(opts) {
2617
2620
  skipped = backlog.skipped;
2618
2621
  newEvents = backlog.events;
2619
2622
  }
2620
- const summary = await emitEvents({ workMeshRoot: meshRoot, poster, newEvents });
2623
+ const summary = await emitEvents({ reportingAllowed: () => reportingRuntimeAllowed(isMachineIdentity()), workMeshRoot: meshRoot, poster, newEvents });
2621
2624
  const out = {
2622
2625
  ok: true,
2623
2626
  action: "emit",
@@ -2643,6 +2646,7 @@ async function runSessionFlush(opts) {
2643
2646
  baseUrl: resolveVaultApiBase(process.env),
2644
2647
  });
2645
2648
  const summary = await flushSessionEvents({
2649
+ reportingAllowed: () => reportingRuntimeAllowed(isMachineIdentity()),
2646
2650
  workMeshRoot: workMeshHomeRoot(),
2647
2651
  workContextRoot: workContextHomeRoot(),
2648
2652
  poster,
@@ -19,6 +19,7 @@ import { VaultClient, PERSONAL_VAULT_JOURNAL_SLUG, getStateDir } from "@indigoai
19
19
  import { buildVaultConfig, cognitoSubjectFromToken, ensureCognitoToken } from "../utils/cognito-session.js";
20
20
  import { decodeJwtPayload } from "../utils/person-token-cache.js";
21
21
  import { resolveCanonicalPersonUid } from "./cloud.js";
22
+ import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
22
23
  export const HELD_UNAVAILABLE_MESSAGE = "this HQ build cannot list or release held uploads yet; update HQ and try again";
23
24
  export function fenceExports(mod = hqCloud) {
24
25
  const list = mod.listPushIdentityFences;
@@ -157,11 +158,10 @@ export async function resolveHeldIdentity(options, io) {
157
158
  return { sub, personalUid };
158
159
  }
159
160
  export function createDefaultSyncHeldDeps() {
160
- const tty = process.stdin.isTTY === true && process.stdout.isTTY === true;
161
161
  return {
162
162
  exports: fenceExports(),
163
163
  stateDir: getStateDir(),
164
- identity: (options) => resolveHeldIdentity({ needPersonal: options.needPersonal, interactive: options.interactive && tty }, {
164
+ identity: (options) => resolveHeldIdentity({ needPersonal: options.needPersonal, interactive: gatedInteractiveLogin(options.interactive) }, {
165
165
  ensureToken: (interactive) => ensureCognitoToken({ interactive }),
166
166
  listPersons: async (token) => (await new VaultClient(buildVaultConfig(token)).entity.listByType("person")),
167
167
  }),