@indigoai-us/hq-cli 5.345.201 → 5.345.203

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 (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/command-catalog.generated.d.ts +24 -0
  3. package/dist/command-catalog.generated.js +26 -0
  4. package/dist/commands/capacity.js +10 -2
  5. package/dist/commands/daemon.d.ts +2 -1
  6. package/dist/commands/daemon.js +225 -4
  7. package/dist/commands/integrations-core.d.ts +3 -0
  8. package/dist/commands/integrations-core.js +11 -0
  9. package/dist/commands/integrations-manage.js +2 -2
  10. package/dist/commands/integrations.js +5 -5
  11. package/dist/commands/sync-core.js +2 -1
  12. package/dist/lib/bot/executive-settings.d.ts +1 -1
  13. package/dist/lib/bot/executive-settings.js +16 -6
  14. package/dist/lib/bot/jobs/meeting-detect.js +2 -2
  15. package/dist/lib/bot/jobs/meeting-rider.js +2 -2
  16. package/dist/lib/bot/jobs/signal-to-lane.d.ts +3 -2
  17. package/dist/lib/bot/jobs/signal-to-lane.js +31 -8
  18. package/dist/lib/bot/run.js +3 -3
  19. package/dist/lib/capacity/codex-availability.d.ts +6 -0
  20. package/dist/lib/capacity/codex-availability.js +33 -0
  21. package/dist/lib/capacity/ledger.d.ts +0 -11
  22. package/dist/lib/capacity/ledger.js +30 -3
  23. package/dist/lib/capacity/route.js +11 -5
  24. package/dist/lib/daemon/catalog.d.ts +3 -0
  25. package/dist/lib/daemon/catalog.js +15 -2
  26. package/dist/lib/daemon/handoff.d.ts +2 -0
  27. package/dist/lib/daemon/handoff.js +9 -2
  28. package/dist/lib/daemon/install.d.ts +99 -0
  29. package/dist/lib/daemon/install.js +316 -7
  30. package/dist/lib/daemon/migrate.d.ts +3 -1
  31. package/dist/lib/daemon/migrate.js +14 -2
  32. package/dist/lib/daemon/plist-refresh.d.ts +34 -0
  33. package/dist/lib/daemon/plist-refresh.js +62 -0
  34. package/dist/lib/daemon/run.js +25 -0
  35. package/dist/lib/daemon/selfcheck.d.ts +71 -0
  36. package/dist/lib/daemon/selfcheck.js +150 -0
  37. package/dist/lib/daemon/supervisor.d.ts +31 -0
  38. package/dist/lib/daemon/supervisor.js +130 -6
  39. package/dist/lib/daemon/unit-holds.js +2 -1
  40. package/dist/lib/service-manager/launchd.d.ts +25 -0
  41. package/dist/lib/service-manager/launchd.js +51 -3
  42. package/dist/lib/service-manager/migration.d.ts +14 -0
  43. package/dist/lib/service-manager/migration.js +34 -0
  44. package/dist/lib/service-manager/systemd.d.ts +13 -0
  45. package/dist/lib/service-manager/systemd.js +19 -0
  46. package/dist/lib/service-manager/types.d.ts +8 -0
  47. package/dist/lib/sync-core/reconcile.d.ts +4 -1
  48. package/dist/lib/sync-core/reconcile.js +12 -2
  49. package/dist/lib/sync-core/release.d.ts +23 -0
  50. package/dist/lib/sync-core/release.js +39 -0
  51. package/dist/main.js +3 -3
  52. package/dist/utils/cli-network-lifecycle.d.ts +9 -0
  53. package/dist/utils/cli-network-lifecycle.js +25 -1
  54. package/package.json +9 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.203] — 2026-11-05
6
+ - Fix Codex capacity routing after plan exhaustion: available credits keep accounts usable at lowest priority, and superseded snapshots or passed resets no longer retain exhausted usage.
7
+ - Keep company Outpost integration commands on their delegated machine session and allow sharing connections with an `out_*` principal.
8
+ - On macOS the `hq daemon` LaunchAgent now runs node directly and is no longer attributed to the HQ app, so launchd loads it at login whether or not the app is installed or allowed. `hq daemon status` now asks launchd or systemd and says whether the daemon runs under the service manager, runs outside it, or is not loaded. New `hq daemon start` loads the unit when it is missing and starts it. A Mac that still has the older plist, which runs through the app, gets it rewritten and reloaded when the daemon next starts or when `hq daemon start` runs, with no `hq daemon install` needed; `hq daemon status` warns while the plist, or the job launchd has loaded, still runs through the app, and `hq daemon start` reloads a job left on the old definition. `hq daemon start` leaves a daemon the desktop app hosts alone, and no longer mistakes state left by an earlier app-hosted daemon for that. `hq daemon install` treats a failed bootstrap as an error and retries it, and `hq daemon migrate --apply` loads the daemon's agent first and loads an older agent again if taking it over fails.
9
+ - The daemon no longer holds a sync engine for 30 minutes when its heartbeat timeouts come from a slow transfer the engine reports progress on (up to six such restarts per episode; after that they count again), and a sync engine held because another process had its lock (exit 17) starts again as soon as that process exits.
10
+ - The sync engine release can now be pinned on the server (`pin.json` on the engine download base); the CLI's built-in release is the fallback, and an installed engine is never downgraded.
11
+ - A self-check in the daemon sends the person signed in on the machine a DM when a sync engine unit has been held or failing for over 30 minutes, at most once a day per machine, naming every affected vault. It reports only units the daemon runs, and sends nothing when the machine is signed in as an agent.
12
+ - The Executive opt-in key is now `executive.enabled` in the HQ menubar settings file. The old `executiveMeetingRider.enabled` key is ignored, so values saved under it earlier now read as off.
13
+
14
+ ## [5.345.202] — 2026-11-04
15
+ - The long-running `hq daemon run` and `hq mesh daemon run` processes no longer start Sentry. On some Macs, Sentry in these processes kept opening connections after network changes until the machine ran out of ports, so Claude and HQ lost internet access while other apps kept working. Short-lived CLI commands still report errors to Sentry, and the daemons keep their local logs.
16
+ - The Executive (meeting rider, action-item lanes and source report DMs) is now off unless `executiveMeetingRider.enabled` is set to `true` in the HQ menubar settings file. In 5.345.200 it was on by default for every setup bot.
17
+ - Native command guards on Windows now resolve tools with the same Git directories and home bin precedence as the Bash fallback, preventing a plain Windows PATH from selecting System32 timeout or missing Git tools. Added exact CLI command-argument parity coverage and portable temporary-path fixtures.
18
+ - Port the complete policy reminder injector to Rust, including scope resolution, trigger matching, relevance ranking, output ceilings, deferred policies, cadence and seen ledgers, and receipt-backed enforcement. Shadow runs use isolated state so the Bash reference sees the original session history. Add a pinned full-injector corpus using staging regressions, release-shipped core policies and reproducibly generated synthetic policy/event payloads, with byte comparisons of output and state after each event. No rollout flags change.
19
+ - Match Bash locale collation when indexing policy files, including Git Bash Windows collation and the macOS non-ASCII fallback order. Index format 3 rebuilds old cache files.
20
+
5
21
  ## [5.345.201] — 2026-11-04
6
22
  - `hq deploy domains buy` accepts a registrar price in `usd` or `USD`. Search shows that price as US dollars, and renew accepts the same code. A price in another currency is still refused, and the message shows the currency code the server sent.
7
23
  - The Executive sends source reports to its owner when a meeting ends and one final threaded report after all source lanes finish. Delivery receipts suppress normal duplicates and retain reports during the ten-minute cooldown. Recovery retries an intent without a sent receipt; a crash after sending but before recording the receipt can resend that report.
@@ -18236,6 +18236,18 @@ export declare const COMMAND_CATALOG: readonly [{
18236
18236
  readonly hidden: false;
18237
18237
  }];
18238
18238
  readonly subcommands: readonly [];
18239
+ }, {
18240
+ readonly name: "start";
18241
+ readonly description: "Load the daemon's OS unit if the service manager does not have it, and start it";
18242
+ readonly aliases: readonly [];
18243
+ readonly hidden: false;
18244
+ readonly helpOption: true;
18245
+ readonly helpCommand: false;
18246
+ readonly usage: "[options]";
18247
+ readonly helpText: readonly [];
18248
+ readonly arguments: readonly [];
18249
+ readonly options: readonly [];
18250
+ readonly subcommands: readonly [];
18239
18251
  }, {
18240
18252
  readonly name: "uninstall";
18241
18253
  readonly description: "Stop the daemon and remove its OS unit";
@@ -18816,6 +18828,18 @@ export declare const COMMAND_CATALOG: readonly [{
18816
18828
  readonly hidden: false;
18817
18829
  }];
18818
18830
  readonly subcommands: readonly [];
18831
+ }, {
18832
+ readonly name: "sync-core-selfcheck";
18833
+ readonly description: "DM the person signed in here when a sync engine unit has been held or failing for over 30 minutes (at most daily per unit set)";
18834
+ readonly aliases: readonly [];
18835
+ readonly hidden: false;
18836
+ readonly helpOption: true;
18837
+ readonly helpCommand: false;
18838
+ readonly usage: "[options]";
18839
+ readonly helpText: readonly [];
18840
+ readonly arguments: readonly [];
18841
+ readonly options: readonly [];
18842
+ readonly subcommands: readonly [];
18819
18843
  }, {
18820
18844
  readonly name: "sync-core-manifest";
18821
18845
  readonly description: "Upload the sync-audit manifest for every vault the new sync engine owns on this machine";
@@ -22248,6 +22248,19 @@ export const COMMAND_CATALOG = [
22248
22248
  ],
22249
22249
  "subcommands": []
22250
22250
  },
22251
+ {
22252
+ "name": "start",
22253
+ "description": "Load the daemon's OS unit if the service manager does not have it, and start it",
22254
+ "aliases": [],
22255
+ "hidden": false,
22256
+ "helpOption": true,
22257
+ "helpCommand": false,
22258
+ "usage": "[options]",
22259
+ "helpText": [],
22260
+ "arguments": [],
22261
+ "options": [],
22262
+ "subcommands": []
22263
+ },
22251
22264
  {
22252
22265
  "name": "uninstall",
22253
22266
  "description": "Stop the daemon and remove its OS unit",
@@ -22941,6 +22954,19 @@ export const COMMAND_CATALOG = [
22941
22954
  ],
22942
22955
  "subcommands": []
22943
22956
  },
22957
+ {
22958
+ "name": "sync-core-selfcheck",
22959
+ "description": "DM the person signed in here when a sync engine unit has been held or failing for over 30 minutes (at most daily per unit set)",
22960
+ "aliases": [],
22961
+ "hidden": false,
22962
+ "helpOption": true,
22963
+ "helpCommand": false,
22964
+ "usage": "[options]",
22965
+ "helpText": [],
22966
+ "arguments": [],
22967
+ "options": [],
22968
+ "subcommands": []
22969
+ },
22944
22970
  {
22945
22971
  "name": "sync-core-manifest",
22946
22972
  "description": "Upload the sync-audit manifest for every vault the new sync engine owns on this machine",
@@ -1,3 +1,4 @@
1
+ import { hasCodexCreditRoom } from "../lib/capacity/codex-availability.js";
1
2
  import * as crypto from "node:crypto";
2
3
  import * as fs from "node:fs";
3
4
  import * as os from "node:os";
@@ -98,7 +99,10 @@ export function formatCapacityTable(view, nowMs, accounts = []) {
98
99
  return view.providers.map((provider) => {
99
100
  const head = `${provider.provider} ${provider.state}`;
100
101
  if (!provider.accounts || provider.accounts.length === 0) {
101
- return `${head}\n${formatLimitTable(provider.limits, nowMs)}`;
102
+ const creditNote = provider.provider === "codex" && provider.state === "ok"
103
+ && provider.limits.some((limit) => limit.state === "exhausted")
104
+ ? " — usable on Codex credits" : "";
105
+ return `${head}${creditNote}\n${formatLimitTable(provider.limits, nowMs)}`;
102
106
  }
103
107
  const blocks = provider.accounts.map((account) => {
104
108
  const labels = account.labels.map((label) => {
@@ -112,7 +116,11 @@ export function formatCapacityTable(view, nowMs, accounts = []) {
112
116
  const fix = presented.command ? formatRefreshLine(provider.provider, presented.command) : "";
113
117
  return fix ? `${title}\n${reason}\n${fix}` : `${title}\n${reason}`;
114
118
  }
115
- return `${title}\n${formatLimitTable(account.limits, nowMs)}`;
119
+ const creditNote = provider.provider === "codex"
120
+ && account.limits.some((limit) => limit.state === "exhausted")
121
+ && hasCodexCreditRoom(account.limits, nowMs)
122
+ ? " — usable on Codex credits (ranked last)" : "";
123
+ return `${title}${creditNote}\n${formatLimitTable(account.limits, nowMs)}`;
116
124
  });
117
125
  return [head, ...blocks].join("\n");
118
126
  }).join("\n\n");
@@ -4,7 +4,8 @@
4
4
  * hq daemon status (same as `hq daemon status`)
5
5
  * hq daemon install detect the machine type, install the one OS unit
6
6
  * hq daemon run what the OS unit runs; starts the default services
7
- * hq daemon status [--json]
7
+ * hq daemon start load the OS unit if needed and start it
8
+ * hq daemon status [--json] includes whether the service manager runs it
8
9
  * hq daemon logs [unit] [--follow] [--lines N]
9
10
  * hq daemon restart [unit] | trigger <unit>
10
11
  * hq daemon enable|disable <service...> per-machine overrides
@@ -4,7 +4,8 @@
4
4
  * hq daemon status (same as `hq daemon status`)
5
5
  * hq daemon install detect the machine type, install the one OS unit
6
6
  * hq daemon run what the OS unit runs; starts the default services
7
- * hq daemon status [--json]
7
+ * hq daemon start load the OS unit if needed and start it
8
+ * hq daemon status [--json] includes whether the service manager runs it
8
9
  * hq daemon logs [unit] [--follow] [--lines N]
9
10
  * hq daemon restart [unit] | trigger <unit>
10
11
  * hq daemon enable|disable <service...> per-machine overrides
@@ -24,7 +25,8 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
24
25
  import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
25
26
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
26
27
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
27
- import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, legacyWindowsTask, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
28
+ import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, daemonServiceStatus, describeSupervision, legacyWindowsTask, startDaemonUnit, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
29
+ import { PLIST_REFRESH_MARKER } from "../lib/daemon/plist-refresh.js";
28
30
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
29
31
  import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
30
32
  import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
@@ -438,28 +440,126 @@ function formatUnit(u) {
438
440
  const restarts = u.restarts > 0 ? ` (restarted ${u.restarts}x)` : "";
439
441
  return ` ${u.id.padEnd(40)} ${u.status.padEnd(12)} ${detail}${restarts}`;
440
442
  }
443
+ /**
444
+ * True when the daemon holding the lock is the one the desktop app hosts.
445
+ * state.json outlives the daemon that wrote it, and a new daemon writes its own
446
+ * only after starting up, so a `host: "desktop"` left by an earlier app-hosted
447
+ * daemon counts only when its pid is the lock holder's.
448
+ */
449
+ function hostedByDesktop(state, lockPid) {
450
+ return lockPid !== null && state?.host === "desktop" && state.pid === lockPid;
451
+ }
452
+ /**
453
+ * The service manager's warnings, plus where the automatic plist rewrite
454
+ * logged when it was tried and the plist still needs it.
455
+ */
456
+ function statusWarnings(service, paths) {
457
+ const warnings = [...(service.warnings ?? [])];
458
+ if (warnings.length > 0 && fs.existsSync(path.join(paths.root, PLIST_REFRESH_MARKER))) {
459
+ warnings.push(`The daemon tried to rewrite it automatically and did not finish. See: ${path.join(paths.root, "logs", "plist-refresh.log")}`);
460
+ }
461
+ return warnings;
462
+ }
441
463
  async function runStatus(d, opts) {
442
464
  const host = currentHost(d);
443
465
  const { paths } = host;
444
466
  const pid = runningDaemonPid(paths.lockPath, d.daemonAlive);
445
467
  const state = pid ? readState(paths) : null;
446
468
  const config = readDaemonConfig(paths.configPath);
469
+ // The pid lock alone says nothing about whether the daemon comes back after
470
+ // a crash or a login: ask the service manager too.
471
+ const service = daemonServiceStatus(hostFor(d, paths, false), d.run, pid);
472
+ // A daemon hosted by the desktop app is supervised by the app, not the OS.
473
+ const hosted = hostedByDesktop(state, pid);
447
474
  if (opts.json) {
448
- d.out(JSON.stringify({ running: pid !== null, pid, machineType: state?.machineType ?? config.machineType ?? null, units: state?.units ?? [] }, null, 2));
475
+ d.out(JSON.stringify({
476
+ running: pid !== null,
477
+ pid,
478
+ supervision: hosted && service.supervision === "unmanaged" ? "desktop-app" : service.supervision,
479
+ serviceManager: {
480
+ manager: service.manager,
481
+ unitInstalled: service.unitInstalled,
482
+ loaded: service.loaded,
483
+ active: service.active,
484
+ pid: service.managerPid ?? null,
485
+ detail: service.detail,
486
+ },
487
+ warnings: statusWarnings(service, paths),
488
+ machineType: state?.machineType ?? config.machineType ?? null,
489
+ units: state?.units ?? [],
490
+ }, null, 2));
449
491
  return;
450
492
  }
493
+ const managerLine = hosted && service.supervision === "unmanaged"
494
+ ? `Service manager: running as the HQ desktop app's child (${service.detail}).`
495
+ : `Service manager: ${describeSupervision(service)}.`;
496
+ const warnings = statusWarnings(service, paths).map((w) => `Warning: ${w}`);
451
497
  if (!pid || !state) {
452
498
  const { detected } = host;
453
499
  d.out(`hq daemon is not running. Machine type: ${detected.type} (${detected.detail}).`);
500
+ d.out(managerLine);
501
+ for (const line of warnings)
502
+ d.out(line);
454
503
  d.out("Default services for this machine:");
455
504
  for (const line of describeServices(detected.type, config))
456
505
  d.out(line);
457
506
  return;
458
507
  }
459
508
  d.out(`hq daemon is running (pid ${pid}) on a ${state.machineType} machine.`);
509
+ d.out(managerLine);
510
+ for (const line of warnings)
511
+ d.out(line);
460
512
  for (const u of state.units)
461
513
  d.out(formatUnit(u));
462
514
  }
515
+ async function runStart(d) {
516
+ const host = currentHost(d);
517
+ const { paths } = host;
518
+ const pid = runningDaemonPid(paths.lockPath, d.daemonAlive);
519
+ // The desktop app hosts this daemon and restarts it itself. Loading the OS
520
+ // unit now would start a second daemon that exits on the lock and is
521
+ // respawned until the app quits.
522
+ if (hostedByDesktop(readState(paths), pid)) {
523
+ d.out(`hq daemon is running (pid ${pid}) inside the desktop app, which restarts it. ` +
524
+ "Nothing to start. To run it under the service manager instead, quit the app, then run: hq daemon start");
525
+ return;
526
+ }
527
+ const before = daemonServiceStatus(hostFor(d, paths, false), d.run, pid);
528
+ if (!before.unitInstalled && before.manager !== "none") {
529
+ // Nothing to load: install writes the unit, loads it and starts it.
530
+ d.out("hq daemon is not installed here yet; installing it.");
531
+ await runInstall(d, {});
532
+ return;
533
+ }
534
+ const result = startDaemonUnit(hostFor(d, paths, false), {
535
+ run: d.run,
536
+ sleep: d.sleep,
537
+ daemonPid: () => runningDaemonPid(paths.lockPath, d.daemonAlive),
538
+ });
539
+ handOver(d, host);
540
+ const refresh = result.refresh;
541
+ if (refresh?.action === "rewritten" || refresh?.action === "reloaded") {
542
+ d.out(`Rewrote ${refresh.plistPath} so launchd loads the daemon at login without HQ.app.`);
543
+ }
544
+ if (refresh?.error)
545
+ d.err(`warning: ${refresh.error}`);
546
+ if (result.action === "already-running") {
547
+ d.out(`hq daemon is already running (pid ${result.status.daemonPid}) under the service manager.`);
548
+ return;
549
+ }
550
+ if (result.status.supervision !== "managed" || result.status.daemonPid === null) {
551
+ const what = result.action === "loaded"
552
+ ? "Loaded the daemon's unit"
553
+ : result.action === "reloaded"
554
+ ? "Reloaded the daemon's unit"
555
+ : "Asked the service manager to start the daemon";
556
+ const why = result.status.supervision === "managed"
557
+ ? `no daemon took the lock within 15 seconds (${result.status.detail})`
558
+ : `it is ${describeSupervision(result.status)}`;
559
+ throw new DaemonCommandError(`${what}, but ${why}. See: hq daemon logs`);
560
+ }
561
+ d.out(`${result.action === "loaded" ? "Loaded and started" : result.action === "reloaded" ? "Reloaded and started" : "Started"} hq daemon (pid ${result.status.daemonPid}) under the service manager.`);
562
+ }
463
563
  function tailLines(file, lines) {
464
564
  const text = fs.readFileSync(file, "utf8");
465
565
  return text.split("\n").filter((l, i, all) => i < all.length - 1 || l !== "").slice(-lines);
@@ -886,7 +986,26 @@ async function runMigrate(d, opts) {
886
986
  if (!installed) {
887
987
  throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
888
988
  }
889
- const result = adoptLegacyUnits(units, migrateDeps(d, host.paths, host.runAsUser));
989
+ // The daemon starts the adopted services, so its own unit must be loaded
990
+ // before the older units are booted out (an installed but unloaded plist
991
+ // would leave every adopted service off until the next manual start).
992
+ const ensureDaemonLoaded = (when) => {
993
+ const daemonPid = () => runningDaemonPid(host.paths.lockPath, d.daemonAlive);
994
+ const status = daemonServiceStatus(hostFor(d, host.paths, false), d.run, daemonPid());
995
+ if (status.loaded || status.manager === "none")
996
+ return;
997
+ try {
998
+ startDaemonUnit(hostFor(d, host.paths, false), { run: d.run, sleep: d.sleep, daemonPid, waitMs: 0 });
999
+ d.out(`Loaded the daemon's unit ${when}.`);
1000
+ }
1001
+ catch (err) {
1002
+ throw new DaemonCommandError(`The daemon's unit is not loaded and could not be loaded ${when}: ${err.message}`);
1003
+ }
1004
+ };
1005
+ ensureDaemonLoaded("before taking over the older units");
1006
+ const result = adoptLegacyUnits(units, { ...migrateDeps(d, host.paths, host.runAsUser), sleep: d.sleep });
1007
+ if (result.adopted.length > 0)
1008
+ ensureDaemonLoaded("after taking over the older units");
890
1009
  handOver(d, host);
891
1010
  for (const a of result.adopted)
892
1011
  d.out(`Took over ${a.unitId} from ${describeLegacyUnit(a)}`);
@@ -1306,6 +1425,68 @@ async function runServiceCodexRemoteControl(d, opts) {
1306
1425
  watch: codex,
1307
1426
  });
1308
1427
  }
1428
+ /**
1429
+ * DM the person signed in on this machine (the machine owner) through the
1430
+ * same route as `hq dm`. A machine identity (agent box) has no person to tell.
1431
+ */
1432
+ /**
1433
+ * Which engine vaults the self-check reports, and their names: a vault skipped
1434
+ * in ~/.hq/sync-core/skip.json or not on the rollout recorded in rollout.json
1435
+ * is kept on the old sync on purpose, so a hold left over for it is not an outage.
1436
+ */
1437
+ async function selfCheckVaults(home) {
1438
+ const [{ readSyncCoreSkips }, { readSyncCoreRolloutState }] = await Promise.all([
1439
+ import("../lib/sync-core/skip.js"),
1440
+ import("../lib/sync-core/rollout-state.js"),
1441
+ ]);
1442
+ let skipped = new Set();
1443
+ try {
1444
+ skipped = new Set(readSyncCoreSkips(home).map((s) => s.uid));
1445
+ }
1446
+ catch {
1447
+ // An unreadable skip list skips nothing here; the reconcile reports it.
1448
+ }
1449
+ let rollout = null;
1450
+ try {
1451
+ rollout = readSyncCoreRolloutState(home);
1452
+ }
1453
+ catch {
1454
+ // No rollout file: every vault counts.
1455
+ }
1456
+ const onRollout = rollout ? new Map(rollout.vaults.map((v) => [v.uid, v])) : null;
1457
+ return {
1458
+ wanted: (uid) => !skipped.has(uid) && (onRollout === null || onRollout.has(uid)),
1459
+ vaultName: (uid) => {
1460
+ const v = onRollout?.get(uid);
1461
+ if (!v)
1462
+ return undefined;
1463
+ return v.kind === "personal" ? "your personal vault" : v.slug;
1464
+ },
1465
+ };
1466
+ }
1467
+ async function sendDmToMachineOwner(message) {
1468
+ const [{ ensureCognitoToken }, { agentUidFromToken, resolveCallerPersonUid, vaultApiFetch }] = await Promise.all([
1469
+ import("../utils/cognito-session.js"),
1470
+ import("../utils/vault-api.js"),
1471
+ ]);
1472
+ const token = await ensureCognitoToken();
1473
+ // An agent box has no person signed in, and the server does not deliver a
1474
+ // DM an agent sends itself: there is no owner to reach from here.
1475
+ if (agentUidFromToken(token))
1476
+ return null;
1477
+ const personUid = await resolveCallerPersonUid(token, undefined, { command: "hq daemon service sync-core-selfcheck" });
1478
+ const res = await vaultApiFetch({
1479
+ token,
1480
+ path: "/v1/notify/dm",
1481
+ method: "POST",
1482
+ body: { toPersonUid: personUid, body: message.body, details: message.details },
1483
+ });
1484
+ if (!res.ok) {
1485
+ const err = (await res.json().catch(() => ({})));
1486
+ throw new DaemonCommandError(`could not send the self-check DM: HTTP ${res.status} ${err.code ?? ""} ${err.error ?? err.message ?? ""}`.trim());
1487
+ }
1488
+ return personUid;
1489
+ }
1309
1490
  export function registerDaemonCommand(program, over = {}) {
1310
1491
  let resolved = null;
1311
1492
  const deps = () => (resolved ??= loadDefaults(over));
@@ -1330,6 +1511,10 @@ export function registerDaemonCommand(program, over = {}) {
1330
1511
  .option("--hq-root <path>", "HQ folder the services work against")
1331
1512
  .option("--user <login>", "on a box: the login user the daemon runs as (never root)")
1332
1513
  .action(wrap((d, opts) => runInstall(d, opts)));
1514
+ daemon
1515
+ .command("start")
1516
+ .description("Load the daemon's OS unit if the service manager does not have it, and start it")
1517
+ .action(wrap((d) => runStart(d)));
1333
1518
  daemon
1334
1519
  .command("uninstall")
1335
1520
  .description("Stop the daemon and remove its OS unit")
@@ -1601,6 +1786,42 @@ export function registerDaemonCommand(program, over = {}) {
1601
1786
  err: d.err,
1602
1787
  });
1603
1788
  }));
1789
+ service
1790
+ .command("sync-core-selfcheck")
1791
+ .description("DM the person signed in here when a sync engine unit has been held or failing for over 30 minutes (at most daily per unit set)")
1792
+ .action(wrap(async (d) => {
1793
+ const [{ runSelfCheck }, { readUnitHolds }] = await Promise.all([
1794
+ import("../lib/daemon/selfcheck.js"),
1795
+ import("../lib/daemon/unit-holds.js"),
1796
+ ]);
1797
+ const { paths } = currentHost(d);
1798
+ await runSelfCheck({
1799
+ statePath: paths.statePath,
1800
+ holdsPath: paths.holdsPath,
1801
+ recordPath: path.join(paths.root, "selfcheck.json"),
1802
+ now: Date.now,
1803
+ machine: os.hostname(),
1804
+ readState: () => {
1805
+ try {
1806
+ return readState(paths);
1807
+ }
1808
+ catch {
1809
+ return null;
1810
+ }
1811
+ },
1812
+ readHolds: () => {
1813
+ try {
1814
+ return readUnitHolds(paths.holdsPath);
1815
+ }
1816
+ catch {
1817
+ return {};
1818
+ }
1819
+ },
1820
+ sendToOwner: (message) => sendDmToMachineOwner(message),
1821
+ ...(await selfCheckVaults(d.home)),
1822
+ log: (line) => d.out(line),
1823
+ });
1824
+ }));
1604
1825
  service
1605
1826
  .command("sync-core-manifest")
1606
1827
  .description("Upload the sync-audit manifest for every vault the new sync engine owns on this machine")
@@ -8,6 +8,7 @@
8
8
  * Sentry live in exactly one place. `integrations.ts` re-exports this module's
9
9
  * public surface, so existing importers keep working unchanged.
10
10
  */
11
+ import { ensureCognitoIdToken } from "../utils/cognito-session.js";
11
12
  export { bareProvider } from "../lib/integrations/provider-slug.js";
12
13
  /** Write policy on a connection — mirrors hq-pro's `WritePolicy`. */
13
14
  export type WritePolicy = "auto-allow" | "confirm" | "deny";
@@ -415,4 +416,6 @@ export declare function selectAccountConnections(connections: readonly AdminConn
415
416
  account: string;
416
417
  provider?: string;
417
418
  }): AdminConnection[];
419
+ /** A runner-selected session must never fall back to a cached human identity. */
420
+ export declare function ensureIntegrationIdToken(options?: Parameters<typeof ensureCognitoIdToken>[0]): Promise<string>;
418
421
  //# sourceMappingURL=integrations-core.d.ts.map
@@ -9,6 +9,8 @@
9
9
  * public surface, so existing importers keep working unchanged.
10
10
  */
11
11
  import { randomUUID } from "node:crypto";
12
+ import { delegatedMachineSessionFilePath, loadDelegatedMachineSession, getValidMachineTokens } from "@indigoai-us/hq-cloud";
13
+ import { DEFAULT_COGNITO, ensureCognitoIdToken } from "../utils/cognito-session.js";
12
14
  import chalk from "chalk";
13
15
  import { vaultApiFetch } from "../utils/vault-api.js";
14
16
  import { AuthError } from "../utils/auth-error.js";
@@ -991,4 +993,13 @@ export function selectAccountConnections(connections, opts) {
991
993
  }
992
994
  return matches;
993
995
  }
996
+ /** A runner-selected session must never fall back to a cached human identity. */
997
+ export async function ensureIntegrationIdToken(options) {
998
+ if (delegatedMachineSessionFilePath() !== null) {
999
+ if (!loadDelegatedMachineSession())
1000
+ throw new Error("HQ_MACHINE_SESSION_FILE is not a usable delegated session; the job runner must rewrite it.");
1001
+ return (await getValidMachineTokens(DEFAULT_COGNITO)).idToken;
1002
+ }
1003
+ return options === undefined ? ensureCognitoIdToken() : ensureCognitoIdToken(options);
1004
+ }
994
1005
  //# sourceMappingURL=integrations-core.js.map
@@ -71,10 +71,10 @@ export function parsePrincipal(raw) {
71
71
  return { granteeType: "group", granteeId: value };
72
72
  // Agents are people to the ACL: they hold an identity and a membership, so a
73
73
  // grant to `agt_…` is a person grant, not a separate grantee kind.
74
- if (/^(prs|psn|agt)_[A-Za-z0-9_-]+$/i.test(value)) {
74
+ if (/^(prs|psn|agt|out)_[A-Za-z0-9_-]+$/i.test(value)) {
75
75
  return { granteeType: "person", granteeId: value };
76
76
  }
77
- throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
77
+ throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / out_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
78
78
  }
79
79
  /** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
80
80
  export const PRINCIPAL_HELP = "Email, uid, vault group (grp_…), or `everyone`";
@@ -53,7 +53,7 @@
53
53
  import { randomUUID } from "node:crypto";
54
54
  import { readFile } from "node:fs/promises";
55
55
  import chalk from "chalk";
56
- import { ensureCognitoIdToken } from "../utils/cognito-session.js";
56
+ import { ensureIntegrationIdToken } from "./integrations-core.js";
57
57
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
58
58
  import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
59
59
  import { IntegrationsCliError, bareProvider, callGateway, parseCallTimeoutMs, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, connectionAccountLabel, connectionOwnerLabel, selectAccountConnections, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, accountCandidateFor, accountSelectionMessage, ACCOUNT_REQUIRED_CODE, } from "./integrations-core.js";
@@ -361,7 +361,7 @@ export function registerIntegrationsCommand(program) {
361
361
  .option("--usable", "Only apps you can use right now (connected and shared with you), with the --provider value to call them")
362
362
  .option("--no-login", "Fail instead of opening a browser sign-in when there is no HQ session")
363
363
  .action(async (opts) => {
364
- const token = await ensureCognitoIdToken(opts.login === false ? { interactive: false } : {});
364
+ const token = await ensureIntegrationIdToken(opts.login === false ? { interactive: false } : {});
365
365
  const companyUid = await getCompanyUid(token, opts.company);
366
366
  if (opts.usable) {
367
367
  const result = await fetchUsableApps(token, companyUid);
@@ -424,7 +424,7 @@ export function registerIntegrationsCommand(program) {
424
424
  .option("--describe <tool>", "Print the full input schema for one tool")
425
425
  .option("--json", "Machine-readable output")
426
426
  .action(async (opts) => {
427
- const token = await ensureCognitoIdToken();
427
+ const token = await ensureIntegrationIdToken();
428
428
  const companyUid = await getCompanyUid(token, opts.company);
429
429
  const connections = await fetchConnections(token, companyUid);
430
430
  const { connection, accountMatches } = selectToolConnection(connections, opts);
@@ -516,7 +516,7 @@ export function registerIntegrationsCommand(program) {
516
516
  .action(async (tool, opts) => {
517
517
  const waitMs = parseCallTimeoutMs(opts.timeout);
518
518
  const parsedArgs = await resolveCallArgs(opts);
519
- const token = await ensureCognitoIdToken();
519
+ const token = await ensureIntegrationIdToken();
520
520
  const companyUid = await getCompanyUid(token, opts.company);
521
521
  const { connection, accountMatches } = selectToolConnection(await fetchConnections(token, companyUid), opts);
522
522
  if (reportRevokedConnection(connection, { ...opts, fail: true }))
@@ -605,7 +605,7 @@ export function registerIntegrationsCommand(program) {
605
605
  .option("--company <slug>", "Company slug, e.g. indigo")
606
606
  .option("--json", "Machine-readable output")
607
607
  .action(async (queueId, opts) => {
608
- const token = await ensureCognitoIdToken();
608
+ const token = await ensureIntegrationIdToken();
609
609
  const companyUid = await getCompanyUid(token, opts.company);
610
610
  const connection = selectConnection(await fetchConnections(token, companyUid), opts);
611
611
  const res = await vaultApiFetch({
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { ensureSyncCoreBinary } from "../lib/sync-core/release.js";
4
+ import { ensureSyncCoreBinary, readSyncCorePin } from "../lib/sync-core/release.js";
5
5
  import { readCompanyRolloutFlag } from "../lib/sync-core/flag.js";
6
6
  import { reconcileSyncCore } from "../lib/sync-core/reconcile.js";
7
7
  import { engineOwnedManifestScopes, manifestScopeArg } from "../lib/sync-core/manifest-scopes.js";
@@ -59,6 +59,7 @@ export async function runSyncCoreReconcile(opts, d = defaultDeps()) {
59
59
  }),
60
60
  readRolloutFlag: (companyUid) => readCompanyRolloutFlag(companyUid, token, { env: d.env }),
61
61
  ensureBinary: (release) => ensureSyncCoreBinary({ home: d.home, platform: d.platform, arch: d.arch, release, env: d.env }),
62
+ readPin: () => readSyncCorePin({ env: d.env }),
62
63
  log: opts.json ? () => { } : d.out,
63
64
  });
64
65
  if (opts.json)
@@ -1,5 +1,5 @@
1
1
  import { type BotConfig } from "./config.js";
2
- export declare function executiveRiderEnabled(home?: string): boolean;
2
+ export declare function executiveEnabled(home?: string): boolean;
3
3
  export declare function isExecutiveBot(config: Pick<BotConfig, "name" | "kind" | "workerId">, home?: string): boolean;
4
4
  export declare function executiveStatus(config: Pick<BotConfig, "name" | "kind" | "workerId">, dir: string, home?: string): {
5
5
  is_executive: boolean;
@@ -1,21 +1,31 @@
1
- /** Shared Executive identity and the desktop's default-on rider preference. */
1
+ /**
2
+ * Shared Executive identity and its opt-in switch.
3
+ *
4
+ * The Executive (meeting rider, signal-to-lane, source report DMs) is off
5
+ * unless ~/.hq/menubar.json sets executive.enabled to true. It
6
+ * stays internal until the live end-to-end proof passes. The older
7
+ * executiveMeetingRider key is ignored whatever its value: the desktop beta
8
+ * saved executiveMeetingRider.enabled: true for users who never chose it.
9
+ */
2
10
  import * as fs from "node:fs";
3
11
  import * as path from "node:path";
4
12
  import * as os from "node:os";
5
13
  import { effectiveBotKind } from "./config.js";
6
14
  import { tailBotEvents } from "./events.js";
7
- export function executiveRiderEnabled(home = os.homedir()) {
15
+ export function executiveEnabled(home = os.homedir()) {
8
16
  try {
9
17
  const settings = JSON.parse(fs.readFileSync(path.join(home, ".hq/menubar.json"), "utf8"));
10
- return settings?.executiveMeetingRider?.enabled !== false;
18
+ return settings?.executive?.enabled === true;
11
19
  }
12
- catch (err) {
13
- return err.code === "ENOENT";
20
+ catch {
21
+ return false;
14
22
  }
15
23
  }
16
24
  export function isExecutiveBot(config, home = os.homedir()) {
17
25
  if (effectiveBotKind(config) !== "personal")
18
26
  return false;
27
+ if (!executiveEnabled(home))
28
+ return false;
19
29
  let name = "setup";
20
30
  try {
21
31
  const value = JSON.parse(fs.readFileSync(path.join(home, ".hq/executive.json"), "utf8"));
@@ -34,7 +44,7 @@ export function isExecutiveBot(config, home = os.homedir()) {
34
44
  export function executiveStatus(config, dir, home = os.homedir()) {
35
45
  const last = tailBotEvents(dir, (event) => event.event === "meeting.detected").at(-1);
36
46
  return {
37
- is_executive: isExecutiveBot(config, home), rider_enabled: executiveRiderEnabled(home),
47
+ is_executive: isExecutiveBot(config, home), rider_enabled: executiveEnabled(home),
38
48
  // The first desktop producer did not set source.
39
49
  detect_source_last: last ? (last.payload.source === "poll" ? "poll" : "desktop") : null,
40
50
  last_detected_at: last?.at ?? null,
@@ -9,7 +9,7 @@ import { getCompanyUid } from "../../../utils/vault-api.js";
9
9
  import { assertSafeSegment } from "../../hot-channels/landing.js";
10
10
  import { readManifestCompanies } from "../company-bind.js";
11
11
  import { emitBotEvent } from "../events.js";
12
- import { executiveRiderEnabled, isExecutiveBot } from "../executive-settings.js";
12
+ import { executiveEnabled, isExecutiveBot } from "../executive-settings.js";
13
13
  const RETENTION_MS = 24 * 60 * 60 * 1000;
14
14
  const ACTIVE = new Set(["joining", "recording", "in_call"]);
15
15
  export function detectIntervalSeconds(hqRoot) {
@@ -161,7 +161,7 @@ async function untilAborted(signal, work) {
161
161
  export function defaultMeetingDetectDeps(config, dir, log) {
162
162
  return {
163
163
  hqRoot: config.hqRoot, dir, log,
164
- enabled: () => isExecutiveBot(config) && executiveRiderEnabled(),
164
+ enabled: () => isExecutiveBot(config) && executiveEnabled(),
165
165
  companies: () => config.companySlug ? [config.companySlug] : readManifestCompanies(config.hqRoot),
166
166
  intervalSeconds: () => detectIntervalSeconds(config.hqRoot),
167
167
  list: async (company, signal) => {