@indigoai-us/hq-cli 5.116.0 → 5.117.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/command-catalog.generated.d.ts +59 -1
  3. package/dist/command-catalog.generated.js +77 -1
  4. package/dist/commands/agent-kit.d.ts +23 -3
  5. package/dist/commands/agent-kit.js +110 -13
  6. package/dist/commands/agent-probe.d.ts +15 -7
  7. package/dist/commands/agent-probe.js +59 -21
  8. package/dist/commands/bot.d.ts +140 -1
  9. package/dist/commands/bot.js +757 -22
  10. package/dist/commands/dm.d.ts +10 -0
  11. package/dist/commands/dm.js +80 -0
  12. package/dist/lib/agent-kit/fallback.d.ts +63 -0
  13. package/dist/lib/agent-kit/fallback.js +129 -0
  14. package/dist/lib/agent-kit/run/inbox.d.ts +18 -6
  15. package/dist/lib/agent-kit/run/inbox.js +38 -6
  16. package/dist/lib/agent-kit/run/mesh-listener.d.ts +22 -6
  17. package/dist/lib/agent-kit/run/mesh-listener.js +44 -8
  18. package/dist/lib/agent-kit/run/supervisor.d.ts +35 -0
  19. package/dist/lib/agent-kit/run/supervisor.js +85 -0
  20. package/dist/lib/bot/api.d.ts +51 -0
  21. package/dist/lib/bot/api.js +32 -0
  22. package/dist/lib/bot/daemon.d.ts +17 -0
  23. package/dist/lib/bot/daemon.js +44 -3
  24. package/dist/lib/bot/index.d.ts +4 -0
  25. package/dist/lib/bot/index.js +4 -0
  26. package/dist/lib/bot/inflight.d.ts +14 -0
  27. package/dist/lib/bot/local-config.d.ts +70 -0
  28. package/dist/lib/bot/local-config.js +147 -0
  29. package/dist/lib/bot/local-name.d.ts +54 -0
  30. package/dist/lib/bot/local-name.js +114 -0
  31. package/dist/lib/bot/run.d.ts +9 -0
  32. package/dist/lib/bot/run.js +117 -24
  33. package/dist/lib/bot/runnable.d.ts +51 -0
  34. package/dist/lib/bot/runnable.js +65 -0
  35. package/dist/lib/bot/self-heal.d.ts +52 -0
  36. package/dist/lib/bot/self-heal.js +79 -0
  37. package/dist/lib/bot/split.d.ts +32 -0
  38. package/dist/lib/bot/split.js +241 -0
  39. package/dist/lib/mesh/live/daemon/credentials.d.ts +27 -0
  40. package/dist/lib/mesh/live/daemon/credentials.js +95 -0
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,131 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.117.1] — 2026-09-16
6
+
7
+ ### Fixed
8
+
9
+ - `hq agent probe` works for external agents. The work-mesh check no longer
10
+ reads the owner-only company roster (which answered "Not found" to an agent);
11
+ it passes when the realtime credential vend names the agent, the agent's own
12
+ inbox answers, and this host's heartbeat and mesh listener are live. The DM
13
+ check reads the probe message back from the agent's inbox instead of the
14
+ human-only thread view that refused agents. Output format is unchanged.
15
+ - The agent kit's mesh listener stays connected. It now asks for the personal
16
+ realtime credentials that cover the agent's own doorbell topics (the company
17
+ presence credentials it used made the connection drop before subscribing),
18
+ subscribes only to topics those credentials grant, and polls the inbox
19
+ immediately when an inbox doorbell rings.
20
+ - Inbox poll failures are diagnosable: the log names the method, full URL,
21
+ status and the first 300 characters of the response, and the inbox URL keeps
22
+ any base path from `apiBaseUrl` without doubling `/v1`.
23
+ - `hq dm inbox` works for agent identities by reading the agent inbox;
24
+ `hq dm thread` tells agents to use `hq dm inbox` instead of failing with a
25
+ server refusal.
26
+ - `hq agent kit install` works on hosts with no systemd user session or
27
+ launchd: it starts the services under one background `hq agent kit run all`
28
+ supervisor (restarting any service that exits), records its pid in
29
+ `~/.hq-agent/kit.pid`, and prints the `@reboot` crontab line that keeps it
30
+ running after a restart. `kit status` and `kit uninstall` understand this
31
+ mode.
32
+
33
+ ## [5.117.0] — 2026-09-16
34
+
35
+ ### Added
36
+
37
+ - Your local bots can come back after a reinstall. A bot is an identity HQ
38
+ keeps for you plus the state that runs it on one computer; wiping that
39
+ computer's HQ folder, or moving to a new Mac, used to leave every bot dead
40
+ with no way back — making a new one was refused, because the old one still
41
+ existed. Now `hq bot restore` brings back every bot you own that is missing
42
+ from this computer, with its own settings, memory folder and startup agent,
43
+ keeping the same identity and the same conversation. `hq bot adopt <name>`
44
+ does one bot, `hq bot list --remote` shows every bot you own and which of
45
+ them are set up here, and trying to create a bot you already own now says so
46
+ and points at adopting it instead. Bots made before this shipped come back on
47
+ sensible defaults, and HQ tells you it did that.
48
+
49
+ ### Fixed
50
+
51
+ - `hq bot restore` no longer brings back a bot this Mac cannot run. A company
52
+ bot — one that acts for a company rather than as you — was being set up here
53
+ as a personal bot: it got credentials, a folder, a memory folder and a startup
54
+ entry, was counted as restored with nothing reported as failed, and then died
55
+ at every start and at every login, while the Mac offered "check that Claude
56
+ Code is signed in" as the remedy, which could never help. Restore, `hq bot
57
+ adopt` and `hq bot list --remote` now ask exactly the question the bot's own
58
+ startup asks, before anything is written or any password is rotated. A bot
59
+ that cannot run here is listed plainly, counted as skipped rather than
60
+ restored, and refused by adopt with one sentence saying where it does run —
61
+ and nothing is left behind on this computer.
62
+ - Bringing a bot back no longer leaves a second copy of it. A bot you created
63
+ here as `qa-x` is `qa-x-<your suffix>` in HQ, and bringing it back built a
64
+ whole second folder under that longer name: a second startup entry, a second
65
+ memory folder, and the original folder left behind holding the live password
66
+ of a bot nothing could reach any more. HQ now recognises the folder a bot
67
+ already has on this computer — by the identity inside it, even when a wipe
68
+ left only its credentials — and repairs that one in place. A bot that is not
69
+ here at all comes back under the name you created it with. If that name
70
+ already belongs to a different bot, both are kept, under names that tell them
71
+ apart.
72
+ - `hq bot list --remote --json` now says `ok: true` when it worked. Every
73
+ failure said `ok: false`, and the success document had no `ok` at all, so
74
+ anything that learned to check it read a good listing as a failure.
75
+ - A bot's long answer now reaches you, in several messages. HQ Cloud refuses any
76
+ single message over 4000 characters, and nothing split one up: an answer over
77
+ that length was refused, saved, and sent again unchanged on every check — a
78
+ test machine recorded the same rejection 231 times in eight minutes while the
79
+ person saw no reply and no error at all, and the bot's 44 seconds of work was
80
+ thrown away. A reply that is too long is now cut into numbered parts — on
81
+ paragraph breaks where it can, on sentences otherwise, and never leaving a
82
+ block of code half-open — and posted in order. If something goes wrong partway
83
+ through, only the parts you have not seen are sent again. And if a message
84
+ truly cannot be delivered, the bot stops after five tries and tells you so in
85
+ one short line, instead of asking the server to do the impossible for as long
86
+ as it runs.
87
+ - A bot's startup entry no longer outlives the bot. When a bot's local setup was
88
+ gone, the startup entry macOS keeps for it relaunched it every five seconds,
89
+ forever, filling its log with the same failure (one test machine recorded 267
90
+ of them in a row). HQ now says once what happened, tells you how to bring the
91
+ bot back, removes the startup entry it no longer needs, and stops cleanly so
92
+ nothing relaunches it. This works when macOS itself is the one running the
93
+ bot, which is the case that matters: the first version did its tidying in an
94
+ order that had macOS shut the bot down before it could finish, so the startup
95
+ entry survived and brought the same dead bot back at every login, and the
96
+ explanation never reached the bot's log. HQ now writes the explanation and
97
+ removes the entry first, and lets go of itself last.
98
+
99
+ - You can remove a bot whose settings this computer has already lost. A wiped
100
+ HQ folder could leave a bot half-here — a folder and a startup entry, no
101
+ settings — and nothing could clear it: removing it said there was no such
102
+ bot and suggested creating one, and creating one said you already had it.
103
+ `hq bot rm <name>` now clears whatever is actually left on this computer, and
104
+ `hq bot rm <name> --remote` also removes the bot HQ still holds under that
105
+ name. It checks with HQ before it deletes anything local, so a service that
106
+ cannot answer leaves nothing half-done.
107
+
108
+ - Removing a bot now tells the truth when HQ only gets part of the way. When
109
+ HQ's own teardown of a bot's cloud identity stopped half-way, the
110
+ machine-readable form said the removal had failed but gave no reason and no
111
+ explanation, reported success to anything checking the exit code, and printed
112
+ the raw cloud error — including internal infrastructure addresses — straight
113
+ at you. It now says one plain sentence, names the reason in the same
114
+ vocabulary as everything else, and exits non-zero, and no internal detail is
115
+ repeated anywhere. It also stops making you run the command twice for
116
+ nothing: when the part HQ could not finish would fail the same way every
117
+ time, the bot is removed from this computer and you are told the leftover is
118
+ HQ's to clear; when it is something HQ may yet finish, your settings are kept
119
+ exactly as they were so running the same command again picks the job back up.
120
+
121
+ - Bringing bots back now fails in plain language when HQ Cloud is too old for
122
+ it. `hq bot adopt`, `hq bot restore`, and `hq bot list --remote` all ask HQ
123
+ which bots you own; against an HQ Cloud without that capability, the raw
124
+ network error was printed at you, and the machine-readable form printed
125
+ nothing at all — so the desktop app had nothing it could read. Each now says
126
+ one plain sentence, and `--json` always returns a result naming the reason,
127
+ so the app can quietly fall back to showing the bots that are on this
128
+ computer.
129
+
5
130
  ## [5.116.0] — 2026-09-16
6
131
 
7
132
  ### Added
@@ -4217,7 +4217,7 @@ export declare const COMMAND_CATALOG: readonly [{
4217
4217
  readonly subcommands: readonly [];
4218
4218
  }, {
4219
4219
  readonly name: "run";
4220
- readonly description: "Run one kit service in the foreground (sync|mesh|inbox|heartbeat)";
4220
+ readonly description: "Run one kit service in the foreground (sync|mesh|inbox|heartbeat), or `all` to supervise every service in one process";
4221
4221
  readonly aliases: readonly [];
4222
4222
  readonly hidden: false;
4223
4223
  readonly usage: "[options] <service>";
@@ -6374,6 +6374,61 @@ export declare const COMMAND_CATALOG: readonly [{
6374
6374
  readonly usage: "[options]";
6375
6375
  readonly arguments: readonly [];
6376
6376
  readonly options: readonly [{
6377
+ readonly flags: "--remote";
6378
+ readonly description: "List the local bots your HQ account owns instead, with a here column saying which are set up on this computer";
6379
+ }, {
6380
+ readonly flags: "--json";
6381
+ readonly description: "Print machine-readable JSON";
6382
+ }];
6383
+ readonly subcommands: readonly [];
6384
+ }, {
6385
+ readonly name: "adopt";
6386
+ readonly description: "Bring a bot you already own back to this computer: new machine credentials, its saved settings, worker folder, and startup agent";
6387
+ readonly aliases: readonly [];
6388
+ readonly hidden: false;
6389
+ readonly usage: "[options] <name>";
6390
+ readonly arguments: readonly [{
6391
+ readonly name: "name";
6392
+ readonly required: true;
6393
+ readonly variadic: false;
6394
+ }];
6395
+ readonly options: readonly [{
6396
+ readonly flags: "--runtime <runtime>";
6397
+ readonly description: "Model CLI to run it with, overriding its saved one: claude|codex|grok";
6398
+ }, {
6399
+ readonly flags: "--model <model>";
6400
+ readonly description: "Model override passed to the runtime CLI, replacing its saved one";
6401
+ }, {
6402
+ readonly flags: "--no-daemon";
6403
+ readonly description: "Do not install the launchd agent (macOS)";
6404
+ }, {
6405
+ readonly flags: "--no-start";
6406
+ readonly description: "Bring it back without starting it";
6407
+ }, {
6408
+ readonly flags: "--json";
6409
+ readonly description: "Print machine-readable JSON";
6410
+ }];
6411
+ readonly subcommands: readonly [];
6412
+ }, {
6413
+ readonly name: "restore";
6414
+ readonly description: "Bring back every local bot your HQ account owns that is not set up on this computer (after a reinstall, or on a new Mac)";
6415
+ readonly aliases: readonly [];
6416
+ readonly hidden: false;
6417
+ readonly usage: "[options]";
6418
+ readonly arguments: readonly [];
6419
+ readonly options: readonly [{
6420
+ readonly flags: "--all";
6421
+ readonly description: "Also re-issue credentials and reinstall the startup agent for the bots already set up here";
6422
+ }, {
6423
+ readonly flags: "--dry-run";
6424
+ readonly description: "Show what would be brought back, change nothing";
6425
+ }, {
6426
+ readonly flags: "--no-daemon";
6427
+ readonly description: "Do not install the launchd agents (macOS)";
6428
+ }, {
6429
+ readonly flags: "--no-start";
6430
+ readonly description: "Bring them back without starting them";
6431
+ }, {
6377
6432
  readonly flags: "--json";
6378
6433
  readonly description: "Print machine-readable JSON";
6379
6434
  }];
@@ -6488,6 +6543,9 @@ export declare const COMMAND_CATALOG: readonly [{
6488
6543
  readonly options: readonly [{
6489
6544
  readonly flags: "-y, --yes";
6490
6545
  readonly description: "Skip the confirmation prompt";
6546
+ }, {
6547
+ readonly flags: "--remote";
6548
+ readonly description: "When the bot's local settings are already gone, also delete the bot HQ still has under that name";
6491
6549
  }, {
6492
6550
  readonly flags: "--json";
6493
6551
  readonly description: "Print machine-readable JSON";
@@ -5455,7 +5455,7 @@ export const COMMAND_CATALOG = [
5455
5455
  },
5456
5456
  {
5457
5457
  "name": "run",
5458
- "description": "Run one kit service in the foreground (sync|mesh|inbox|heartbeat)",
5458
+ "description": "Run one kit service in the foreground (sync|mesh|inbox|heartbeat), or `all` to supervise every service in one process",
5459
5459
  "aliases": [],
5460
5460
  "hidden": false,
5461
5461
  "usage": "[options] <service>",
@@ -8237,6 +8237,78 @@ export const COMMAND_CATALOG = [
8237
8237
  "usage": "[options]",
8238
8238
  "arguments": [],
8239
8239
  "options": [
8240
+ {
8241
+ "flags": "--remote",
8242
+ "description": "List the local bots your HQ account owns instead, with a here column saying which are set up on this computer"
8243
+ },
8244
+ {
8245
+ "flags": "--json",
8246
+ "description": "Print machine-readable JSON"
8247
+ }
8248
+ ],
8249
+ "subcommands": []
8250
+ },
8251
+ {
8252
+ "name": "adopt",
8253
+ "description": "Bring a bot you already own back to this computer: new machine credentials, its saved settings, worker folder, and startup agent",
8254
+ "aliases": [],
8255
+ "hidden": false,
8256
+ "usage": "[options] <name>",
8257
+ "arguments": [
8258
+ {
8259
+ "name": "name",
8260
+ "required": true,
8261
+ "variadic": false
8262
+ }
8263
+ ],
8264
+ "options": [
8265
+ {
8266
+ "flags": "--runtime <runtime>",
8267
+ "description": "Model CLI to run it with, overriding its saved one: claude|codex|grok"
8268
+ },
8269
+ {
8270
+ "flags": "--model <model>",
8271
+ "description": "Model override passed to the runtime CLI, replacing its saved one"
8272
+ },
8273
+ {
8274
+ "flags": "--no-daemon",
8275
+ "description": "Do not install the launchd agent (macOS)"
8276
+ },
8277
+ {
8278
+ "flags": "--no-start",
8279
+ "description": "Bring it back without starting it"
8280
+ },
8281
+ {
8282
+ "flags": "--json",
8283
+ "description": "Print machine-readable JSON"
8284
+ }
8285
+ ],
8286
+ "subcommands": []
8287
+ },
8288
+ {
8289
+ "name": "restore",
8290
+ "description": "Bring back every local bot your HQ account owns that is not set up on this computer (after a reinstall, or on a new Mac)",
8291
+ "aliases": [],
8292
+ "hidden": false,
8293
+ "usage": "[options]",
8294
+ "arguments": [],
8295
+ "options": [
8296
+ {
8297
+ "flags": "--all",
8298
+ "description": "Also re-issue credentials and reinstall the startup agent for the bots already set up here"
8299
+ },
8300
+ {
8301
+ "flags": "--dry-run",
8302
+ "description": "Show what would be brought back, change nothing"
8303
+ },
8304
+ {
8305
+ "flags": "--no-daemon",
8306
+ "description": "Do not install the launchd agents (macOS)"
8307
+ },
8308
+ {
8309
+ "flags": "--no-start",
8310
+ "description": "Bring them back without starting them"
8311
+ },
8240
8312
  {
8241
8313
  "flags": "--json",
8242
8314
  "description": "Print machine-readable JSON"
@@ -8387,6 +8459,10 @@ export const COMMAND_CATALOG = [
8387
8459
  "flags": "-y, --yes",
8388
8460
  "description": "Skip the confirmation prompt"
8389
8461
  },
8462
+ {
8463
+ "flags": "--remote",
8464
+ "description": "When the bot's local settings are already gone, also delete the bot HQ still has under that name"
8465
+ },
8390
8466
  {
8391
8467
  "flags": "--json",
8392
8468
  "description": "Print machine-readable JSON"
@@ -10,7 +10,10 @@
10
10
  * `install` renders one user-level unit per service (LaunchAgent on macOS,
11
11
  * systemd --user on Linux), each of which just runs
12
12
  * `node hq agent kit run <service>`; the Docker image runs the same four
13
- * commands. Logs: ~/.hq-agent/logs/<service>.log. Skills:
13
+ * commands. Hosts where the service manager is unusable (no systemd user
14
+ * session bus, permission refused, launchd unavailable) fall back to one
15
+ * detached `hq agent kit run all` supervisor (pid in ~/.hq-agent/kit.pid,
16
+ * output in ~/.hq-agent/logs/kit.log). Logs: ~/.hq-agent/logs/<service>.log. Skills:
14
17
  * ~/.hq-agent/skills/<name>/SKILL.md.
15
18
  */
16
19
  import { Command } from "commander";
@@ -19,6 +22,7 @@ import { type KitConfig } from "../lib/agent-kit/kit-config.js";
19
22
  import { type KitLogger } from "../lib/agent-kit/log.js";
20
23
  import { type AgentKitPaths } from "../lib/agent-kit/paths.js";
21
24
  import { type KitService } from "../lib/agent-kit/services.js";
25
+ import { type DetachedSpawn, type KillFn } from "../lib/agent-kit/fallback.js";
22
26
  import { type ServiceHostPaths, type ServiceManagerDeps, type ServiceSetResult } from "../lib/service-manager/index.js";
23
27
  export declare class KitError extends Error {
24
28
  constructor(message: string);
@@ -32,12 +36,25 @@ export interface KitInstallOptions {
32
36
  inboxAck?: boolean;
33
37
  activate?: boolean;
34
38
  }
39
+ export interface KitFallbackInfo {
40
+ mode: "fallback";
41
+ pid: number;
42
+ pidFile: string;
43
+ logPath: string;
44
+ /** Add to `crontab -e` so the supervisor survives a reboot. */
45
+ crontab: string;
46
+ }
35
47
  export interface KitInstallResult {
36
48
  config: KitConfig;
37
49
  skills: string[];
38
50
  services: ServiceSetResult;
51
+ fallback?: KitFallbackInfo;
52
+ }
53
+ export interface KitFallbackDeps {
54
+ spawn?: DetachedSpawn;
55
+ kill?: KillFn;
39
56
  }
40
- export declare function installKit(paths: AgentKitPaths, opts: KitInstallOptions, host: ServiceHostPaths, deps?: ServiceManagerDeps): KitInstallResult;
57
+ export declare function installKit(paths: AgentKitPaths, opts: KitInstallOptions, host: ServiceHostPaths, deps?: ServiceManagerDeps, fallbackDeps?: KitFallbackDeps): KitInstallResult;
41
58
  export declare function formatServiceSet(result: ServiceSetResult): string[];
42
59
  /** Shared runtime for `kit run <service>`. */
43
60
  export interface KitRuntime {
@@ -47,7 +64,10 @@ export interface KitRuntime {
47
64
  getToken: () => Promise<string>;
48
65
  log: KitLogger;
49
66
  }
50
- export declare function buildKitRuntime(service: KitService): KitRuntime;
67
+ export declare function buildKitRuntime(service: KitService | "all"): KitRuntime;
51
68
  export declare function runKitService(service: KitService, rt: KitRuntime): Promise<void>;
69
+ /** `kit run all`: supervise the four services as child processes with restart backoff. */
70
+ export declare function runAllKitServices(rt: KitRuntime, host: ServiceHostPaths): Promise<void>;
71
+ export declare function formatFallbackStatus(paths: AgentKitPaths, kill?: KillFn): string[] | null;
52
72
  export declare function registerAgentKitCommand(agent: Command): void;
53
73
  //# sourceMappingURL=agent-kit.d.ts.map
@@ -10,7 +10,10 @@
10
10
  * `install` renders one user-level unit per service (LaunchAgent on macOS,
11
11
  * systemd --user on Linux), each of which just runs
12
12
  * `node hq agent kit run <service>`; the Docker image runs the same four
13
- * commands. Logs: ~/.hq-agent/logs/<service>.log. Skills:
13
+ * commands. Hosts where the service manager is unusable (no systemd user
14
+ * session bus, permission refused, launchd unavailable) fall back to one
15
+ * detached `hq agent kit run all` supervisor (pid in ~/.hq-agent/kit.pid,
16
+ * output in ~/.hq-agent/logs/kit.log). Logs: ~/.hq-agent/logs/<service>.log. Skills:
14
17
  * ~/.hq-agent/skills/<name>/SKILL.md.
15
18
  */
16
19
  import chalk from "chalk";
@@ -25,7 +28,11 @@ import { agentKitPaths } from "../lib/agent-kit/paths.js";
25
28
  import { isKitService, KIT_SERVICES, kitServiceSpecs, } from "../lib/agent-kit/services.js";
26
29
  import { writeKitSkills } from "../lib/agent-kit/skills.js";
27
30
  import { runHeartbeatLoop } from "../lib/agent-kit/run/heartbeat.js";
28
- import { runInboxLoop } from "../lib/agent-kit/run/inbox.js";
31
+ import { pollInboxOnce, runInboxLoop } from "../lib/agent-kit/run/inbox.js";
32
+ import { superviseKitServices } from "../lib/agent-kit/run/supervisor.js";
33
+ import { fallbackStatus, rebootCrontabLine, shouldUseFallback, startFallback, stopFallback, } from "../lib/agent-kit/fallback.js";
34
+ import { serviceLogPath } from "../lib/agent-kit/paths.js";
35
+ import { spawn } from "node:child_process";
29
36
  import { startMeshListener } from "../lib/agent-kit/run/mesh-listener.js";
30
37
  import { runSyncLoop } from "../lib/agent-kit/run/sync.js";
31
38
  import { resolveHqBinary, resolveNodeBinary, } from "../lib/mesh/live/daemon/install.js";
@@ -57,7 +64,7 @@ export function resolveServiceHost(paths) {
57
64
  hqBinary: resolveHqBinary(),
58
65
  };
59
66
  }
60
- export function installKit(paths, opts, host, deps = {}) {
67
+ export function installKit(paths, opts, host, deps = {}, fallbackDeps = {}) {
61
68
  requireExternalCreds(paths);
62
69
  ensureKitDirs(paths);
63
70
  const existing = fs.existsSync(paths.kitConfigPath)
@@ -72,10 +79,20 @@ export function installKit(paths, opts, host, deps = {}) {
72
79
  fs.mkdirSync(config.hqRoot, { recursive: true, mode: 0o700 });
73
80
  writeKitConfig(paths, config);
74
81
  const skills = writeKitSkills(paths);
82
+ const activate = opts.activate ?? deps.activate ?? true;
75
83
  const services = installServices(kitServiceSpecs(paths), host, {
76
84
  ...deps,
77
- activate: opts.activate ?? deps.activate ?? true,
85
+ activate,
78
86
  });
87
+ if (shouldUseFallback(services, activate)) {
88
+ const started = startFallback(paths, host, fallbackDeps);
89
+ return {
90
+ config,
91
+ skills,
92
+ services,
93
+ fallback: { mode: "fallback", ...started, crontab: rebootCrontabLine(paths, host) },
94
+ };
95
+ }
79
96
  return { config, skills, services };
80
97
  }
81
98
  export function formatServiceSet(result) {
@@ -151,6 +168,16 @@ export async function runKitService(service, rt) {
151
168
  refreshMs: rt.config.meshRefreshMs,
152
169
  getToken: rt.getToken,
153
170
  log: rt.log,
171
+ // An inbox doorbell means "poll now" rather than waiting for the loop.
172
+ onInboxDoorbell: () => pollInboxOnce({
173
+ paths: rt.paths,
174
+ agentUid: rt.creds.entityUid,
175
+ apiBaseUrl: rt.creds.apiBaseUrl,
176
+ pollMs: rt.config.inboxPollMs,
177
+ ack: rt.config.inboxAck,
178
+ getToken: rt.getToken,
179
+ log: rt.log,
180
+ }),
154
181
  });
155
182
  const stop = () => {
156
183
  void handle.stop().then(() => process.exit(0));
@@ -164,6 +191,45 @@ export async function runKitService(service, rt) {
164
191
  }
165
192
  }
166
193
  }
194
+ /** `kit run all`: supervise the four services as child processes with restart backoff. */
195
+ export async function runAllKitServices(rt, host) {
196
+ rt.log("info", `supervisor starting ${KIT_SERVICES.join(",")} agent=${rt.creds.entityUid} cli=${CLI_VERSION}`);
197
+ const handle = superviseKitServices({
198
+ services: KIT_SERVICES,
199
+ log: rt.log,
200
+ spawnService: (service) => {
201
+ const fd = fs.openSync(serviceLogPath(rt.paths, service), "a", 0o600);
202
+ try {
203
+ return spawn(host.nodeBinary, [host.hqBinary, "agent", "kit", "run", service], {
204
+ stdio: ["ignore", fd, fd],
205
+ env: process.env,
206
+ cwd: rt.paths.agentDir,
207
+ });
208
+ }
209
+ finally {
210
+ fs.closeSync(fd);
211
+ }
212
+ },
213
+ });
214
+ await new Promise((resolve) => {
215
+ const stop = (signal) => {
216
+ rt.log("info", `supervisor stopping (${signal})`);
217
+ handle.stop("SIGTERM");
218
+ resolve();
219
+ };
220
+ process.once("SIGTERM", () => stop("SIGTERM"));
221
+ process.once("SIGINT", () => stop("SIGINT"));
222
+ });
223
+ }
224
+ export function formatFallbackStatus(paths, kill) {
225
+ const st = fallbackStatus(paths, kill);
226
+ if (!st)
227
+ return null;
228
+ const state = st.running
229
+ ? chalk.green(`running (pid ${st.pid})`)
230
+ : chalk.yellow(st.pid === null ? "pidfile unreadable" : `not running (stale pid ${st.pid})`);
231
+ return [` mode: fallback supervisor (hq agent kit run all) ${state}${chalk.dim(` ${st.pidFile}`)}`];
232
+ }
167
233
  function fail(err) {
168
234
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
169
235
  process.exit(1);
@@ -191,9 +257,19 @@ export function registerAgentKitCommand(agent) {
191
257
  console.log(` hq root: ${result.config.hqRoot}`);
192
258
  console.log(` logs: ${paths.logsDir}`);
193
259
  console.log(` skills: ${paths.skillsDir} (${result.skills.length} skills)`);
194
- console.log(" services:");
195
- for (const line of formatServiceSet(result.services))
196
- console.log(line);
260
+ if (result.fallback) {
261
+ console.log(chalk.yellow(" services: service manager unavailable — running in fallback mode " +
262
+ `(hq agent kit run all, pid ${result.fallback.pid})`));
263
+ console.log(` pidfile: ${result.fallback.pidFile}`);
264
+ console.log(` kit log: ${result.fallback.logPath}`);
265
+ console.log(" To restart it after a reboot, add this line with `crontab -e`:");
266
+ console.log(` ${result.fallback.crontab}`);
267
+ }
268
+ else {
269
+ console.log(" services:");
270
+ for (const line of formatServiceSet(result.services))
271
+ console.log(line);
272
+ }
197
273
  console.log("");
198
274
  console.log("Next: hq agent probe");
199
275
  }
@@ -208,13 +284,19 @@ export function registerAgentKitCommand(agent) {
208
284
  .action((opts) => {
209
285
  try {
210
286
  const paths = agentKitPaths(os.homedir(), process.env);
287
+ const fallback = fallbackStatus(paths);
211
288
  const result = servicesStatus(kitServiceSpecs(paths), resolveServiceHost(paths));
212
289
  if (opts.json) {
213
- console.log(JSON.stringify({ ok: true, ...result }, null, 2));
290
+ console.log(JSON.stringify({ ok: true, ...result, ...(fallback ? { fallback } : {}) }, null, 2));
214
291
  return;
215
292
  }
216
- for (const line of formatServiceSet(result))
217
- console.log(line);
293
+ const fallbackLines = formatFallbackStatus(paths);
294
+ if (fallbackLines)
295
+ for (const line of fallbackLines)
296
+ console.log(line);
297
+ else
298
+ for (const line of formatServiceSet(result))
299
+ console.log(line);
218
300
  }
219
301
  catch (err) {
220
302
  fail(err);
@@ -227,11 +309,15 @@ export function registerAgentKitCommand(agent) {
227
309
  .action((opts) => {
228
310
  try {
229
311
  const paths = agentKitPaths(os.homedir(), process.env);
312
+ const fallback = stopFallback(paths);
230
313
  const result = uninstallServices(kitServiceSpecs(paths), resolveServiceHost(paths));
231
314
  if (opts.json) {
232
- console.log(JSON.stringify({ ok: true, ...result }, null, 2));
315
+ console.log(JSON.stringify({ ok: true, ...result, fallback }, null, 2));
233
316
  return;
234
317
  }
318
+ if (fallback.pid !== null) {
319
+ console.log(` fallback supervisor pid ${fallback.pid} ${fallback.killed ? "stopped" : "was not running"}; pidfile removed`);
320
+ }
235
321
  console.log(chalk.green("HQ agent kit services removed."));
236
322
  for (const line of formatServiceSet(result))
237
323
  console.log(line);
@@ -242,10 +328,21 @@ export function registerAgentKitCommand(agent) {
242
328
  });
243
329
  kit
244
330
  .command("run <service>")
245
- .description(`Run one kit service in the foreground (${KIT_SERVICES.join("|")})`)
331
+ .description(`Run one kit service in the foreground (${KIT_SERVICES.join("|")}), or \`all\` to supervise every service in one process`)
246
332
  .action(async (service) => {
333
+ if (service === "all") {
334
+ try {
335
+ const paths = agentKitPaths(os.homedir(), process.env);
336
+ const rt = buildKitRuntime("all");
337
+ await runAllKitServices(rt, resolveServiceHost(paths));
338
+ process.exit(0);
339
+ }
340
+ catch (err) {
341
+ fail(err);
342
+ }
343
+ }
247
344
  if (!isKitService(service)) {
248
- console.error(chalk.red(`Unknown kit service "${service}". Expected one of: ${KIT_SERVICES.join(", ")}`));
345
+ console.error(chalk.red(`Unknown kit service "${service}". Expected one of: all, ${KIT_SERVICES.join(", ")}`));
249
346
  process.exit(1);
250
347
  }
251
348
  try {
@@ -4,9 +4,14 @@
4
4
  *
5
5
  * whoami mint as the machine identity; token names this agent
6
6
  * team-sync `hq sync pull --all` succeeds and the company folder exists
7
- * work-mesh realtime credentials vend for this agent and the roster row
8
- * shows presence online/stale (heartbeat landed)
9
- * dm send a DM to self via the notify surface and read it back
7
+ * work-mesh the personal (contract-2) realtime vend names this agent, the
8
+ * agent-authorized inbox route answers 200, and this host's own
9
+ * presence is live: a fresh last-heartbeat.json plus an ok
10
+ * component-mesh stamp from the doorbell listener. (The company
11
+ * roster route is owner/admin-only and 404s for agt_ callers.)
12
+ * dm send a DM to self via POST /v1/notify/dm and read it back
13
+ * through GET /v1/agents/{uid}/inbox (the agent read surface;
14
+ * /v1/notify/thread is intentionally closed to agents)
10
15
  * secrets `GET /secrets/{companyUid}` answers 200
11
16
  *
12
17
  * Output is the exact shape the console recipes promise: one
@@ -18,6 +23,7 @@
18
23
  * the kit's last heartbeat.
19
24
  */
20
25
  import { Command } from "commander";
26
+ import { type InboxFetchResult } from "../lib/agent-kit/run/inbox.js";
21
27
  import { type AgentKitPaths } from "../lib/agent-kit/paths.js";
22
28
  import type { ExternalMachineCreds } from "../lib/agent-kit/creds.js";
23
29
  export interface ProbeCheck {
@@ -34,6 +40,8 @@ export interface ProbeResult {
34
40
  }
35
41
  export declare const DM_ROUNDTRIP_TIMEOUT_MS = 20000;
36
42
  export declare const DM_ROUNDTRIP_POLL_MS = 2000;
43
+ /** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
44
+ export declare const PRESENCE_FRESH_SECONDS = 180;
37
45
  export interface ProbeDeps {
38
46
  paths: AgentKitPaths;
39
47
  creds: ExternalMachineCreds;
@@ -44,11 +52,9 @@ export interface ProbeDeps {
44
52
  vendRealtime: (token: string) => Promise<{
45
53
  actorUid: string;
46
54
  }>;
47
- listRoster: (token: string, companyUid: string) => Promise<Array<Record<string, unknown>>>;
55
+ /** GET /v1/agents/{self}/inbox agent-authorized, self-scoped. */
56
+ readInbox: (token: string) => Promise<InboxFetchResult>;
48
57
  sendDm: (token: string, toUid: string, body: string) => Promise<void>;
49
- readThread: (token: string, withUid: string) => Promise<Array<{
50
- body: string;
51
- }>>;
52
58
  listSecrets: (token: string, companyUid: string) => Promise<number>;
53
59
  postResult: (token: string, result: ProbeResult) => Promise<number>;
54
60
  now?: () => Date;
@@ -58,6 +64,8 @@ export interface ProbeDeps {
58
64
  }
59
65
  export declare function heartbeatAgeSeconds(paths: Pick<AgentKitPaths, "lastHeartbeatPath">, now?: () => Date): number | null;
60
66
  export declare function runProbe(deps: ProbeDeps): Promise<ProbeResult>;
67
+ /** True when any inbox item (whatever its channel shape) carries the nonce. */
68
+ export declare function inboxContainsNonce(body: unknown, nonce: string): boolean;
61
69
  export declare function formatHeartbeatAge(age: number | null): string;
62
70
  /**
63
71
  * Recipe contract: `PASS <check> <detail>` / `FAIL <check> <detail>` per