@indigoai-us/hq-cli 5.345.171 → 5.345.172

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 (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/command-catalog.generated.d.ts +74 -2
  3. package/dist/command-catalog.generated.js +88 -2
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/bot.js +46 -8
  7. package/dist/commands/cloud.js +9 -5
  8. package/dist/commands/daemon.js +16 -1
  9. package/dist/commands/files.d.ts +1 -0
  10. package/dist/commands/files.js +22 -4
  11. package/dist/commands/sync-core.d.ts +22 -0
  12. package/dist/commands/sync-core.js +124 -0
  13. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +278 -224
  14. package/dist/hq-cloud-sync-runner/package.json +1 -1
  15. package/dist/lib/bot/fleet-local.d.ts +16 -1
  16. package/dist/lib/bot/fleet-local.js +99 -48
  17. package/dist/lib/bot/fleet-python.d.ts +47 -0
  18. package/dist/lib/bot/fleet-python.js +218 -0
  19. package/dist/lib/bot/index.d.ts +1 -0
  20. package/dist/lib/bot/index.js +1 -0
  21. package/dist/lib/bot/prompt.d.ts +6 -1
  22. package/dist/lib/bot/prompt.js +8 -3
  23. package/dist/lib/daemon/catalog.d.ts +12 -4
  24. package/dist/lib/daemon/catalog.js +70 -27
  25. package/dist/lib/daemon/config.d.ts +0 -7
  26. package/dist/lib/daemon/config.js +0 -32
  27. package/dist/lib/daemon/run.d.ts +5 -1
  28. package/dist/lib/daemon/run.js +48 -11
  29. package/dist/lib/daemon/supervisor.js +3 -1
  30. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  31. package/dist/lib/sync-core/engine-home.js +193 -0
  32. package/dist/lib/sync-core/flag.d.ts +17 -0
  33. package/dist/lib/sync-core/flag.js +47 -0
  34. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  35. package/dist/lib/sync-core/reconcile.js +196 -0
  36. package/dist/lib/sync-core/release.d.ts +32 -0
  37. package/dist/lib/sync-core/release.js +140 -0
  38. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  39. package/dist/lib/sync-core/rollout-state.js +94 -0
  40. package/dist/lib/vault-sync-switches.d.ts +5 -0
  41. package/dist/lib/vault-sync-switches.js +43 -0
  42. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  43. package/dist/utils/remote-list-not-converging.js +40 -0
  44. package/package.json +8 -8
  45. package/dist/lib/daemon/sync-core-canary.d.ts +0 -12
  46. package/dist/lib/daemon/sync-core-canary.js +0 -61
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.172] — 2026-10-27
6
+ - The daemon now moves vaults to the new sync engine by itself, following a per-company switch (`sync.rust-engine-rollout`) instead of a hand-written `syncCoreCanary` entry in `~/.hq/daemon/config.json`. A new `sync-core` service on desktop and self-hosted machines runs `hq daemon service sync-core-reconcile` every 10 minutes: it reads the switch for every company this machine belongs to, downloads the pinned hq-syncd release (v0.1.13) from the public mirror and checks it against the release's SHA256SUMS, installs it at `~/.hq/bin/hq-syncd`, writes one engine home per switched vault under `~/.hq/sync-core/engines/<vault>/`, and records the result in `~/.hq/sync-core/rollout.json`. Each vault's engine first runs read-only alongside the current sync until it has built its index and planned a pass; the reconcile records that plan's upload, download, delete, server-row and local-file counts per vault (`hq sync-core status` shows them). A vault is then promoted to full mode and turned off in the current sync (`menubar.json`) only when the plan would upload or delete at most 100 paths, and the daemon runs its `sync-core:<vault>` unit in full mode only after the current sync has stopped touching that vault. Company vaults stay read-only in this release: the engine's server snapshot is filtered by the caller's visibility while the current sync pulled the whole tree, so a full-mode company engine would upload the difference; promotion waits for an engine release that scopes the local tree the same way. The personal vault moves when at least one of the person's companies is switched on. When a switch turns off, the daemon stops that vault's engine, turns the current sync back on for it, and leaves the engine's index on disk. A switch that cannot be read keeps the vault where it was. `hq sync-core reconcile` and `hq sync-core status` run the same logic by hand (agent boxes run `reconcile --companies-only`). The old `syncCoreCanary` config key is ignored.
7
+
8
+ - `hq files share 'clients/*/11_reports' --with <person>` now sends the path as typed instead of refusing it. The vault accepts one `*` standing for a whole folder name, so a single grant covers the `11_reports` folder under every client. A `*` inside a folder name (`acme*`), two wildcard folders, or a path made only of wildcards is still refused by the vault. Feedback 600e747f-e608-4266-8e1a-a9a9a4034828.
9
+ - A bot created with `--hq-fleet-runtime` now starts with HQ knowledge: its standing HQ instructions (identity, owner, HQ folder, worker definition, persona and memory rules) become the runtime persona, the HQ folder's skills are available to it, and the runtime's own lifecycle and system notices no longer reach the conversation. The CLI checks those instructions with the runtime's own scanner before saving them and refreshes them from the worker folder on every start and restart. `hq bot list` and `hq bot status` read a runtime bot's process from its gateway, and the HQ background service no longer starts a second, legacy copy of a runtime bot. Pins hq-fleet-local v0.4.5.
10
+ - Creating a bot with `--hq-fleet-runtime` no longer needs Python on the computer: the CLI installs a private,
11
+ pinned Python under the HQ fleet folder and builds the bot's runtime from it, on macOS, Linux and Windows.
12
+ - `hq-hook` 0.3.1: the bridge runs a hook port only for a bash script version
13
+ the port was checked against. It hashes the `--script` it is given, and any
14
+ other version of `policy-enforcement-gate.sh` runs bash alone (logged as
15
+ `script-mismatch`), so an hq-core change to the gate is never answered by
16
+ an older engine.
17
+ - The gate port now opens only the path column of the policy list, as bash
18
+ does, and never the excerpt text.
19
+ - On Windows the gate port goes to bash for:
20
+ - UNC, device and MSYS-form policy paths, and for root forms Win32 and MSYS
21
+ read differently;
22
+ - a state folder it cannot create;
23
+ - Windows device names used as session ids.
24
+ - The pass stamp records the bytes the decision read.
25
+ - The nightly `hq-hook` workflow now runs the gate parity test on every target,
26
+ using Git Bash on Windows.
27
+ - In `on` mode, a hook port that declines a payload makes `hq-hook bridge`
28
+ exit 75 with no output. master-hook then runs the bash hook itself, so the
29
+ engine no longer starts bash for those calls. On Windows this removes about
30
+ 200 to 330 ms from every gate call the port declines.
31
+ - Agent sign-in no longer hangs on a stale network connection (hq-cloud 6.18.103).
32
+ - When a company's sync stops because HQ could not finish listing the company's vault files (error code `REMOTE_LIST_NOT_CONVERGING` from hq-cloud), `hq sync now`, `hq sync push`, `hq sync pull` and their `--all` and `--personal` forms now print "HQ could not finish listing this company's files. Try hq sync now again in a minute; if it keeps happening, run hq feedback bug." with the original engine message on a following `Detail:` line, instead of only the raw engine message. `hq sync push --json` carries the same text in its `fatal` event. Menubar and background sync use the sync runner bundled from hq-cloud, which reports per-company errors as plain text without the code, so it still shows the engine message. Feedback e5eda195-9191-49b7-a73c-886f8139670b (DEV-5465).
33
+
5
34
  ## [5.345.171] — 2026-10-26
6
35
  - The daemon's sync-core unit now accepts a `~/.hq/sync-core/config.toml` that sets `full_vault = true` instead of `canary_path`, so the Rust engine can keep the whole personal vault in sync under the same supervision (ownership check, legacy personal sync paused first, health restarts). A config that sets both keys, or neither, keeps the unit blocked with a reason that names the fix. `hq daemon status` describes the unit as the personal vault rather than a canary, and the unit's environment carries `HQ_SYNC_FULL_VAULT=1`. Company vaults and machines without the sync-core setting are unchanged. Requires an hq-sync-core release that understands `full_vault` (after 0.1.6).
7
36
 
@@ -3051,7 +3051,7 @@ export declare const COMMAND_CATALOG: readonly [{
3051
3051
  }];
3052
3052
  }, {
3053
3053
  readonly name: "files";
3054
- readonly description: "Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children.";
3054
+ readonly description: "Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children, and a quoted 'clients/*/reports' shares that folder under every client.";
3055
3055
  readonly aliases: readonly [];
3056
3056
  readonly hidden: false;
3057
3057
  readonly helpOption: true;
@@ -3066,7 +3066,7 @@ export declare const COMMAND_CATALOG: readonly [{
3066
3066
  }];
3067
3067
  readonly subcommands: readonly [{
3068
3068
  readonly name: "share";
3069
- readonly description: "Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.";
3069
+ readonly description: "Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*', or a one-folder wildcard such as 'clients/*/reports', so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.";
3070
3070
  readonly aliases: readonly [];
3071
3071
  readonly hidden: false;
3072
3072
  readonly helpOption: true;
@@ -18341,6 +18341,22 @@ export declare const COMMAND_CATALOG: readonly [{
18341
18341
  readonly arguments: readonly [];
18342
18342
  readonly options: readonly [];
18343
18343
  readonly subcommands: readonly [];
18344
+ }, {
18345
+ readonly name: "sync-core-reconcile";
18346
+ readonly description: "Read each company's new-sync-engine switch and write this machine's engine homes and legacy off-switches";
18347
+ readonly aliases: readonly [];
18348
+ readonly hidden: false;
18349
+ readonly helpOption: true;
18350
+ readonly helpCommand: false;
18351
+ readonly usage: "[options]";
18352
+ readonly helpText: readonly [];
18353
+ readonly arguments: readonly [];
18354
+ readonly options: readonly [{
18355
+ readonly flags: "--hq-root <path>";
18356
+ readonly description: "HQ folder the engine works against";
18357
+ readonly hidden: false;
18358
+ }];
18359
+ readonly subcommands: readonly [];
18344
18360
  }, {
18345
18361
  readonly name: "remote-control";
18346
18362
  readonly description: "Keep `claude remote-control` running in a tmux session in the HQ folder";
@@ -18463,6 +18479,62 @@ export declare const COMMAND_CATALOG: readonly [{
18463
18479
  readonly subcommands: readonly [];
18464
18480
  }];
18465
18481
  }];
18482
+ }, {
18483
+ readonly name: "sync-core";
18484
+ readonly description: "Move this machine's vaults between the current sync and the new sync engine, following each company's switch";
18485
+ readonly aliases: readonly [];
18486
+ readonly hidden: true;
18487
+ readonly helpOption: true;
18488
+ readonly helpCommand: true;
18489
+ readonly usage: "[options] [command]";
18490
+ readonly helpText: readonly [];
18491
+ readonly arguments: readonly [];
18492
+ readonly options: readonly [];
18493
+ readonly subcommands: readonly [{
18494
+ readonly name: "reconcile";
18495
+ readonly description: "Read each company's switch, install the engine when needed, and write the engine homes and legacy off-switches";
18496
+ readonly aliases: readonly [];
18497
+ readonly hidden: false;
18498
+ readonly helpOption: true;
18499
+ readonly helpCommand: false;
18500
+ readonly usage: "[options]";
18501
+ readonly helpText: readonly [];
18502
+ readonly arguments: readonly [];
18503
+ readonly options: readonly [{
18504
+ readonly flags: "--hq-root <path>";
18505
+ readonly description: "HQ folder (default: the daemon's)";
18506
+ readonly hidden: false;
18507
+ }, {
18508
+ readonly flags: "--companies-only";
18509
+ readonly description: "Never move the personal vault (agent boxes)";
18510
+ readonly hidden: false;
18511
+ }, {
18512
+ readonly flags: "--token-file <path>";
18513
+ readonly description: "Token file the engine reads (default: this user's hq sign-in)";
18514
+ readonly hidden: false;
18515
+ }, {
18516
+ readonly flags: "--json";
18517
+ readonly description: "Print the written rollout state as JSON";
18518
+ readonly hidden: false;
18519
+ }];
18520
+ readonly subcommands: readonly [];
18521
+ }, {
18522
+ readonly name: "status";
18523
+ readonly description: "Show which vaults are on the new sync engine on this machine";
18524
+ readonly aliases: readonly [];
18525
+ readonly hidden: false;
18526
+ readonly helpOption: true;
18527
+ readonly helpCommand: false;
18528
+ readonly usage: "[options]";
18529
+ readonly helpText: readonly [];
18530
+ readonly arguments: readonly [];
18531
+ readonly options: readonly [{
18532
+ readonly flags: "--json";
18533
+ readonly description: "Print JSON";
18534
+ readonly hidden: false;
18535
+ }];
18536
+ readonly subcommands: readonly [];
18537
+ }];
18466
18538
  }, {
18467
18539
  readonly name: "bot";
18468
18540
  readonly description: "Personal local bots that run on this computer with your own model login";
@@ -3674,7 +3674,7 @@ export const COMMAND_CATALOG = [
3674
3674
  },
3675
3675
  {
3676
3676
  "name": "files",
3677
- "description": "Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children.",
3677
+ "description": "Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children, and a quoted 'clients/*/reports' shares that folder under every client.",
3678
3678
  "aliases": [],
3679
3679
  "hidden": false,
3680
3680
  "helpOption": true,
@@ -3692,7 +3692,7 @@ export const COMMAND_CATALOG = [
3692
3692
  "subcommands": [
3693
3693
  {
3694
3694
  "name": "share",
3695
- "description": "Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.",
3695
+ "description": "Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*', or a one-folder wildcard such as 'clients/*/reports', so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.",
3696
3696
  "aliases": [],
3697
3697
  "hidden": false,
3698
3698
  "helpOption": true,
@@ -22349,6 +22349,25 @@ export const COMMAND_CATALOG = [
22349
22349
  "options": [],
22350
22350
  "subcommands": []
22351
22351
  },
22352
+ {
22353
+ "name": "sync-core-reconcile",
22354
+ "description": "Read each company's new-sync-engine switch and write this machine's engine homes and legacy off-switches",
22355
+ "aliases": [],
22356
+ "hidden": false,
22357
+ "helpOption": true,
22358
+ "helpCommand": false,
22359
+ "usage": "[options]",
22360
+ "helpText": [],
22361
+ "arguments": [],
22362
+ "options": [
22363
+ {
22364
+ "flags": "--hq-root <path>",
22365
+ "description": "HQ folder the engine works against",
22366
+ "hidden": false
22367
+ }
22368
+ ],
22369
+ "subcommands": []
22370
+ },
22352
22371
  {
22353
22372
  "name": "remote-control",
22354
22373
  "description": "Keep `claude remote-control` running in a tmux session in the HQ folder",
@@ -22497,6 +22516,73 @@ export const COMMAND_CATALOG = [
22497
22516
  }
22498
22517
  ]
22499
22518
  },
22519
+ {
22520
+ "name": "sync-core",
22521
+ "description": "Move this machine's vaults between the current sync and the new sync engine, following each company's switch",
22522
+ "aliases": [],
22523
+ "hidden": true,
22524
+ "helpOption": true,
22525
+ "helpCommand": true,
22526
+ "usage": "[options] [command]",
22527
+ "helpText": [],
22528
+ "arguments": [],
22529
+ "options": [],
22530
+ "subcommands": [
22531
+ {
22532
+ "name": "reconcile",
22533
+ "description": "Read each company's switch, install the engine when needed, and write the engine homes and legacy off-switches",
22534
+ "aliases": [],
22535
+ "hidden": false,
22536
+ "helpOption": true,
22537
+ "helpCommand": false,
22538
+ "usage": "[options]",
22539
+ "helpText": [],
22540
+ "arguments": [],
22541
+ "options": [
22542
+ {
22543
+ "flags": "--hq-root <path>",
22544
+ "description": "HQ folder (default: the daemon's)",
22545
+ "hidden": false
22546
+ },
22547
+ {
22548
+ "flags": "--companies-only",
22549
+ "description": "Never move the personal vault (agent boxes)",
22550
+ "hidden": false
22551
+ },
22552
+ {
22553
+ "flags": "--token-file <path>",
22554
+ "description": "Token file the engine reads (default: this user's hq sign-in)",
22555
+ "hidden": false
22556
+ },
22557
+ {
22558
+ "flags": "--json",
22559
+ "description": "Print the written rollout state as JSON",
22560
+ "hidden": false
22561
+ }
22562
+ ],
22563
+ "subcommands": []
22564
+ },
22565
+ {
22566
+ "name": "status",
22567
+ "description": "Show which vaults are on the new sync engine on this machine",
22568
+ "aliases": [],
22569
+ "hidden": false,
22570
+ "helpOption": true,
22571
+ "helpCommand": false,
22572
+ "usage": "[options]",
22573
+ "helpText": [],
22574
+ "arguments": [],
22575
+ "options": [
22576
+ {
22577
+ "flags": "--json",
22578
+ "description": "Print JSON",
22579
+ "hidden": false
22580
+ }
22581
+ ],
22582
+ "subcommands": []
22583
+ }
22584
+ ]
22585
+ },
22500
22586
  {
22501
22587
  "name": "bot",
22502
22588
  "description": "Personal local bots that run on this computer with your own model login",
@@ -515,6 +515,13 @@ export declare const REGISTRATION_PLAN: readonly [{
515
515
  readonly module: "./commands/daemon.js";
516
516
  readonly exportName: "registerDaemonCommand";
517
517
  readonly hidden: true;
518
+ }, {
519
+ readonly type: "registrar";
520
+ readonly root: "sync-core";
521
+ readonly parent: "program";
522
+ readonly module: "./commands/sync-core.js";
523
+ readonly exportName: "registerSyncCoreCommand";
524
+ readonly hidden: true;
518
525
  }, {
519
526
  readonly type: "registrar";
520
527
  readonly root: "bot";
@@ -88,6 +88,7 @@ export const REGISTRATION_PLAN = [
88
88
  { type: "registrar", root: "lanes", parent: "program", module: "./commands/lanes.js", exportName: "registerLanesCommand" },
89
89
  { type: "registrar", root: "monitor", parent: "program", module: "./commands/monitor.js", exportName: "registerMonitorCommand" },
90
90
  { type: "registrar", root: "daemon", parent: "program", module: "./commands/daemon.js", exportName: "registerDaemonCommand", hidden: true },
91
+ { type: "registrar", root: "sync-core", parent: "program", module: "./commands/sync-core.js", exportName: "registerSyncCoreCommand", hidden: true },
91
92
  { type: "registrar", root: "bot", parent: "program", module: "./commands/bot.js", exportName: "registerBotCommand" },
92
93
  { type: "registrar", root: "report", parent: "program", module: "./commands/report.js", exportName: "registerReportCommand" },
93
94
  { type: "group", root: "deploy", id: "deploy", name: "deploy", description: "Manage deployments in hq-deploy" },
@@ -52,7 +52,7 @@ import { isAncestorProcess } from "../lib/bot/self-process.js";
52
52
  import { runtimeDisplayName } from "../lib/bot/runtime-sign-in.js";
53
53
  import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
54
54
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
55
- import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, authenticateFleetLocalBot, bootstrapFleetLocalBot, controlFleetLocalBot, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, removeFleetLocalBot, } from "../lib/bot/index.js";
55
+ import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
56
56
  function collectRepeated(value, previous) {
57
57
  return [...previous, value];
58
58
  }
@@ -117,7 +117,37 @@ function requireBot(name) {
117
117
  function isFleetRuntimeBot(config) {
118
118
  return config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID;
119
119
  }
120
+ /**
121
+ * The standing HQ instructions an HQ Fleet Runtime bot runs under: the same
122
+ * text a legacy local bot gets, read fresh from its worker folder, with the
123
+ * runtime's own way of labelling senders.
124
+ */
125
+ function fleetBotSystemPrompt(name, config, overrides = {}) {
126
+ return buildSystemPrompt({
127
+ botName: name,
128
+ ...(config.displayName ? { displayName: config.displayName } : {}),
129
+ agentUid: config.agentUid,
130
+ ownerUid: config.ownerUid,
131
+ hqRoot: config.hqRoot,
132
+ workerDir: config.workerDir,
133
+ workerSource: config.workerSource,
134
+ workerId: config.workerId,
135
+ companySlug: config.companySlug,
136
+ kind: overrides.kind ?? effectiveBotKind(config),
137
+ companies: overrides.companies ?? effectiveBotCompanies(config),
138
+ memoryDir: config.memoryDir,
139
+ fleetRuntime: true,
140
+ sources: readPromptSources(config.hqRoot, config.workerDir, undefined, {
141
+ memoryDir: config.memoryDir,
142
+ workerSource: config.workerSource,
143
+ companySlug: config.companySlug,
144
+ }),
145
+ });
146
+ }
120
147
  function runFleetRuntimeControl(name, config, action, json) {
148
+ if (action === "start" || action === "restart") {
149
+ refreshFleetLocalInstructions(config.agentUid, fleetBotSystemPrompt(name, config));
150
+ }
121
151
  const result = controlFleetLocalBot(config.agentUid, action);
122
152
  if (json) {
123
153
  console.log(JSON.stringify({ name, runtime: HQ_FLEET_RUNTIME_ENGINE_ID, ...result }, null, 2));
@@ -149,15 +179,16 @@ async function ownerToken(interactive = process.stdin.isTTY === true) {
149
179
  function rowFor(name, opts = {}) {
150
180
  const dir = botDir(name);
151
181
  const config = readBotConfig(dir);
152
- const status = readBotStatus(dir);
153
- const pid = status?.pid ?? null;
154
- const processAlive = pid !== null && isPidAlive(pid) && status?.state === "running";
182
+ const fleet = isFleetRuntimeBot(config);
183
+ const status = fleet ? null : readBotStatus(dir);
184
+ const pid = fleet ? fleetLocalGatewayPid(config.agentUid) : (status?.pid ?? null);
185
+ const processAlive = pid !== null && isPidAlive(pid) && (fleet || status?.state === "running");
155
186
  // Turns this bot has taken and not finished: what "thinking" means.
156
187
  const inflightTurns = readInflightTurns(dir);
157
188
  let daemonInstalled = false;
158
189
  let daemonLoaded = false;
159
190
  let daemonNodeMissing = null;
160
- if (opts.withDaemon !== false) {
191
+ if (opts.withDaemon !== false && !fleet) {
161
192
  try {
162
193
  const d = botDaemonStatus(buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot }), daemonDeps());
163
194
  daemonInstalled = d.installed;
@@ -678,6 +709,7 @@ async function runBotCreateLocked(name, opts) {
678
709
  const fleetInstallation = useFleetRuntime
679
710
  ? await installPinnedFleetLocalDistribution()
680
711
  : null;
712
+ const fleetPython = fleetInstallation ? await ensureFleetPython() : null;
681
713
  const token = await ownerToken();
682
714
  const owner = await resolveCallerPersonUid(token, undefined, { command: "hq bot create" });
683
715
  if (!owner)
@@ -912,15 +944,18 @@ async function runBotCreateLocked(name, opts) {
912
944
  const scaffold = worker
913
945
  ? scaffoldBotMemory(hqRoot, name, undefined, scaffoldOpts)
914
946
  : scaffoldBotWorker(hqRoot, name, agentUid, fleetInstallation ? HQ_FLEET_RUNTIME_ENGINE_ID : runtime, undefined, scaffoldOpts);
915
- const fleetBootstrap = fleetInstallation
916
- ? bootstrapFleetLocalBot({
947
+ if (fleetInstallation)
948
+ fs.rmSync(path.join(dir, "status.json"), { force: true });
949
+ const fleetBootstrap = fleetInstallation && fleetPython
950
+ ? await bootstrapFleetLocalBot({
917
951
  agentUid,
918
952
  ownerUid: config.ownerUid,
919
953
  hqRoot,
920
954
  botDir: dir,
921
955
  installation: fleetInstallation,
922
956
  machineCredentials: botCredsPath(dir),
923
- }, { start: opts.start !== false })
957
+ systemPrompt: fleetBotSystemPrompt(name, config, { kind, companies }),
958
+ }, { start: opts.start !== false, python: fleetPython })
924
959
  : null;
925
960
  const fleetProfile = fleetBootstrap?.profile ?? null;
926
961
  const runtimeLabel = fleetInstallation ? HQ_FLEET_RUNTIME_ENGINE_ID : runtime;
@@ -1076,6 +1111,9 @@ async function runBotResident(nameArg) {
1076
1111
  const name = validateBotName(nameArg);
1077
1112
  const dir = botDir(name);
1078
1113
  const config = readBotConfig(dir);
1114
+ if (config && isFleetRuntimeBot(config)) {
1115
+ throw Object.assign(new Error(`${name} runs on the HQ Fleet Runtime under its own background service. Use hq bot start ${name}, hq bot status ${name} or hq bot logs ${name}.`), { expected: true });
1116
+ }
1079
1117
  if (!config) {
1080
1118
  // A LaunchAgent that outlived its bot: exiting non-zero here made launchd
1081
1119
  // relaunch this every five seconds forever. Say so once, take the stale
@@ -13,6 +13,7 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
  import { isAccessDeniedError, accessDeniedKeyOf, formatAccessDenied, } from "../utils/access-denied-hint.js";
16
+ import { describeSyncError, formatRemoteListNotConverging, } from "../utils/remote-list-not-converging.js";
16
17
  import { findReadOnlyLocalOnly, isGrantEnforced, readVaultAccessManifest, } from "../lib/sync/read-only-local-only.js";
17
18
  import { SERVER_OWNED_FOLDERS, findServerOwnedLocalOnly, } from "../lib/sync/server-owned-local-only.js";
18
19
  import chalk from "chalk";
@@ -74,6 +75,9 @@ export function formatSyncFailure(err, ctx = {}) {
74
75
  const unreachable = formatUnreachablePushPaths(err, ctx);
75
76
  if (unreachable)
76
77
  return unreachable;
78
+ const remoteList = formatRemoteListNotConverging(err);
79
+ if (remoteList)
80
+ return remoteList;
77
81
  // A plan-limit refusal the runner threw (a 402 on the whole request) renders
78
82
  // with the same reason, fix options and upgrade link as every other refusal.
79
83
  const planGate = planGateErrorFromVaultError(err);
@@ -1744,7 +1748,7 @@ export function registerCloudCommands(program) {
1744
1748
  await withSyncHealthReport(() => runUnderSyncWatchdog(watchdogLimits, "hq sync now", () => runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true)));
1745
1749
  }
1746
1750
  catch (err) {
1747
- console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
1751
+ console.error(chalk.red("\n✗ Sync now failed:"), describeSyncError(err));
1748
1752
  process.exit(1);
1749
1753
  }
1750
1754
  });
@@ -1894,7 +1898,7 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1894
1898
  });
1895
1899
  }
1896
1900
  catch (err) {
1897
- console.error(chalk.red("\n✗ Pull-all failed:"), err instanceof Error ? err.message : String(err));
1901
+ console.error(chalk.red("\n✗ Pull-all failed:"), describeSyncError(err));
1898
1902
  // Unwind (never process.exit) so withSyncHealthReport records the
1899
1903
  // terminal sync_failure before exiting on our behalf.
1900
1904
  throw new SyncExitError(1);
@@ -1963,7 +1967,7 @@ export async function runPullPersonal(hqRoot, onConflict) {
1963
1967
  catch (err) {
1964
1968
  if (err instanceof SyncExitError)
1965
1969
  throw err;
1966
- console.error(chalk.red("\n✗ Pull (personal) failed:"), err instanceof Error ? err.message : String(err));
1970
+ console.error(chalk.red("\n✗ Pull (personal) failed:"), describeSyncError(err));
1967
1971
  throw new SyncExitError(1);
1968
1972
  }
1969
1973
  }
@@ -2048,7 +2052,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
2048
2052
  });
2049
2053
  }
2050
2054
  catch (err) {
2051
- console.error(chalk.red("\n✗ Push-all failed:"), err instanceof Error ? err.message : String(err));
2055
+ console.error(chalk.red("\n✗ Push-all failed:"), describeSyncError(err));
2052
2056
  // Unwind (never process.exit) so withSyncHealthReport records the
2053
2057
  // terminal sync_failure before exiting on our behalf.
2054
2058
  throw new SyncExitError(1);
@@ -2399,7 +2403,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2399
2403
  catch (err) {
2400
2404
  if (err instanceof SyncExitError)
2401
2405
  throw err;
2402
- console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
2406
+ console.error(chalk.red("\n✗ Sync now failed:"), describeSyncError(err));
2403
2407
  throw new SyncExitError(1);
2404
2408
  }
2405
2409
  }
@@ -426,7 +426,7 @@ function formatUnit(u) {
426
426
  ? `last run ${u.lastResult}`
427
427
  : (u.reason ?? "");
428
428
  const restarts = u.restarts > 0 ? ` (restarted ${u.restarts}x)` : "";
429
- return ` ${u.id.padEnd(22)} ${u.status.padEnd(12)} ${detail}${restarts}`;
429
+ return ` ${u.id.padEnd(40)} ${u.status.padEnd(12)} ${detail}${restarts}`;
430
430
  }
431
431
  async function runStatus(d, opts) {
432
432
  const host = currentHost(d);
@@ -1538,6 +1538,21 @@ export function registerDaemonCommand(program, over = {}) {
1538
1538
  .command("self-update")
1539
1539
  .description("Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI")
1540
1540
  .action(wrap((d) => runServiceSelfUpdate(d)));
1541
+ service
1542
+ .command("sync-core-reconcile")
1543
+ .description("Read each company's new-sync-engine switch and write this machine's engine homes and legacy off-switches")
1544
+ .option("--hq-root <path>", "HQ folder the engine works against")
1545
+ .action(wrap(async (d, opts) => {
1546
+ const { runSyncCoreReconcile } = await import("./sync-core.js");
1547
+ await runSyncCoreReconcile({ hqRoot: opts.hqRoot }, {
1548
+ home: d.home,
1549
+ env: d.env,
1550
+ platform: d.platform,
1551
+ arch: process.arch,
1552
+ out: d.out,
1553
+ err: d.err,
1554
+ });
1555
+ }));
1541
1556
  service
1542
1557
  .command("remote-control")
1543
1558
  .description("Keep `claude remote-control` running in a tmux session in the HQ folder")
@@ -78,6 +78,7 @@ export declare function registerFilesCommand(program: Command): Command;
78
78
  * whether it is a folder (`Makefile`, `bin/hq`); resolveBareSharePrefix asks
79
79
  * the vault.
80
80
  */
81
+ export declare function isSingleSegmentWildcardPrefix(prefix: string): boolean;
81
82
  export declare function looksLikeBareFolderPrefix(prefix: string): boolean;
82
83
  /**
83
84
  * Refuse a bare prefix that would become a malformed grant if it were
@@ -164,7 +164,7 @@ export function formatShareSessionError(err) {
164
164
  export function registerFilesCommand(program) {
165
165
  const files = program
166
166
  .command("files")
167
- .description("Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children.")
167
+ .description("Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children, and a quoted 'clients/*/reports' shares that folder under every client.")
168
168
  .option("--company <slug>", "Company slug (resolves to companyUid)");
169
169
  // An exported HQ_API_KEY must never fall back to the caller's Cognito session
170
170
  // (that would ignore the key's scope entirely), so every subcommand is gated.
@@ -193,7 +193,7 @@ export function registerFilesCommand(program) {
193
193
  });
194
194
  files
195
195
  .command("share [paths...]")
196
- .description("Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
196
+ .description("Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*', or a one-folder wildcard such as 'clients/*/reports', so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
197
197
  .option("--with <principal>", "Email address, group id, company Outpost id (out_...), or '@all' to share with every active company member")
198
198
  .option("--permission <level>", "Permission level (only with --with): read | write")
199
199
  .option("--exact", "With --with: grant exactly the object named by <prefix>, even when it looks like a folder (by default a bare folder name such as 'knowledge' is granted as the folder glob 'knowledge/*').")
@@ -763,6 +763,8 @@ export function registerFilesCommand(program) {
763
763
  async function warnIfEmptySharePrefix(params) {
764
764
  if (isPrivateFolderPattern(params.prefix))
765
765
  return;
766
+ if (isSingleSegmentWildcardPrefix(params.prefix))
767
+ return;
766
768
  let enabled;
767
769
  try {
768
770
  enabled = await resolveCompanyFlagGate(params.companyUid, params.token, FILES_SHARE_EMPTY_PREFIX_WARNING_FLAG, { fallback: false }, () => false);
@@ -879,9 +881,21 @@ async function resolveMentionPersonUid(mention, companySlug) {
879
881
  * whether it is a folder (`Makefile`, `bin/hq`); resolveBareSharePrefix asks
880
882
  * the vault.
881
883
  */
884
+ export function isSingleSegmentWildcardPrefix(prefix) {
885
+ const body = prefix.endsWith("/*") ? prefix.slice(0, -2) : prefix;
886
+ const segments = body.split("/");
887
+ if (segments.some((segment) => segment.length === 0))
888
+ return false;
889
+ if (segments.some((segment) => segment !== "*" && segment.includes("*")))
890
+ return false;
891
+ const wildcards = segments.filter((segment) => segment === "*").length;
892
+ return wildcards === 1 && wildcards < segments.length;
893
+ }
882
894
  export function looksLikeBareFolderPrefix(prefix) {
883
895
  if (prefix === "*" || prefix.endsWith("/") || prefix.endsWith("/*"))
884
896
  return false;
897
+ if (isSingleSegmentWildcardPrefix(prefix))
898
+ return false;
885
899
  const last = prefix.slice(prefix.lastIndexOf("/") + 1);
886
900
  return last.length > 0 && !last.includes(".");
887
901
  }
@@ -894,8 +908,12 @@ export function malformedBareSharePrefix(prefix) {
894
908
  if (!looksLikeBareFolderPrefix(prefix))
895
909
  return null;
896
910
  if (prefix.includes("*")) {
897
- return (`'${prefix}' has a '*' inside a path segment. Only a whole trailing '/*' segment ` +
898
- `(for example 'knowledge/*') or '*' alone (--full) is a glob.`);
911
+ const reason = prefix.split("/").some((segment) => segment !== "*" && segment.includes("*"))
912
+ ? "has a '*' inside a path segment"
913
+ : "has more than one '*' folder";
914
+ return (`'${prefix}' ${reason}. A '*' must be a whole folder name: once per path ` +
915
+ `(for example 'clients/*/reports'), as a trailing '/*' (for example 'knowledge/*'), ` +
916
+ `or '*' alone (--full).`);
899
917
  }
900
918
  if (prefix.split("/").some((segment) => segment.length === 0)) {
901
919
  return `'${prefix}' has an empty path segment. Pass a company-relative path such as 'knowledge'.`;
@@ -0,0 +1,22 @@
1
+ import type { Command } from "commander";
2
+ import { type SyncCoreReconcileResult } from "../lib/sync-core/reconcile.js";
3
+ export interface SyncCoreReconcileCommandOptions {
4
+ hqRoot?: string;
5
+ companiesOnly?: boolean;
6
+ tokenFile?: string;
7
+ json?: boolean;
8
+ }
9
+ export interface SyncCoreCommandDeps {
10
+ home: string;
11
+ env: NodeJS.ProcessEnv;
12
+ platform: NodeJS.Platform;
13
+ arch: string;
14
+ out: (line: string) => void;
15
+ err: (line: string) => void;
16
+ }
17
+ export declare function runSyncCoreReconcile(opts: SyncCoreReconcileCommandOptions, d?: SyncCoreCommandDeps): Promise<SyncCoreReconcileResult>;
18
+ export declare function runSyncCoreStatus(opts: {
19
+ json?: boolean;
20
+ }, d?: SyncCoreCommandDeps): void;
21
+ export declare function registerSyncCoreCommand(program: Command, over?: Partial<SyncCoreCommandDeps>): void;
22
+ //# sourceMappingURL=sync-core.d.ts.map