@indigoai-us/hq-cli 5.345.189 → 5.345.191

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,62 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.191] — 2026-11-02
6
+ - `hq bot list` and `hq bot status --json` rows now carry `engine: "hq-fleet-runtime"` for a bot on
7
+ the HQ Fleet Runtime, so the desktop app can tell a Fleet bot from a model-CLI bot (Create brief
8
+ uses this to prefer a Fleet bot).
9
+ - `hq bot auth` on an HQ Fleet Runtime bot gives the restarted gateway up to 15 s to come up before
10
+ reporting; it used to say "signed in, but did not start" while the bot was in fact starting.
11
+ - `hq bot status` on an HQ Fleet Runtime bot shows whether its gateway is installed as a login
12
+ service instead of a "launchd: not installed" line that was about the legacy bot daemon.
13
+ - `hq bot create --no-auto-approve` now reaches an HQ Fleet Runtime bot's profile (`autoApprove:
14
+ false`); with hq-fleet-local 0.4.18 the default profile runs with command approvals off, like a
15
+ model-CLI bot, so a headless bot no longer stalls on an approval nobody can answer.
16
+ - HQ Fleet Runtime pin moves to hq-fleet-local 0.4.18, which stores the bot's
17
+ machine credential with Windows DPAPI under PowerShell 5.1 and no longer
18
+ flashes a console on Windows. The CLI now trusts two release signing keys
19
+ (the 2026-10-30 key and a second key added 2026-11-01); a manifest signed by
20
+ either verifies, and any other key is rejected.
21
+ - `hq bot create` / `hq bot start` (HQ Fleet Runtime): every helper, uv and
22
+ Python child process now always starts with `windowsHide`, which a caller's
23
+ options can no longer turn off, so no console window flashes on Windows.
24
+ A new test records every child process in a Windows create, start, stop and
25
+ status run and checks each one.
26
+
27
+ ## [5.345.190] — 2026-11-01
28
+ - `hq files purge <path> --version-id <id>` permanently deletes one old version of a vault file, after a confirmation prompt.
29
+ - `hq bot create` / `hq bot start` on Windows: the shared HQ Fleet Runtime now
30
+ installs under `%USERPROFILE%\.hq-fleet\runtimes\<16 hex>\{v,s\r,s\d}` instead
31
+ of the 64-character identity plus `sources\runtime`, so the runtime's deepest
32
+ files stay under the 260-character Windows path limit when long paths are
33
+ off. Before, extraction stopped partway with "HQ Fleet Local runtime
34
+ extraction failed." Runtimes already installed under the old layout keep
35
+ working, and a half-extracted old one is removed. macOS and Linux paths are
36
+ unchanged.
37
+ - HQ Fleet Local install steps now include the last lines of the failing
38
+ command's stderr in their error, and on Windows the CLI checks the runtime
39
+ archive's longest path before extracting and names the path limit and
40
+ `LongPathsEnabled` setting when it would not fit.
41
+ - The CLI now warns on stderr when the HQ Fleet Local release manifest's
42
+ `compatibility.minimum_hq_cli_version` is newer than the running CLI. It does
43
+ not block the install.
44
+ - `hq-hook`: the `block-credential-file-read` port follows the fixed bash guard
45
+ (hq-core-staging #1753, sha256 7c6a8cfa) and its pin moves to that version.
46
+ Zero-padded brace ranges are read in base 10 and long or stepped ranges
47
+ block; letter ranges expand exactly; the mktemp scan replacement is literal
48
+ (no `patsub_replacement`) with capped loops; `function name { ... }` bodies
49
+ are analysed; extglob groups are an unknown word part; and `ls`, `stat` and
50
+ the other metadata commands are not trusted after an alias, hash, enable,
51
+ PATH change, a write into a bin directory, or when named by a path outside
52
+ /bin, /usr/bin, /sbin and /usr/sbin. The port also picks up the guard's
53
+ cygpath and `upstream-pin.env` changes. The vendored fixture and corpus are
54
+ refreshed. The port flag stays off.
55
+ - `hq sync now`: when the push leg holds files after an account sign-in change
56
+ on this computer, the push line now shows the held count, prints how to
57
+ release them (`hq sync held`, `hq sync release-held`), skips the "Sync now
58
+ complete" line and exits 1. Before, it printed "0 uploaded, 0 skipped" and a
59
+ success line (feedback_2356e54f).
60
+
5
61
  ## [5.345.189] — 2026-11-01
6
62
  - A local bot that cannot reach HQ when it starts now retries the identity check up to five times
7
63
  (waits of 5 s, 15 s, 45 s and 2 min) on a network error (connection refused, reset, DNS failure,
@@ -3243,6 +3243,35 @@ export declare const COMMAND_CATALOG: readonly [{
3243
3243
  readonly hidden: false;
3244
3244
  }];
3245
3245
  readonly subcommands: readonly [];
3246
+ }, {
3247
+ readonly name: "purge";
3248
+ readonly description: "Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.";
3249
+ readonly aliases: readonly [];
3250
+ readonly hidden: false;
3251
+ readonly helpOption: true;
3252
+ readonly helpCommand: false;
3253
+ readonly usage: "[options] <path>";
3254
+ readonly helpText: readonly [];
3255
+ readonly arguments: readonly [{
3256
+ readonly name: "path";
3257
+ readonly required: true;
3258
+ readonly variadic: false;
3259
+ readonly description: "";
3260
+ }];
3261
+ readonly options: readonly [{
3262
+ readonly flags: "--personal";
3263
+ readonly description: "Target your own personal vault instead of a company vault (mutually exclusive with --company)";
3264
+ readonly hidden: false;
3265
+ }, {
3266
+ readonly flags: "--version-id <id>";
3267
+ readonly description: "Exact version ID to purge (see `hq files versions`)";
3268
+ readonly hidden: false;
3269
+ }, {
3270
+ readonly flags: "-y, --yes";
3271
+ readonly description: "Skip the confirmation prompt (for scripts)";
3272
+ readonly hidden: false;
3273
+ }];
3274
+ readonly subcommands: readonly [];
3246
3275
  }, {
3247
3276
  readonly name: "trash";
3248
3277
  readonly description: "List deleted vault keys retained as tombstones. Use --personal for your personal vault.";
@@ -3912,6 +3912,42 @@ export const COMMAND_CATALOG = [
3912
3912
  ],
3913
3913
  "subcommands": []
3914
3914
  },
3915
+ {
3916
+ "name": "purge",
3917
+ "description": "Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.",
3918
+ "aliases": [],
3919
+ "hidden": false,
3920
+ "helpOption": true,
3921
+ "helpCommand": false,
3922
+ "usage": "[options] <path>",
3923
+ "helpText": [],
3924
+ "arguments": [
3925
+ {
3926
+ "name": "path",
3927
+ "required": true,
3928
+ "variadic": false,
3929
+ "description": ""
3930
+ }
3931
+ ],
3932
+ "options": [
3933
+ {
3934
+ "flags": "--personal",
3935
+ "description": "Target your own personal vault instead of a company vault (mutually exclusive with --company)",
3936
+ "hidden": false
3937
+ },
3938
+ {
3939
+ "flags": "--version-id <id>",
3940
+ "description": "Exact version ID to purge (see `hq files versions`)",
3941
+ "hidden": false
3942
+ },
3943
+ {
3944
+ "flags": "-y, --yes",
3945
+ "description": "Skip the confirmation prompt (for scripts)",
3946
+ "hidden": false
3947
+ }
3948
+ ],
3949
+ "subcommands": []
3950
+ },
3915
3951
  {
3916
3952
  "name": "trash",
3917
3953
  "description": "List deleted vault keys retained as tombstones. Use --personal for your personal vault.",
@@ -3,7 +3,7 @@ import { type RuntimeSignInCheck, type RuntimeSignInIssue } from "../lib/bot/run
3
3
  import type { BotStatusFile } from "../lib/bot/status.js";
4
4
  import { probeBotRuntime, type RuntimeProbeResult } from "../lib/bot/probe.js";
5
5
  import { type SpawnInteractive } from "../lib/bot/runtime-auth.js";
6
- import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type RuntimeCrash, type BotConfig, type BotRuntimeId, type LaunchctlExec, type BotRole } from "../lib/bot/index.js";
6
+ import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type RuntimeCrash, type BotConfig, type BotRuntimeId, type LaunchctlExec, HQ_FLEET_RUNTIME_ENGINE_ID, type BotRole } from "../lib/bot/index.js";
7
7
  /** Real launchctl; every failure is a non-ok result, never a throw. */
8
8
  export declare const defaultLaunchctl: LaunchctlExec;
9
9
  /** The desktop app creates HQ's setup bot under this name, always from the `setup` worker. */
@@ -13,6 +13,8 @@ export interface BotRow {
13
13
  agentUid: string;
14
14
  ownerUid: string;
15
15
  runtime: BotRuntimeId;
16
+ /** `hq-fleet-runtime` for a bot on the HQ Fleet Runtime; absent for a model-CLI bot. */
17
+ engine?: typeof HQ_FLEET_RUNTIME_ENGINE_ID;
16
18
  needsSignIn?: boolean;
17
19
  authCommand?: string;
18
20
  serviceInstallPending?: boolean;
@@ -57,7 +57,7 @@ import { runtimeDisplayName, signInCommand } from "../lib/bot/runtime-sign-in.js
57
57
  import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
58
58
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
59
59
  import { BOT_LOGIN_RUNTIMES, botAuthEnv, botRuntimeAuth, capacityAccountHome, describeBotAuth, formatBotAuth, isBotLoginRuntime, restoreBotSessions, setAsideBotSessions, signInBotRuntime, signOutBotRuntime, withRuntimeAuth, } from "../lib/bot/runtime-auth.js";
60
- 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, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetDefaultCompanies, fleetOwnerCompanies, fleetRuntimeIsDefault, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
60
+ 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, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetLocalServiceInstalled, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetDefaultCompanies, fleetOwnerCompanies, fleetRuntimeIsDefault, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
61
61
  function collectRepeated(value, previous) {
62
62
  return [...previous, value];
63
63
  }
@@ -152,6 +152,22 @@ function fleetBotSystemPrompt(name, config, overrides = {}) {
152
152
  function fleetAuthCommand(name, config) {
153
153
  return `hq bot auth ${name} --provider ${config.runtime === "grok" ? "grok" : config.runtime === "codex" ? "chatgpt" : "claude"}`;
154
154
  }
155
+ /** How long a just-(re)started HQ Fleet gateway gets to write its pid before it counts as not started. */
156
+ function fleetStartWaitMs() {
157
+ const raw = Number(process.env.HQ_BOT_FLEET_START_WAIT_MS);
158
+ return Number.isFinite(raw) && raw >= 0 ? raw : 15_000;
159
+ }
160
+ async function fleetGatewayComesUp(agentUid, waitMs, stepMs = 500) {
161
+ const deadline = Date.now() + waitMs;
162
+ for (;;) {
163
+ const pid = fleetLocalGatewayPid(agentUid);
164
+ if (pid !== null && isPidAlive(pid))
165
+ return true;
166
+ if (Date.now() >= deadline)
167
+ return false;
168
+ await new Promise((r) => setTimeout(r, stepMs));
169
+ }
170
+ }
155
171
  function runFleetRuntimeControl(name, config, action, json) {
156
172
  if ((action === "install" || action === "start" || action === "restart") && config.fleetAuthPending === true) {
157
173
  const authCommand = fleetAuthCommand(name, config);
@@ -236,6 +252,7 @@ function rowFor(name, opts = {}) {
236
252
  agentUid: config.agentUid,
237
253
  ownerUid: config.ownerUid,
238
254
  runtime: config.runtime,
255
+ ...(fleet ? { engine: HQ_FLEET_RUNTIME_ENGINE_ID } : {}),
239
256
  model: config.model,
240
257
  effort: effectiveBotEffort(config),
241
258
  effortIsDefault: !config.effort,
@@ -477,6 +494,7 @@ async function adoptFleetRuntimeProfile(name, dir, config) {
477
494
  systemPrompt: fleetBotSystemPrompt(name, config),
478
495
  provider: fleetProviderForRuntime(config.runtime, config.model),
479
496
  ownerCompanies: companyScope,
497
+ ...(config.autoApprove === false ? { autoApprove: false } : {}),
480
498
  }, { python, start: false });
481
499
  patchBotConfig(dir, { engine: installation.engine });
482
500
  const credentials = botCredsPath(dir);
@@ -1124,6 +1142,7 @@ async function runBotCreateLocked(name, opts) {
1124
1142
  systemPrompt: fleetBotSystemPrompt(name, config, { kind, companies }),
1125
1143
  provider: fleetProvider,
1126
1144
  ownerCompanies,
1145
+ ...(opts.autoApprove === false ? { autoApprove: false } : {}),
1127
1146
  firstStart: {
1128
1147
  intro: introDmText(name, runtime, intro, { kind, companies }),
1129
1148
  ...(kickoff ? { kickoff } : {}),
@@ -1452,7 +1471,12 @@ async function runBotStatus(nameArg, opts) {
1452
1471
  console.log(` inbox: last poll ${status.lastInboxPollAt}`);
1453
1472
  }
1454
1473
  console.log(` answering: ${row.busy ? chalk.green(`yes${row.busySince ? `, since ${row.busySince}` : ""}`) : chalk.gray("no")}`);
1455
- console.log(` launchd: ${row.daemonInstalled ? (row.daemonLoaded ? "installed, loaded" : "installed, not loaded") : "not installed"}`);
1474
+ if (fleet) {
1475
+ console.log(` service: ${fleetLocalServiceInstalled(config.agentUid) ? "installed (HQ Fleet gateway, starts at login)" : row.serviceInstallPending ? "not installed yet; installs on hq bot start" : "not installed"}`);
1476
+ }
1477
+ else {
1478
+ console.log(` launchd: ${row.daemonInstalled ? (row.daemonLoaded ? "installed, loaded" : "installed, not loaded") : "not installed"}`);
1479
+ }
1456
1480
  if (row.daemonNodeMissing) {
1457
1481
  console.log(` ${chalk.red(`node missing: ${row.daemonNodeMissing}`)} — launchd cannot start ${name}; fix with: hq bot start ${name}`);
1458
1482
  }
@@ -2150,8 +2174,7 @@ async function runBotAuth(nameArg, opts) {
2150
2174
  model: result.model,
2151
2175
  ...(b.config.fleetAuthPending === true ? { fleetAuthPending: false, fleetServiceInstallPending: !result.restarted, enabled: result.restarted } : {}),
2152
2176
  });
2153
- const pid = fleetLocalGatewayPid(b.config.agentUid);
2154
- const running = pid !== null && isPidAlive(pid);
2177
+ const running = await fleetGatewayComesUp(b.config.agentUid, result.restarted ? fleetStartWaitMs() : 0);
2155
2178
  if (opts.json) {
2156
2179
  console.log(JSON.stringify({ ok: true, name: b.name, runtime: HQ_FLEET_RUNTIME_ENGINE_ID, ...result, needsSignIn: false, running, serviceInstallPending: b.config.fleetAuthPending === true && !result.restarted }, null, 2));
2157
2180
  return;
@@ -2446,6 +2469,7 @@ async function adoptRemoteBotLocked(input) {
2446
2469
  systemPrompt: fleetBotSystemPrompt(name, config),
2447
2470
  provider: fleetProviderForRuntime(config.runtime, config.model),
2448
2471
  ownerCompanies: await fleetOwnerCompanies(),
2472
+ ...(config.autoApprove === false ? { autoApprove: false } : {}),
2449
2473
  ...(input.preserveFirstStart
2450
2474
  ? {
2451
2475
  firstStart: {
@@ -2351,12 +2351,19 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2351
2351
  }
2352
2352
  const pushProgressCompany = personalMode ? null : (pushSlug ?? null);
2353
2353
  const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(pushProgressCompany));
2354
+ // feedback_2356e54f: the push leg held files after an account switch and
2355
+ // still printed "0 uploaded, 0 skipped" and "Sync now complete".
2356
+ const pushIdentityHeld = createIdentityHeldTracker();
2357
+ const pushRecord = trackingProgressRecorder(planLimitTracker, pushProgressCompany);
2354
2358
  const pushResult = await share({
2355
2359
  paths: pushPaths,
2356
2360
  company: targetCompany,
2357
2361
  vaultConfig,
2358
2362
  hqRoot,
2359
- progressRecorder: trackingProgressRecorder(planLimitTracker, pushProgressCompany),
2363
+ progressRecorder: (event) => {
2364
+ pushIdentityHeld.observe(event);
2365
+ pushRecord(event);
2366
+ },
2360
2367
  skipUnchanged: true,
2361
2368
  serverOwnedPathPolicy: "skip",
2362
2369
  propagateDeletes: true,
@@ -2370,7 +2377,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2370
2377
  });
2371
2378
  const pushRefused = planLimitTracker.summary();
2372
2379
  const pushUploadFailed = readUploadFailures(pushResult);
2380
+ const pushHeldCount = pushIdentityHeld.count();
2373
2381
  const pushStatus = pushResult.aborted ||
2382
+ pushHeldCount > 0 ||
2374
2383
  pushResult.filesExcludedByScope > 0 ||
2375
2384
  (pushResult.filesRefusedStale ?? 0) > 0 ||
2376
2385
  pushUploadFailed.count > 0 ||
@@ -2380,6 +2389,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2380
2389
  console.log(` ${pushStatus} ` +
2381
2390
  `${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
2382
2391
  `${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
2392
+ (pushHeldCount > 0 ? `, ${pushHeldCount} held` : "") +
2383
2393
  (pushResult.aborted ? " — aborted" : "") +
2384
2394
  (pushRefused
2385
2395
  ? ` — ${describeRefusedCount(pushRefused)} refused (new files paused)`
@@ -2397,6 +2407,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2397
2407
  for (const line of uploadFailedLines(pushUploadFailed.count, pushUploadFailed.paths)) {
2398
2408
  console.log(chalk.yellow(line));
2399
2409
  }
2410
+ for (const line of identityHeldLines(pushHeldCount)) {
2411
+ console.log(chalk.yellow(line));
2412
+ }
2400
2413
  if (pushResult.aborted) {
2401
2414
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
2402
2415
  throw new SyncExitError(1);
@@ -2519,7 +2532,8 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2519
2532
  }
2520
2533
  if (pushResult.filesExcludedByScope > 0 ||
2521
2534
  (pushResult.filesRefusedStale ?? 0) > 0 ||
2522
- pushUploadFailed.count > 0) {
2535
+ pushUploadFailed.count > 0 ||
2536
+ pushHeldCount > 0) {
2523
2537
  console.log(chalk.yellow("\n⚠ Sync now finished, but one or more requested uploads were refused."));
2524
2538
  throw new SyncExitError(1, { health: "succeeded" });
2525
2539
  }
@@ -242,6 +242,24 @@ export declare function runFilesRestore(params: {
242
242
  }, deps?: {
243
243
  confirm?: ConfirmFn;
244
244
  }): Promise<FilesRestoreResponse | undefined>;
245
+ export interface FilesPurgeResponse {
246
+ key: string;
247
+ purgedVersionId: string;
248
+ }
249
+ /**
250
+ * feedback_fc711fc3: permanently delete one noncurrent version. The server
251
+ * (hq-pro POST /v1/files/restore, action "purge") enforces owner-only access,
252
+ * refuses the current version and delete markers, and audits before deleting.
253
+ */
254
+ export declare function runFilesPurge(params: {
255
+ key: string;
256
+ versionId: string;
257
+ yes: boolean;
258
+ personal: boolean;
259
+ companySlug: string | undefined;
260
+ }, deps?: {
261
+ confirm?: ConfirmFn;
262
+ }): Promise<FilesPurgeResponse | undefined>;
245
263
  export declare function runFilesTrash(params: {
246
264
  prefix: string;
247
265
  cursor?: string;
@@ -731,6 +731,30 @@ export function registerFilesCommand(program) {
731
731
  process.exit(1);
732
732
  }
733
733
  });
734
+ files
735
+ .command("purge <path>")
736
+ .description("Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.")
737
+ .option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
738
+ .requiredOption("--version-id <id>", "Exact version ID to purge (see `hq files versions`)")
739
+ .option("-y, --yes", "Skip the confirmation prompt (for scripts)")
740
+ .action(async (path, opts) => {
741
+ try {
742
+ const companySlug = files.opts().company;
743
+ const personal = opts.personal === true;
744
+ assertRecoveryScope(personal, companySlug);
745
+ await runFilesPurge({
746
+ key: path,
747
+ versionId: opts.versionId,
748
+ yes: opts.yes === true,
749
+ personal,
750
+ companySlug,
751
+ });
752
+ }
753
+ catch (err) {
754
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
755
+ process.exit(1);
756
+ }
757
+ });
734
758
  files
735
759
  .command("trash")
736
760
  .description("List deleted vault keys retained as tombstones. Use --personal for your personal vault.")
@@ -1767,6 +1791,55 @@ export async function runFilesRestore(params, deps = {}) {
1767
1791
  console.log(chalk.dim("New version: " + data.newVersionId));
1768
1792
  return data;
1769
1793
  }
1794
+ /**
1795
+ * feedback_fc711fc3: permanently delete one noncurrent version. The server
1796
+ * (hq-pro POST /v1/files/restore, action "purge") enforces owner-only access,
1797
+ * refuses the current version and delete markers, and audits before deleting.
1798
+ */
1799
+ export async function runFilesPurge(params, deps = {}) {
1800
+ assertExactRecoveryKey(params.key);
1801
+ if (typeof params.versionId !== "string" || params.versionId.trim() === "") {
1802
+ throw new Error("--version-id is required and must not be empty.");
1803
+ }
1804
+ const confirm = deps.confirm ?? realConfirm;
1805
+ if (!params.yes) {
1806
+ const ok = await confirm("Permanently delete version '" + params.versionId + "' of '" + params.key + "'" +
1807
+ (params.personal ? " in your personal vault" : "") +
1808
+ "? This cannot be undone.");
1809
+ if (!ok) {
1810
+ console.log(chalk.dim("Aborted — nothing was purged."));
1811
+ return undefined;
1812
+ }
1813
+ }
1814
+ const scope = await resolveRecoveryScope(params);
1815
+ const body = scope.personal
1816
+ ? { personal: true, key: params.key }
1817
+ : { company: scope.companyUid, key: params.key };
1818
+ body.action = "purge";
1819
+ body.purgeVersionId = params.versionId;
1820
+ // Tripwire: a server without purge support rejects `versionId: null` with
1821
+ // 400 instead of ignoring `action` and restoring (re-publishing) the version.
1822
+ body.versionId = null;
1823
+ const res = await vaultApiFetch({
1824
+ token: scope.token,
1825
+ path: "/v1/files/restore",
1826
+ method: "POST",
1827
+ body,
1828
+ });
1829
+ if (!res.ok) {
1830
+ const errBody = (await res.json().catch(() => ({})));
1831
+ const err = new FilesRecoveryHttpError(res.status, errBody.message ?? errBody.error ?? res.statusText, errBody.code);
1832
+ throw new Error(formatFilesRecoveryError(err, params.key));
1833
+ }
1834
+ const data = (await res.json());
1835
+ // Belt and braces with the tripwire above: only a server that echoes the
1836
+ // purged version id has purged it.
1837
+ if (data.purgedVersionId !== params.versionId) {
1838
+ throw new Error("The server did not confirm the purge (it may not support purge yet). Check `hq files versions` before retrying.");
1839
+ }
1840
+ console.log(chalk.green("Purged version '" + data.purgedVersionId + "' of '" + params.key + "'."));
1841
+ return { key: data.key ?? params.key, purgedVersionId: data.purgedVersionId };
1842
+ }
1770
1843
  export async function runFilesTrash(params) {
1771
1844
  if (params.prefix.includes("*")) {
1772
1845
  throw new Error("Trash prefix must be literal; wildcard expansion is not supported.");
@@ -41,6 +41,7 @@ export interface FleetLocalProfile {
41
41
  transport?: string;
42
42
  };
43
43
  ownerCompanies?: string[];
44
+ autoApprove?: boolean;
44
45
  hqRoot: string;
45
46
  profileDir: string;
46
47
  runtime: {
@@ -60,7 +61,7 @@ export interface FleetLocalInstallDeps {
60
61
  hqReleaseLinks?: (release: FleetLocalReleasePin) => Promise<Map<string, string> | null>;
61
62
  now?: () => Date;
62
63
  release?: FleetLocalReleasePin;
63
- publicKey?: string;
64
+ publicKey?: string | readonly string[];
64
65
  }
65
66
  export interface FleetLocalProfileInput {
66
67
  agentUid: string;
@@ -79,6 +80,8 @@ export interface FleetLocalProfileInput {
79
80
  };
80
81
  /** Companies the owner's own turns may use; other senders get none of them. */
81
82
  ownerCompanies?: string[];
83
+ /** False keeps the runtime's command approval prompts (`hq bot create --no-auto-approve`). */
84
+ autoApprove?: boolean;
82
85
  }
83
86
  /** The model the HQ Fleet Runtime asks for when a Claude bot names none. */
84
87
  export declare const FLEET_CLAUDE_DEFAULT_MODEL = "claude-opus-5-5";
@@ -99,8 +102,34 @@ export interface FleetLocalBootstrapDeps {
99
102
  start?: boolean;
100
103
  python?: FleetPython;
101
104
  }
105
+ interface SharedFleetRuntime {
106
+ id: string;
107
+ root: string;
108
+ runtimeDir: string;
109
+ sourcesDir: string;
110
+ /** Extracted runtime source tree (`sources/runtime`, or `s/r` on Windows). */
111
+ runtimeSource: string;
112
+ /** Extracted distribution tree (`sources/distribution`, or `s/d` on Windows). */
113
+ distributionSource: string;
114
+ }
115
+ /**
116
+ * Hex characters of the runtime identity used for the shared runtime
117
+ * directory name on Windows. The full 64-character identity plus
118
+ * `sources\runtime` pushed the runtime's deepest files past MAX_PATH (260).
119
+ */
120
+ export declare const WINDOWS_RUNTIME_DIR_ID_LENGTH = 16;
121
+ /** Longest path Windows accepts when long paths are turned off (MAX_PATH minus the terminator). */
122
+ export declare const WINDOWS_MAX_PATH_CHARS = 259;
123
+ /**
124
+ * Where a shared runtime lives. macOS and Linux keep the original layout
125
+ * (`runtimes/<64 hex>/{runtime,sources/runtime,sources/distribution}`).
126
+ * Windows uses `runtimes/<16 hex>/{v,s\r,s\d}` so deep runtime files stay
127
+ * under MAX_PATH; `ready.json` records the full identity.
128
+ */
129
+ export declare function sharedRuntimeLayout(root: string, id: string, platform: NodeJS.Platform): SharedFleetRuntime;
102
130
  type FleetLocalProfileDeps = Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform"> & {
103
131
  runtimeRoot?: string;
132
+ runtime?: SharedFleetRuntime;
104
133
  };
105
134
  export interface FleetLocalBootstrapResult {
106
135
  profile: string;
@@ -136,9 +165,22 @@ export interface FleetLocalRemovalResult {
136
165
  credentialsRemoved: number;
137
166
  }
138
167
  export declare const HQ_FLEET_LOCAL_RELEASE: FleetLocalReleasePin;
168
+ /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
139
169
  export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEASJoapvo0WMTg+Wu3Jvt0nzlXLOBEr+G1aJRi0JkEb4A=\n-----END PUBLIC KEY-----\n";
170
+ /** hq-fleet-local trust/release-public-v2.pem (added 2026-11-01; signs v0.4.18). */
171
+ export declare const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=\n-----END PUBLIC KEY-----\n";
172
+ /** Every key a release manifest may be signed with. Any one must verify it. */
173
+ export declare const HQ_FLEET_LOCAL_PUBLIC_KEYS: readonly string[];
174
+ /** True when any trusted Ed25519 key verifies the manifest signature. */
175
+ export declare function fleetLocalManifestSignatureValid(manifest: Buffer, signature: Buffer, publicKey?: string | readonly string[]): boolean;
140
176
  export declare function fleetHome(home?: string, env?: NodeJS.ProcessEnv): string;
141
- export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string): void;
177
+ export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string | readonly string[]): void;
178
+ /**
179
+ * Warning text when the signed manifest asks for a newer hq CLI than this
180
+ * one, or null. Advisory only: the pinned runtime is still installed, since
181
+ * blocking would strand every user until that CLI version is published.
182
+ */
183
+ export declare function fleetLocalCliCompatibilityWarning(manifest: Buffer, release?: FleetLocalReleasePin, cliVersion?: string): string | null;
142
184
  /** `gh auth token` fallback for the release download token. */
143
185
  export declare const FLEET_RELEASE_TOKEN_TIMEOUT_MS = 15000;
144
186
  export declare function resolveFleetReleaseToken(env?: NodeJS.ProcessEnv, run?: FleetLocalExec): string;
@@ -149,6 +191,15 @@ export declare const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS: number;
149
191
  export declare function installPinnedFleetLocalDistribution(deps?: FleetLocalInstallDeps): Promise<FleetLocalInstallation>;
150
192
  /** The process id the bot's gateway recorded, when its file reads; the caller checks whether it is alive. */
151
193
  export declare function fleetLocalGatewayPid(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome">): number | null;
194
+ /**
195
+ * Whether the bot's HQ Fleet gateway is installed as a login service. The
196
+ * runtime registers it with launchd as `ai.hq-fleet.gateway-<profile>`; null
197
+ * on platforms where hq-cli does not know the service layout.
198
+ */
199
+ export declare function fleetLocalServiceInstalled(agentUid: string, deps?: {
200
+ platform?: NodeJS.Platform;
201
+ home?: string;
202
+ }): boolean | null;
152
203
  export declare function fleetLocalProfileNeedsAdoption(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome" | "platform"> & {
153
204
  release?: FleetLocalReleasePin;
154
205
  }): boolean;
@@ -172,6 +223,14 @@ export declare function writeFleetLocalSoul(agentUid: string, systemPrompt: stri
172
223
  export declare function scanFleetLocalInstructions(agentUid: string, text: string, deps?: FleetLocalControlDeps): string[];
173
224
  /** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
174
225
  export declare function refreshFleetLocalInstructions(agentUid: string, systemPrompt: string, deps?: FleetLocalControlDeps): string;
226
+ /** Last few non-empty stderr lines of a failed child process, trimmed for an error message. */
227
+ export declare function stderrTail(error: unknown): string;
228
+ /**
229
+ * Message for a Windows extraction that cannot fit under MAX_PATH, or null
230
+ * when it fits or long paths are on. `longest` is the longest member name in
231
+ * the archive; members land at `<destination>\<name>`.
232
+ */
233
+ export declare function windowsExtractionPathProblem(destination: string, longest: number, longPathsEnabled: number | null): string | null;
175
234
  /** Moves a fresh machine credential into the bot's secure store and deletes the file. */
176
235
  export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps): void;
177
236
  export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
@@ -6,6 +6,8 @@ import * as path from "node:path";
6
6
  import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
7
7
  import { withBotLock } from "./lock.js";
8
8
  import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
9
+ import semver from "semver";
10
+ import { CLI_VERSION } from "../../cli-version.js";
9
11
  import { ensureCognitoToken } from "../../utils/cognito-session.js";
10
12
  import { vaultApiFetch } from "../../utils/vault-api.js";
11
13
  /** The model the HQ Fleet Runtime asks for when a Claude bot names none. */
@@ -34,22 +36,84 @@ export function fleetProviderForRuntime(runtime, model) {
34
36
  export function fleetProviderNeedsSignIn(provider) {
35
37
  return provider.id !== "anthropic";
36
38
  }
39
+ /**
40
+ * Hex characters of the runtime identity used for the shared runtime
41
+ * directory name on Windows. The full 64-character identity plus
42
+ * `sources\runtime` pushed the runtime's deepest files past MAX_PATH (260).
43
+ */
44
+ export const WINDOWS_RUNTIME_DIR_ID_LENGTH = 16;
45
+ /** Longest path Windows accepts when long paths are turned off (MAX_PATH minus the terminator). */
46
+ export const WINDOWS_MAX_PATH_CHARS = 259;
47
+ function legacySharedRuntimeLayout(root, id) {
48
+ const shared = path.join(root, "runtimes", id);
49
+ const sources = path.join(shared, "sources");
50
+ return {
51
+ id,
52
+ root: shared,
53
+ runtimeDir: path.join(shared, "runtime"),
54
+ sourcesDir: sources,
55
+ runtimeSource: path.join(sources, "runtime"),
56
+ distributionSource: path.join(sources, "distribution"),
57
+ };
58
+ }
59
+ /**
60
+ * Where a shared runtime lives. macOS and Linux keep the original layout
61
+ * (`runtimes/<64 hex>/{runtime,sources/runtime,sources/distribution}`).
62
+ * Windows uses `runtimes/<16 hex>/{v,s\r,s\d}` so deep runtime files stay
63
+ * under MAX_PATH; `ready.json` records the full identity.
64
+ */
65
+ export function sharedRuntimeLayout(root, id, platform) {
66
+ if (platform !== "win32")
67
+ return legacySharedRuntimeLayout(root, id);
68
+ const shared = path.join(root, "runtimes", id.slice(0, WINDOWS_RUNTIME_DIR_ID_LENGTH));
69
+ const sources = path.join(shared, "s");
70
+ return {
71
+ id,
72
+ root: shared,
73
+ runtimeDir: path.join(shared, "v"),
74
+ sourcesDir: sources,
75
+ runtimeSource: path.join(sources, "r"),
76
+ distributionSource: path.join(sources, "d"),
77
+ };
78
+ }
37
79
  export const HQ_FLEET_LOCAL_RELEASE = {
38
80
  repository: "indigoai-us/hq-fleet-local",
39
- tag: "v0.4.17",
40
- version: "0.4.17",
41
- artifact: "hq-fleet-local-0.4.17.zip",
42
- artifactSha256: "d2c5c49d4ef47da6003f4c2f3be7dcd2d8a287523c8577dfe0239e6e0705e4b4",
81
+ tag: "v0.4.18",
82
+ version: "0.4.18",
83
+ artifact: "hq-fleet-local-0.4.18.zip",
84
+ artifactSha256: "194ae7da24d02205061b18172a84c972754bd0607ed3b4c32af7ac10d73904a7",
43
85
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
44
86
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
45
- manifestSha256: "63663ffe8c6bef4f2aefa587d20431156cab4170a0f23533cdab7aab444a7d35",
46
- signatureSha256: "bb4af0b453ae03fdbc18e6335ca23e2f0de42bd9e322dba7c4f31d0c8dc08aa7",
87
+ manifestSha256: "ce14449198a0f5d8558ecc617c19551b7a2a31dba5bd563501a93d2ee58e5ff0",
88
+ signatureSha256: "ca7dac6bcc2d12f6e304d1940e7b00243f9c306e7f0a72fd4aa236b7778d3a93",
47
89
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
48
90
  };
91
+ /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
49
92
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
50
93
  MCowBQYDK2VwAyEASJoapvo0WMTg+Wu3Jvt0nzlXLOBEr+G1aJRi0JkEb4A=
51
94
  -----END PUBLIC KEY-----
52
95
  `;
96
+ /** hq-fleet-local trust/release-public-v2.pem (added 2026-11-01; signs v0.4.18). */
97
+ export const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = `-----BEGIN PUBLIC KEY-----
98
+ MCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=
99
+ -----END PUBLIC KEY-----
100
+ `;
101
+ /** Every key a release manifest may be signed with. Any one must verify it. */
102
+ export const HQ_FLEET_LOCAL_PUBLIC_KEYS = [HQ_FLEET_LOCAL_PUBLIC_KEY, HQ_FLEET_LOCAL_PUBLIC_KEY_V2];
103
+ function publicKeyList(publicKey) {
104
+ return typeof publicKey === "string" ? [publicKey] : publicKey;
105
+ }
106
+ /** True when any trusted Ed25519 key verifies the manifest signature. */
107
+ export function fleetLocalManifestSignatureValid(manifest, signature, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
108
+ return publicKeyList(publicKey).some((key) => {
109
+ try {
110
+ return crypto.verify(null, manifest, crypto.createPublicKey(key), signature);
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ });
116
+ }
53
117
  function digest(value) {
54
118
  return crypto.createHash("sha256").update(value).digest("hex");
55
119
  }
@@ -68,7 +132,7 @@ function engineMetadata(release) {
68
132
  export function fleetHome(home = os.homedir(), env = process.env) {
69
133
  return env.HQ_FLEET_HOME?.trim() || path.join(home, ".hq-fleet");
70
134
  }
71
- export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEY) {
135
+ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
72
136
  if (digest(files.archive) !== release.artifactSha256) {
73
137
  throw expectedError(`HQ Fleet Local ${release.version} archive hash did not match the release pin.`);
74
138
  }
@@ -100,11 +164,34 @@ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE,
100
164
  if (signature.algorithm !== "ed25519" || typeof signature.signature !== "string") {
101
165
  throw expectedError(`HQ Fleet Local ${release.version} signature record is invalid.`);
102
166
  }
103
- const valid = crypto.verify(null, files.manifest, crypto.createPublicKey(publicKey), Buffer.from(signature.signature, "base64"));
167
+ const valid = fleetLocalManifestSignatureValid(files.manifest, Buffer.from(signature.signature, "base64"), publicKey);
104
168
  if (!valid) {
105
169
  throw expectedError(`HQ Fleet Local ${release.version} manifest signature is invalid.`);
106
170
  }
107
171
  }
172
+ /**
173
+ * Warning text when the signed manifest asks for a newer hq CLI than this
174
+ * one, or null. Advisory only: the pinned runtime is still installed, since
175
+ * blocking would strand every user until that CLI version is published.
176
+ */
177
+ export function fleetLocalCliCompatibilityWarning(manifest, release = HQ_FLEET_LOCAL_RELEASE, cliVersion = CLI_VERSION) {
178
+ let minimum;
179
+ try {
180
+ const parsed = JSON.parse(manifest.toString("utf8"));
181
+ minimum = parsed.compatibility?.minimum_hq_cli_version;
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ if (typeof minimum !== "string")
187
+ return null;
188
+ const required = semver.valid(semver.coerce(minimum));
189
+ const current = semver.valid(semver.coerce(cliVersion));
190
+ if (!required || !current || semver.gte(current, required))
191
+ return null;
192
+ return `HQ Fleet Local ${release.version} asks for hq CLI ${minimum} or newer; this is ${cliVersion}. ` +
193
+ "Update with `npm install -g @indigoai-us/hq-cli` if the bot misbehaves.";
194
+ }
108
195
  /** `gh auth token` fallback for the release download token. */
109
196
  export const FLEET_RELEASE_TOKEN_TIMEOUT_MS = 15_000;
110
197
  export function resolveFleetReleaseToken(env = process.env, run = execFileSync) {
@@ -261,7 +348,7 @@ function installFiles(target, files, release, publicKey) {
261
348
  fs.writeFileSync(path.join(staging, release.runtimeArtifact), files.runtimeArchive, { mode: 0o644 });
262
349
  fs.writeFileSync(path.join(staging, "release-manifest.json"), files.manifest, { mode: 0o644 });
263
350
  fs.writeFileSync(path.join(staging, "release-manifest.sig"), files.signature, { mode: 0o644 });
264
- fs.writeFileSync(path.join(staging, "release-public.pem"), publicKey, { mode: 0o644 });
351
+ fs.writeFileSync(path.join(staging, "release-public.pem"), publicKeyList(publicKey).join(""), { mode: 0o644 });
265
352
  fs.writeFileSync(path.join(staging, "install.json"), `${JSON.stringify({ schemaVersion: 1, release, engine: engineMetadata(release) }, null, 2)}\n`, { mode: 0o644 });
266
353
  if (fs.existsSync(target))
267
354
  fs.renameSync(target, backup);
@@ -301,12 +388,18 @@ function cachedRuntimeArchive(root, release) {
301
388
  }
302
389
  export async function installPinnedFleetLocalDistribution(deps = {}) {
303
390
  const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
304
- const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEY;
391
+ const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEYS;
305
392
  const root = deps.fleetHome ?? fleetHome();
306
393
  const dir = path.join(root, "distributions", "hq-fleet-local", release.version);
307
394
  const current = installedFiles(dir, release);
395
+ const warnCompatibility = (manifest) => {
396
+ const warning = fleetLocalCliCompatibilityWarning(manifest, release);
397
+ if (warning)
398
+ process.stderr.write(`Warning: ${warning}\n`);
399
+ };
308
400
  if (current) {
309
401
  verifyFleetLocalRelease(current, release, publicKey);
402
+ warnCompatibility(current.manifest);
310
403
  }
311
404
  else {
312
405
  const fetcher = deps.fetch ?? fetch;
@@ -316,6 +409,7 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
316
409
  ? await downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime)
317
410
  : await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)(), cachedRuntime);
318
411
  verifyFleetLocalRelease(files, release, publicKey);
412
+ warnCompatibility(files.manifest);
319
413
  installFiles(dir, files, release, publicKey);
320
414
  }
321
415
  return {
@@ -377,18 +471,19 @@ function venvExecutable(runtimeDir, name, platform) {
377
471
  ? path.join(runtimeDir, "Scripts", `${name}.exe`)
378
472
  : path.join(runtimeDir, "bin", name);
379
473
  }
380
- function sharedRuntimeFromMarker(profileDir, root) {
474
+ function sharedRuntimeFromMarker(profileDir, root, platform = process.platform) {
381
475
  try {
382
476
  const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
383
477
  if (marker.schemaVersion !== 2 || typeof marker.runtimeId !== "string" || !/^[a-f0-9]{64}$/.test(marker.runtimeId))
384
478
  return null;
385
- const sharedRoot = path.join(root, "runtimes", marker.runtimeId);
386
- return {
387
- id: marker.runtimeId,
388
- root: sharedRoot,
389
- runtimeDir: path.join(sharedRoot, "runtime"),
390
- sourcesDir: path.join(sharedRoot, "sources"),
391
- };
479
+ const layout = sharedRuntimeLayout(root, marker.runtimeId, platform);
480
+ if (platform === "win32" && !fs.existsSync(path.join(layout.root, "ready.json"))) {
481
+ // Bots installed before the short Windows layout keep using their runtime.
482
+ const legacy = legacySharedRuntimeLayout(root, marker.runtimeId);
483
+ if (fs.existsSync(path.join(legacy.root, "ready.json")))
484
+ return legacy;
485
+ }
486
+ return layout;
392
487
  }
393
488
  catch {
394
489
  return null;
@@ -426,7 +521,7 @@ function fleetLocalExecutables(agentUid, deps) {
426
521
  const root = deps.fleetHome ?? fleetHome();
427
522
  const platform = deps.platform ?? process.platform;
428
523
  const profileDir = profileRoot(agentUid, root);
429
- const shared = sharedRuntimeFromMarker(profileDir, root);
524
+ const shared = sharedRuntimeFromMarker(profileDir, root, platform);
430
525
  const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
431
526
  const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
432
527
  const profile = path.join(profileDir, "profile.json");
@@ -440,7 +535,8 @@ function fleetLocalExecutables(agentUid, deps) {
440
535
  function runJson(run, executable, args, options, label) {
441
536
  let output;
442
537
  try {
443
- output = run(executable, args, { windowsHide: true, encoding: "utf8", ...options });
538
+ // windowsHide last: a helper, uv or Python child never opens a console window on Windows.
539
+ output = run(executable, args, { encoding: "utf8", ...options, windowsHide: true });
444
540
  }
445
541
  catch {
446
542
  throw expectedError(`HQ Fleet Local ${label} failed.`);
@@ -468,6 +564,31 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
468
564
  return null;
469
565
  }
470
566
  }
567
+ /** The first hq-fleet-local release whose profile schema accepts `autoApprove`; older ones reject unknown fields. */
568
+ const AUTO_APPROVE_RELEASE = [0, 4, 18];
569
+ function releaseKnowsAutoApprove(release) {
570
+ const parts = release.version.split(".").map((part) => Number.parseInt(part, 10));
571
+ for (let i = 0; i < AUTO_APPROVE_RELEASE.length; i += 1) {
572
+ const have = Number.isFinite(parts[i]) ? parts[i] : 0;
573
+ if (have !== AUTO_APPROVE_RELEASE[i])
574
+ return have > AUTO_APPROVE_RELEASE[i];
575
+ }
576
+ return true;
577
+ }
578
+ /**
579
+ * Whether the bot's HQ Fleet gateway is installed as a login service. The
580
+ * runtime registers it with launchd as `ai.hq-fleet.gateway-<profile>`; null
581
+ * on platforms where hq-cli does not know the service layout.
582
+ */
583
+ export function fleetLocalServiceInstalled(agentUid, deps = {}) {
584
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
585
+ return null;
586
+ const platform = deps.platform ?? process.platform;
587
+ if (platform !== "darwin")
588
+ return null;
589
+ const home = deps.home ?? os.homedir();
590
+ return fs.existsSync(path.join(home, "Library", "LaunchAgents", `ai.hq-fleet.gateway-${profileName(agentUid)}.plist`));
591
+ }
471
592
  export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
472
593
  if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
473
594
  return true;
@@ -484,12 +605,12 @@ export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
484
605
  if (!entries.includes(canonicalName))
485
606
  return true;
486
607
  const profileDir = path.join(profiles, canonicalName);
487
- const shared = sharedRuntimeFromMarker(profileDir, root);
608
+ const platform = deps.platform ?? process.platform;
609
+ const shared = sharedRuntimeFromMarker(profileDir, root, platform);
488
610
  if (!shared)
489
611
  return true;
490
612
  if (!profileEngineMatchesRelease(profileDir, deps.release ?? HQ_FLEET_LOCAL_RELEASE))
491
613
  return true;
492
- const platform = deps.platform ?? process.platform;
493
614
  if (!["hq-fleet", "hq-fleet-local", "python"].every((name) => fs.existsSync(venvExecutable(shared.runtimeDir, name, platform))))
494
615
  return true;
495
616
  if (hasEmbeddedRuntime(profileDir, platform) || fs.existsSync(path.join(profileDir, "sources")))
@@ -639,7 +760,7 @@ export function removeFleetLocalBot(agentUid, deps = {}) {
639
760
  }
640
761
  const platform = deps.platform ?? process.platform;
641
762
  const legacyRuntime = path.join(profileDir, "runtime");
642
- const shared = sharedRuntimeFromMarker(profileDir, deps.fleetHome ?? fleetHome());
763
+ const shared = sharedRuntimeFromMarker(profileDir, deps.fleetHome ?? fleetHome(), platform);
643
764
  const installedRuntime = shared?.runtimeDir ?? legacyRuntime;
644
765
  if (!["hq-fleet", "hq-fleet-local"].every((name) => fs.existsSync(venvExecutable(installedRuntime, name, platform)))) {
645
766
  fs.rmSync(profileDir, { recursive: true, force: true });
@@ -757,9 +878,11 @@ export function writeFleetLocalProfile(input, deps = {}) {
757
878
  }
758
879
  const root = deps.fleetHome ?? fleetHome();
759
880
  const dir = profileRoot(input.agentUid, root);
760
- const markerRuntime = sharedRuntimeFromMarker(dir, root);
761
- const runtimeRoot = deps.runtimeRoot ?? markerRuntime?.root ?? dir;
762
- const runtimeDir = path.join(runtimeRoot, "runtime");
881
+ const layout = deps.runtime ??
882
+ (deps.runtimeRoot ? undefined : sharedRuntimeFromMarker(dir, root, deps.platform ?? process.platform) ?? undefined);
883
+ const runtimeRoot = deps.runtimeRoot ?? dir;
884
+ const runtimeDir = layout?.runtimeDir ?? path.join(runtimeRoot, "runtime");
885
+ const distributionSource = layout?.distributionSource ?? path.join(runtimeRoot, "sources", "distribution");
763
886
  const destination = path.join(dir, "profile.json");
764
887
  const temporary = path.join(dir, `.profile-${process.pid}-${crypto.randomUUID()}`);
765
888
  const profile = {
@@ -767,12 +890,13 @@ export function writeFleetLocalProfile(input, deps = {}) {
767
890
  botId: input.agentUid,
768
891
  ownerUid: input.ownerUid,
769
892
  ...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
893
+ ...(input.autoApprove === false && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: false } : {}),
770
894
  provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
771
895
  hqRoot: path.resolve(input.hqRoot),
772
896
  profileDir: path.resolve(dir),
773
897
  runtime: { commit: input.installation.release.runtimeCommit, path: path.resolve(runtimeDir) },
774
898
  paths: {
775
- pluginSource: path.resolve(path.join(runtimeRoot, "sources", "distribution", "src", "hq_fleet_local", "plugins", "hqdm")),
899
+ pluginSource: path.resolve(path.join(distributionSource, "src", "hq_fleet_local", "plugins", "hqdm")),
776
900
  statusFile: path.resolve(path.join(input.botDir, "status.json")),
777
901
  logsDir: path.resolve(path.join(input.botDir, "logs")),
778
902
  },
@@ -833,11 +957,11 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
833
957
  const run = deps.run ?? execFileSync;
834
958
  const root = deps.fleetHome ?? fleetHome();
835
959
  const profileDir = profileRoot(agentUid, root);
836
- const shared = sharedRuntimeFromMarker(profileDir, root);
960
+ const platform = deps.platform ?? process.platform;
961
+ const shared = sharedRuntimeFromMarker(profileDir, root, platform);
837
962
  const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
838
- const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
839
- const python = venvExecutable(runtimeDir, "python", deps.platform ?? process.platform);
840
- const source = path.join(sourcesDir, "runtime");
963
+ const python = venvExecutable(runtimeDir, "python", platform);
964
+ const source = shared?.runtimeSource ?? path.join(profileDir, "sources", "runtime");
841
965
  if (!fs.existsSync(python) || !fs.existsSync(source)) {
842
966
  throw expectedError("HQ Fleet Local is not installed for this bot, so its instructions could not be checked.");
843
967
  }
@@ -867,14 +991,83 @@ export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {})
867
991
  }
868
992
  return writeFleetLocalSoul(agentUid, systemPrompt, deps);
869
993
  }
994
+ const STDERR_TAIL_LINES = 6;
995
+ const STDERR_TAIL_CHARS = 800;
996
+ /** Last few non-empty stderr lines of a failed child process, trimmed for an error message. */
997
+ export function stderrTail(error) {
998
+ const raw = error?.stderr;
999
+ const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : typeof raw === "string" ? raw : "";
1000
+ const lines = text.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
1001
+ const tail = lines.slice(-STDERR_TAIL_LINES).join("\n");
1002
+ return tail.length > STDERR_TAIL_CHARS ? `...${tail.slice(-STDERR_TAIL_CHARS)}` : tail;
1003
+ }
870
1004
  function checkedRun(run, executable, args, options, label) {
1005
+ // Keep stdout quiet but capture stderr, so a failure names its cause
1006
+ // (for example a path-too-long error during extraction).
1007
+ const stdio = options.stdio === "ignore" ? ["ignore", "ignore", "pipe"] : options.stdio;
871
1008
  try {
872
- run(executable, args, { windowsHide: true, ...options });
1009
+ run(executable, args, {
1010
+ maxBuffer: 16 * 1024 * 1024,
1011
+ ...options,
1012
+ ...(stdio === undefined ? {} : { stdio }),
1013
+ // Last, so no caller can turn it off: no console window flashes on Windows.
1014
+ windowsHide: true,
1015
+ });
873
1016
  }
874
- catch {
875
- throw expectedError(`HQ Fleet Local ${label} failed.`);
1017
+ catch (error) {
1018
+ const tail = stderrTail(error);
1019
+ throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
876
1020
  }
877
1021
  }
1022
+ const FLEET_TAR_PATH_PROBE = [
1023
+ "import json, sys, tarfile",
1024
+ "longest = 0",
1025
+ "with tarfile.open(sys.argv[1]) as archive:",
1026
+ " for member in archive:",
1027
+ " longest = max(longest, len(member.name.rstrip('/')))",
1028
+ "enabled = None",
1029
+ "try:",
1030
+ " import winreg",
1031
+ " key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SYSTEM\\CurrentControlSet\\Control\\FileSystem')",
1032
+ " enabled = int(winreg.QueryValueEx(key, 'LongPathsEnabled')[0])",
1033
+ "except Exception:",
1034
+ " pass",
1035
+ "print(json.dumps({'longest': longest, 'longPathsEnabled': enabled}))",
1036
+ ].join("\n");
1037
+ /**
1038
+ * Message for a Windows extraction that cannot fit under MAX_PATH, or null
1039
+ * when it fits or long paths are on. `longest` is the longest member name in
1040
+ * the archive; members land at `<destination>\<name>`.
1041
+ */
1042
+ export function windowsExtractionPathProblem(destination, longest, longPathsEnabled) {
1043
+ const needed = destination.length + 1 + longest;
1044
+ if (needed <= WINDOWS_MAX_PATH_CHARS || longPathsEnabled === 1)
1045
+ return null;
1046
+ return `The HQ Fleet Runtime cannot be extracted on this PC: its deepest file needs a ${needed}-character path under ${destination}, ` +
1047
+ `over the Windows limit of ${WINDOWS_MAX_PATH_CHARS}, and Windows long paths are turned off (LongPathsEnabled = ${longPathsEnabled ?? "unset"}). ` +
1048
+ "Ask an administrator to set HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem\\LongPathsEnabled to 1, or set HQ_FLEET_HOME to a shorter folder such as C:\\hqf, then retry.";
1049
+ }
1050
+ function assertWindowsExtractionFits(run, python, archive, destination) {
1051
+ let probe;
1052
+ try {
1053
+ const output = run(python, ["-I", "-c", FLEET_TAR_PATH_PROBE, archive], {
1054
+ windowsHide: true,
1055
+ encoding: "utf8",
1056
+ stdio: ["ignore", "pipe", "pipe"],
1057
+ maxBuffer: 1024 * 1024,
1058
+ });
1059
+ probe = JSON.parse((Buffer.isBuffer(output) ? output.toString("utf8") : output).trim());
1060
+ }
1061
+ catch {
1062
+ return; // The probe is advisory; extraction reports its own error.
1063
+ }
1064
+ if (typeof probe?.longest !== "number")
1065
+ return;
1066
+ const enabled = typeof probe.longPathsEnabled === "number" ? probe.longPathsEnabled : null;
1067
+ const problem = windowsExtractionPathProblem(destination, probe.longest, enabled);
1068
+ if (problem)
1069
+ throw expectedError(problem);
1070
+ }
878
1071
  /** Moves a fresh machine credential into the bot's secure store and deletes the file. */
879
1072
  export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}) {
880
1073
  const run = deps.run ?? execFileSync;
@@ -902,25 +1095,37 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
902
1095
  platform,
903
1096
  extras: ["mcp", "anthropic"],
904
1097
  })));
905
- const shared = path.join(root, "runtimes", identity);
906
- const descriptor = () => ({
907
- id: identity,
908
- root: shared,
909
- runtimeDir: path.join(shared, "runtime"),
910
- sourcesDir: path.join(shared, "sources"),
911
- });
1098
+ const layout = sharedRuntimeLayout(root, identity, platform);
1099
+ const shared = layout.root;
912
1100
  return withBotLock(identity, async () => {
913
1101
  const marker = path.join(shared, "ready.json");
914
- const sources = path.join(shared, "sources");
915
- const distributionSource = path.join(sources, "distribution");
916
- const runtimeSource = path.join(sources, "runtime");
917
- const runtimeDir = path.join(shared, "runtime");
1102
+ const { distributionSource, runtimeSource, runtimeDir } = layout;
918
1103
  const executables = ["python", "hq-fleet", "hq-fleet-local"];
1104
+ if (platform === "win32") {
1105
+ const legacy = legacySharedRuntimeLayout(root, identity);
1106
+ if (fs.existsSync(path.join(legacy.root, "ready.json")) &&
1107
+ executables.every((name) => fs.existsSync(venvExecutable(legacy.runtimeDir, name, platform)))) {
1108
+ // A runtime installed before the short Windows layout keeps serving bots.
1109
+ return legacy;
1110
+ }
1111
+ // A half-extracted long-path runtime is never usable; clear it.
1112
+ fs.rmSync(legacy.root, { recursive: true, force: true });
1113
+ }
919
1114
  if (fs.existsSync(marker)) {
1115
+ let recordedId;
1116
+ try {
1117
+ recordedId = JSON.parse(fs.readFileSync(marker, "utf8")).runtimeId;
1118
+ }
1119
+ catch {
1120
+ recordedId = undefined;
1121
+ }
1122
+ if (recordedId !== undefined && recordedId !== identity) {
1123
+ throw expectedError(`The shared HQ Fleet Runtime folder ${shared} belongs to a different runtime. Remove it and retry.`);
1124
+ }
920
1125
  if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
921
1126
  throw expectedError("The shared HQ Fleet Runtime is incomplete. Reinstall it before creating another bot.");
922
1127
  }
923
- return descriptor();
1128
+ return layout;
924
1129
  }
925
1130
  fs.rmSync(shared, { recursive: true, force: true });
926
1131
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
@@ -933,6 +1138,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
933
1138
  installation.archive,
934
1139
  distributionSource,
935
1140
  ], { stdio: "ignore" }, "distribution extraction");
1141
+ if (platform === "win32")
1142
+ assertWindowsExtractionFits(run, python.python, installation.runtimeArchive, runtimeSource);
936
1143
  checkedRun(run, python.python, [
937
1144
  "-m",
938
1145
  "tarfile",
@@ -961,8 +1168,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
961
1168
  if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
962
1169
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
963
1170
  }
964
- fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: installation.engine })}\n`, { mode: 0o600 });
965
- return descriptor();
1171
+ fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, runtimeId: identity, engine: installation.engine })}\n`, { mode: 0o600 });
1172
+ return layout;
966
1173
  }, { root: path.join(root, "runtimes"), command: "HQ Fleet Runtime installation", waitMs: 20 * 60_000, staleMs: 2 * 60 * 60_000 });
967
1174
  }
968
1175
  function writeNativeProfileMarker(profileDir, shared, engine) {
@@ -988,7 +1195,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
988
1195
  const { systemPrompt, machineCredentials, ...profileInput } = input;
989
1196
  if (systemPrompt !== undefined)
990
1197
  refreshFleetLocalInstructions(input.agentUid, systemPrompt, { fleetHome: root, platform, run });
991
- const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform, runtimeRoot: shared.root });
1198
+ const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform, runtime: shared });
992
1199
  const helper = venvExecutable(shared.runtimeDir, "hq-fleet-local", platform);
993
1200
  const runtime = venvExecutable(shared.runtimeDir, "hq-fleet", platform);
994
1201
  if (!fs.existsSync(helper) || !fs.existsSync(runtime)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.189",
3
+ "version": "5.345.191",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.189",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.189",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.189",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.189",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.189",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.189"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.191",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.191",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.191",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.191",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.191",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.191"
123
123
  }
124
124
  }