@indigoai-us/hq-cli 5.167.0 → 5.169.0

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 (37) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/command-catalog.generated.d.ts +88 -1
  3. package/dist/command-catalog.generated.js +112 -1
  4. package/dist/commands/daemon.d.ts +2 -0
  5. package/dist/commands/daemon.js +79 -22
  6. package/dist/commands/lanes.js +146 -31
  7. package/dist/commands/mesh.js +13 -19
  8. package/dist/commands/outpost-pruner.d.ts +3 -0
  9. package/dist/commands/outpost-pruner.js +125 -0
  10. package/dist/lib/daemon/catalog.d.ts +11 -1
  11. package/dist/lib/daemon/catalog.js +31 -6
  12. package/dist/lib/daemon/install.d.ts +7 -0
  13. package/dist/lib/daemon/install.js +18 -2
  14. package/dist/lib/daemon/legacy.js +6 -1
  15. package/dist/lib/daemon/machine-type.d.ts +4 -1
  16. package/dist/lib/daemon/machine-type.js +29 -9
  17. package/dist/lib/daemon/migrate.js +2 -2
  18. package/dist/lib/daemon/remote-control.d.ts +32 -9
  19. package/dist/lib/daemon/remote-control.js +55 -11
  20. package/dist/lib/daemon/run.d.ts +6 -0
  21. package/dist/lib/daemon/run.js +43 -2
  22. package/dist/lib/daemon/tools.d.ts +52 -0
  23. package/dist/lib/daemon/tools.js +95 -0
  24. package/dist/lib/daemon/updater.d.ts +23 -15
  25. package/dist/lib/daemon/updater.js +97 -60
  26. package/dist/lib/lanes/mesh-events.d.ts +4 -0
  27. package/dist/lib/lanes/mesh-events.js +72 -4
  28. package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
  29. package/dist/lib/lanes/outpost-pruner-install.js +226 -0
  30. package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
  31. package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
  32. package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
  33. package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
  34. package/dist/lib/mesh/live/session-events-client.js +3 -2
  35. package/dist/lib/mesh/project-board.d.ts +7 -0
  36. package/dist/lib/mesh/project-board.js +29 -1
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.169.0] — 2026-09-24
6
+
7
+ ### Fixed
8
+
9
+ - `hq lanes create` and `hq lanes story <add|start|block|done>` keep story work
10
+ local when an existing Work Mesh Board rejects a missing story. The
11
+ `story_warning` identifies the project and story and suggests:
12
+ `hq mesh project ensure <projectId> --company <slug> --stories-file prd.json`.
13
+
14
+ ## [5.168.0] — 2026-09-24
15
+
16
+ ### Fixed
17
+
18
+ - `hq mesh project set --stories-file` keeps live Board story status and
19
+
20
+ - `hq mesh` ignores nonpositive, malformed, and expired `Retry-After` hints.
21
+
22
+ ### Added
23
+
24
+ - `hq daemon` keeps the hq CLI, Claude, Codex and Grok up to date on every machine type, desktops included, every six hours. It updates the login user's own copy of each tool through that tool's npm package or its own `update` command, and leaves a copy the user cannot write (for example one root installed in /usr) alone. On outposts the daemon also runs Codex remote control (the box's Codex runner) next to Claude remote control, keeps the session name the box's runner used, and restarts either one onto a newly installed Claude or Codex once the session has been quiet for 10 minutes, or after 2 hours at the latest. `hq daemon install` never sets the daemon up to run as root: a box runs it as the login user from `/etc/hq/machine-type`, the outpost runner unit, `--user` or `sudo`, and an outpost without the marker file is recognised from its runner unit. `hq daemon migrate --apply` also takes over the outpost runner watchdogs and the Codex update timer.
25
+ - Added outpost worktree cleanup commands for dry runs and pruning clean, aged
26
+ worktrees. Lane, session, and process references keep their worktrees. Pressure
27
+ and daily timers use the `outpost.worktree-pruner` hq-flags key, which starts off.
28
+ Live-lane `worker.out` files and matching Claude session transcripts are read in
29
+ bounded chunks. Transcript lookup reads only the file matching the lane's
30
+ provider session ID under `.claude*/projects/*`. An unresolved lane keeps
31
+ worktrees whose root, linked Git index, or linked Git `HEAD` changed at or after
32
+ its `created_at`; a missing or invalid start time keeps every candidate. A
33
+ never-spawned queued lane without a provider session does not hold a worktree.
34
+
5
35
  ## [5.167.0] — 2026-09-24
6
36
 
7
37
  ### Fixed
@@ -5731,6 +5731,9 @@ export declare const COMMAND_CATALOG: readonly [{
5731
5731
  }, {
5732
5732
  readonly flags: "--create";
5733
5733
  readonly description: "Create the Board view when the GET is a 404";
5734
+ }, {
5735
+ readonly flags: "--reset-status";
5736
+ readonly description: "Overwrite live story status from the PRD (prints a one-line summary of changes)";
5734
5737
  }, {
5735
5738
  readonly flags: "--json";
5736
5739
  readonly description: "Print machine-readable JSON";
@@ -6775,6 +6778,72 @@ export declare const COMMAND_CATALOG: readonly [{
6775
6778
  readonly arguments: readonly [];
6776
6779
  readonly options: readonly [];
6777
6780
  readonly subcommands: readonly [{
6781
+ readonly name: "outpost-pruner";
6782
+ readonly description: "Inspect, install, and run safe outpost worktree pruning";
6783
+ readonly aliases: readonly [];
6784
+ readonly hidden: false;
6785
+ readonly usage: "[options] [command]";
6786
+ readonly arguments: readonly [];
6787
+ readonly options: readonly [];
6788
+ readonly subcommands: readonly [{
6789
+ readonly name: "run";
6790
+ readonly description: "Run the guarded outpost worktree and user-cache cleanup";
6791
+ readonly aliases: readonly [];
6792
+ readonly hidden: false;
6793
+ readonly usage: "[options]";
6794
+ readonly arguments: readonly [];
6795
+ readonly options: readonly [{
6796
+ readonly flags: "--trigger <kind>";
6797
+ readonly description: "pressure | daily | manual";
6798
+ }, {
6799
+ readonly flags: "--min-age-days <days>";
6800
+ readonly description: "Minimum age before cleanup (1–365 days)";
6801
+ }, {
6802
+ readonly flags: "--dry-run";
6803
+ readonly description: "List eligible worktrees without deleting files";
6804
+ }, {
6805
+ readonly flags: "--json";
6806
+ readonly description: "JSON on stdout (always)";
6807
+ }];
6808
+ readonly subcommands: readonly [];
6809
+ }, {
6810
+ readonly name: "install";
6811
+ readonly description: "Write the pressure and daily systemd user timers";
6812
+ readonly aliases: readonly [];
6813
+ readonly hidden: false;
6814
+ readonly usage: "[options]";
6815
+ readonly arguments: readonly [];
6816
+ readonly options: readonly [{
6817
+ readonly flags: "--systemd-root <path>";
6818
+ readonly description: "Systemd user unit directory";
6819
+ }, {
6820
+ readonly flags: "--enable";
6821
+ readonly description: "Enable both timers after writing the units";
6822
+ }, {
6823
+ readonly flags: "--json";
6824
+ readonly description: "JSON on stdout (always)";
6825
+ }];
6826
+ readonly subcommands: readonly [];
6827
+ }, {
6828
+ readonly name: "uninstall";
6829
+ readonly description: "Disable and remove the pressure and daily systemd user timers";
6830
+ readonly aliases: readonly [];
6831
+ readonly hidden: false;
6832
+ readonly usage: "[options]";
6833
+ readonly arguments: readonly [];
6834
+ readonly options: readonly [{
6835
+ readonly flags: "--systemd-root <path>";
6836
+ readonly description: "Systemd user unit directory";
6837
+ }, {
6838
+ readonly flags: "--disable";
6839
+ readonly description: "Disable both timers before removing their units";
6840
+ }, {
6841
+ readonly flags: "--json";
6842
+ readonly description: "JSON on stdout (always)";
6843
+ }];
6844
+ readonly subcommands: readonly [];
6845
+ }];
6846
+ }, {
6778
6847
  readonly name: "create";
6779
6848
  readonly description: "Register a lane, run admission, and return immediately";
6780
6849
  readonly aliases: readonly [];
@@ -7660,6 +7729,9 @@ export declare const COMMAND_CATALOG: readonly [{
7660
7729
  }, {
7661
7730
  readonly flags: "--hq-root <path>";
7662
7731
  readonly description: "HQ folder the services work against";
7732
+ }, {
7733
+ readonly flags: "--user <login>";
7734
+ readonly description: "on a box: the login user the daemon runs as (never root)";
7663
7735
  }];
7664
7736
  readonly subcommands: readonly [];
7665
7737
  }, {
@@ -7800,7 +7872,7 @@ export declare const COMMAND_CATALOG: readonly [{
7800
7872
  readonly options: readonly [];
7801
7873
  readonly subcommands: readonly [{
7802
7874
  readonly name: "self-update";
7803
- readonly description: "Update the hq CLI (and Codex), then restart the daemon onto the new version";
7875
+ readonly description: "Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI";
7804
7876
  readonly aliases: readonly [];
7805
7877
  readonly hidden: false;
7806
7878
  readonly usage: "[options]";
@@ -7825,6 +7897,21 @@ export declare const COMMAND_CATALOG: readonly [{
7825
7897
  readonly description: "claude --permission-mode";
7826
7898
  }];
7827
7899
  readonly subcommands: readonly [];
7900
+ }, {
7901
+ readonly name: "codex-remote-control";
7902
+ readonly description: "Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update";
7903
+ readonly aliases: readonly [];
7904
+ readonly hidden: false;
7905
+ readonly usage: "[options]";
7906
+ readonly arguments: readonly [];
7907
+ readonly options: readonly [{
7908
+ readonly flags: "--script <path>";
7909
+ readonly description: "the box's Codex runner script";
7910
+ }, {
7911
+ readonly flags: "--codex <path>";
7912
+ readonly description: "codex binary, watched for updates";
7913
+ }];
7914
+ readonly subcommands: readonly [];
7828
7915
  }];
7829
7916
  }];
7830
7917
  }, {
@@ -7396,6 +7396,10 @@ export const COMMAND_CATALOG = [
7396
7396
  "flags": "--create",
7397
7397
  "description": "Create the Board view when the GET is a 404"
7398
7398
  },
7399
+ {
7400
+ "flags": "--reset-status",
7401
+ "description": "Overwrite live story status from the PRD (prints a one-line summary of changes)"
7402
+ },
7399
7403
  {
7400
7404
  "flags": "--json",
7401
7405
  "description": "Print machine-readable JSON"
@@ -8754,6 +8758,90 @@ export const COMMAND_CATALOG = [
8754
8758
  "arguments": [],
8755
8759
  "options": [],
8756
8760
  "subcommands": [
8761
+ {
8762
+ "name": "outpost-pruner",
8763
+ "description": "Inspect, install, and run safe outpost worktree pruning",
8764
+ "aliases": [],
8765
+ "hidden": false,
8766
+ "usage": "[options] [command]",
8767
+ "arguments": [],
8768
+ "options": [],
8769
+ "subcommands": [
8770
+ {
8771
+ "name": "run",
8772
+ "description": "Run the guarded outpost worktree and user-cache cleanup",
8773
+ "aliases": [],
8774
+ "hidden": false,
8775
+ "usage": "[options]",
8776
+ "arguments": [],
8777
+ "options": [
8778
+ {
8779
+ "flags": "--trigger <kind>",
8780
+ "description": "pressure | daily | manual"
8781
+ },
8782
+ {
8783
+ "flags": "--min-age-days <days>",
8784
+ "description": "Minimum age before cleanup (1–365 days)"
8785
+ },
8786
+ {
8787
+ "flags": "--dry-run",
8788
+ "description": "List eligible worktrees without deleting files"
8789
+ },
8790
+ {
8791
+ "flags": "--json",
8792
+ "description": "JSON on stdout (always)"
8793
+ }
8794
+ ],
8795
+ "subcommands": []
8796
+ },
8797
+ {
8798
+ "name": "install",
8799
+ "description": "Write the pressure and daily systemd user timers",
8800
+ "aliases": [],
8801
+ "hidden": false,
8802
+ "usage": "[options]",
8803
+ "arguments": [],
8804
+ "options": [
8805
+ {
8806
+ "flags": "--systemd-root <path>",
8807
+ "description": "Systemd user unit directory"
8808
+ },
8809
+ {
8810
+ "flags": "--enable",
8811
+ "description": "Enable both timers after writing the units"
8812
+ },
8813
+ {
8814
+ "flags": "--json",
8815
+ "description": "JSON on stdout (always)"
8816
+ }
8817
+ ],
8818
+ "subcommands": []
8819
+ },
8820
+ {
8821
+ "name": "uninstall",
8822
+ "description": "Disable and remove the pressure and daily systemd user timers",
8823
+ "aliases": [],
8824
+ "hidden": false,
8825
+ "usage": "[options]",
8826
+ "arguments": [],
8827
+ "options": [
8828
+ {
8829
+ "flags": "--systemd-root <path>",
8830
+ "description": "Systemd user unit directory"
8831
+ },
8832
+ {
8833
+ "flags": "--disable",
8834
+ "description": "Disable both timers before removing their units"
8835
+ },
8836
+ {
8837
+ "flags": "--json",
8838
+ "description": "JSON on stdout (always)"
8839
+ }
8840
+ ],
8841
+ "subcommands": []
8842
+ }
8843
+ ]
8844
+ },
8757
8845
  {
8758
8846
  "name": "create",
8759
8847
  "description": "Register a lane, run admission, and return immediately",
@@ -9904,6 +9992,10 @@ export const COMMAND_CATALOG = [
9904
9992
  {
9905
9993
  "flags": "--hq-root <path>",
9906
9994
  "description": "HQ folder the services work against"
9995
+ },
9996
+ {
9997
+ "flags": "--user <login>",
9998
+ "description": "on a box: the login user the daemon runs as (never root)"
9907
9999
  }
9908
10000
  ],
9909
10001
  "subcommands": []
@@ -10080,7 +10172,7 @@ export const COMMAND_CATALOG = [
10080
10172
  "subcommands": [
10081
10173
  {
10082
10174
  "name": "self-update",
10083
- "description": "Update the hq CLI (and Codex), then restart the daemon onto the new version",
10175
+ "description": "Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI",
10084
10176
  "aliases": [],
10085
10177
  "hidden": false,
10086
10178
  "usage": "[options]",
@@ -10110,6 +10202,25 @@ export const COMMAND_CATALOG = [
10110
10202
  }
10111
10203
  ],
10112
10204
  "subcommands": []
10205
+ },
10206
+ {
10207
+ "name": "codex-remote-control",
10208
+ "description": "Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update",
10209
+ "aliases": [],
10210
+ "hidden": false,
10211
+ "usage": "[options]",
10212
+ "arguments": [],
10213
+ "options": [
10214
+ {
10215
+ "flags": "--script <path>",
10216
+ "description": "the box's Codex runner script"
10217
+ },
10218
+ {
10219
+ "flags": "--codex <path>",
10220
+ "description": "codex binary, watched for updates"
10221
+ }
10222
+ ],
10223
+ "subcommands": []
10113
10224
  }
10114
10225
  ]
10115
10226
  }
@@ -29,6 +29,8 @@ export interface DaemonCommandDeps {
29
29
  isRoot: boolean;
30
30
  fileExists: (p: string) => boolean;
31
31
  readFile: (p: string) => string;
32
+ /** Writes a root-owned file such as /etc/hq/machine-type, creating its folder. */
33
+ writeFile: (p: string, text: string) => void;
32
34
  legacyUnits: () => LegacyUnit[];
33
35
  resolveHqRoot: (flag: string | undefined) => string | undefined;
34
36
  /** Home folder of the login user a box's system unit runs as. */
@@ -20,10 +20,10 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices } from "../lib/
20
20
  import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
21
21
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
22
22
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
23
- import { SYSTEM_UNIT_DIR, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
23
+ import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
24
24
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
25
25
  import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
26
- import { detectMachineType, homeFromPasswd } from "../lib/daemon/machine-type.js";
26
+ import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
27
27
  import { daemonPaths } from "../lib/daemon/paths.js";
28
28
  import { writeControlRequest } from "../lib/daemon/requests.js";
29
29
  import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
@@ -67,6 +67,11 @@ async function loadDefaults(over) {
67
67
  isRoot: over.isRoot ?? (typeof process.getuid === "function" && process.getuid() === 0),
68
68
  fileExists: over.fileExists ?? fs.existsSync,
69
69
  readFile: over.readFile ?? ((p) => fs.readFileSync(p, "utf8")),
70
+ writeFile: over.writeFile ??
71
+ ((p, text) => {
72
+ fs.mkdirSync(path.dirname(p), { recursive: true });
73
+ fs.writeFileSync(p, text, { mode: 0o644 });
74
+ }),
70
75
  legacyUnits: over.legacyUnits ??
71
76
  (() => findLegacyUnits({
72
77
  platform,
@@ -142,17 +147,22 @@ function loginHome(user, run) {
142
147
  * Where the daemon's files are for this machine. On boxes the system unit runs
143
148
  * as the login user, so its files live in that user's home, not root's.
144
149
  */
145
- function hostPaths(d, flag) {
150
+ function hostPaths(d, flag, userFlag) {
146
151
  const own = daemonPaths(d.home, d.env);
147
152
  const detected = detect(d, flag, readDaemonConfig(own.configPath));
148
153
  if (installScopeFor(detected.type) !== "system")
149
154
  return { paths: own, detected };
150
- const runAsUser = detected.runAsUser;
155
+ // An explicit --user, then the marker or the outpost runner's User=, then
156
+ // the account that ran sudo.
157
+ const sudoUser = d.env.SUDO_USER?.trim();
158
+ const runAsUser = userFlag ?? detected.runAsUser ?? (sudoUser && sudoUser !== "root" ? sudoUser : undefined);
159
+ if (runAsUser === "root")
160
+ throw new DaemonCommandError(`${ROOT_REFUSAL} Pass the box's login user with --user <name>.`);
151
161
  if (!runAsUser) {
152
- throw new DaemonCommandError("A box install needs the login user the daemon runs as. Provisioning writes it to /etc/hq/machine-type (user=<name>).");
162
+ throw new DaemonCommandError("A box install needs the login user the daemon runs as: pass --user <name>, or provisioning writes it to /etc/hq/machine-type (user=<name>).");
153
163
  }
154
164
  const paths = daemonPaths(d.userHome(runAsUser), d.env);
155
- if (flag !== undefined)
165
+ if (flag !== undefined || userFlag !== undefined)
156
166
  return { paths, detected, runAsUser };
157
167
  // Root normally has no saved config; the login user's saved type is the real one.
158
168
  const saved = readDaemonConfig(paths.configPath);
@@ -177,8 +187,12 @@ function handOver(d, host) {
177
187
  d.run("chown", ["-R", `${host.runAsUser}:`, host.paths.root]);
178
188
  }
179
189
  async function runInstall(d, opts) {
180
- const { paths, detected, runAsUser } = hostPaths(d, opts.machineType);
190
+ const { paths, detected, runAsUser } = hostPaths(d, opts.machineType, opts.user);
181
191
  const scope = installScopeFor(detected.type);
192
+ // A per-user unit installed by root would run every service as root.
193
+ const refusal = scope === "user" ? daemonRootRefusal(d.isRoot, detected.type) : null;
194
+ if (refusal)
195
+ throw new DaemonCommandError(refusal);
182
196
  const config = {
183
197
  ...readDaemonConfig(paths.configPath),
184
198
  machineType: detected.type,
@@ -198,6 +212,13 @@ async function runInstall(d, opts) {
198
212
  for (const u of migratable) {
199
213
  d.out(`Note: ${describeLegacyUnit(u)} still runs ${u.unitId}. The daemon leaves it alone until \`hq daemon migrate\` takes it over.`);
200
214
  }
215
+ // A box worked out from its runner unit, --user or sudo gets the marker file
216
+ // provisioning would have written: `migrate --apply` moves the runner unit
217
+ // aside, and later root commands (uninstall) must still find the box's type
218
+ // and login user.
219
+ if (scope === "system" && runAsUser && d.isRoot && !d.fileExists(MACHINE_TYPE_MARKER_PATH)) {
220
+ d.writeFile(MACHINE_TYPE_MARKER_PATH, `type=${detected.type}\nuser=${runAsUser}\n`);
221
+ }
201
222
  const runAsHome = runAsUser ? d.userHome(runAsUser) : undefined;
202
223
  const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, { run: d.run });
203
224
  handOver(d, { paths, detected, runAsUser });
@@ -397,32 +418,41 @@ async function runMigrate(d, opts) {
397
418
  d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
398
419
  }
399
420
  async function runServiceSelfUpdate(d) {
400
- const [{ daemonSelfUpdate }, { npmCodexUpdater, runDaemonUpdater, updaterEnv }, { cliSelfUpdatable }] = await Promise.all([
421
+ const [{ daemonSelfUpdate }, { hostToolUpdater, runDaemonUpdater, updaterEnv }, { TOOL_NAMES }, { cliSelfUpdatable }] = await Promise.all([
401
422
  import("../utils/self-update.js"),
402
423
  import("../lib/daemon/updater.js"),
424
+ import("../lib/daemon/tools.js"),
403
425
  import("../lib/daemon/run.js"),
404
426
  ]);
405
- // Same guard as the plan: never replace a dev checkout or a project's copy with a global install.
406
- if (!cliSelfUpdatable()) {
407
- d.out("This hq CLI is not a global install; nothing to update here.");
408
- return;
409
- }
410
427
  const { paths } = currentHost(d);
428
+ const env = updaterEnv(d.env);
411
429
  const result = await runDaemonUpdater({
412
- updateCli: () => daemonSelfUpdate({ env: updaterEnv(d.env) }),
413
- codex: npmCodexUpdater(),
430
+ // Same guard as every self-update: never replace a dev checkout or a project's copy with a global install.
431
+ updateCli: cliSelfUpdatable() ? () => daemonSelfUpdate({ env }) : null,
432
+ tools: TOOL_NAMES.map((name) => hostToolUpdater(name, env, d.home)),
414
433
  requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
415
434
  log: d.out,
416
435
  });
417
436
  if (!result.ok)
418
437
  process.exitCode = 1;
419
438
  }
420
- async function runServiceRemoteControl(d, opts) {
439
+ /** Size, mtime and real path of a binary: changes whenever a new version is installed over it. */
440
+ function binaryFingerprint(bin) {
441
+ try {
442
+ const real = fs.realpathSync(bin);
443
+ const st = fs.statSync(real);
444
+ return `${real}:${st.size}:${st.mtimeMs}`;
445
+ }
446
+ catch {
447
+ return null;
448
+ }
449
+ }
450
+ async function runTmuxService(d, opts) {
421
451
  const { runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
422
452
  const { findOnPath } = await import("../lib/daemon/run.js");
423
453
  const hqRoot = d.env.HQ_ROOT?.trim();
424
454
  if (!hqRoot)
425
- throw new DaemonCommandError("remote-control needs HQ_ROOT (the daemon sets it from the saved HQ folder)");
455
+ throw new DaemonCommandError(`${opts.label} needs HQ_ROOT (the daemon sets it from the saved HQ folder)`);
426
456
  const tmux = findOnPath("tmux", d.env);
427
457
  if (!tmux)
428
458
  throw new DaemonCommandError("tmux is not installed");
@@ -433,10 +463,12 @@ async function runServiceRemoteControl(d, opts) {
433
463
  try {
434
464
  process.exitCode = await runRemoteControlHost({
435
465
  tmux,
436
- claude: opts.claude,
437
- hqRoot,
438
- name: opts.name,
439
- permissionMode: opts.permissionMode,
466
+ session: opts.session,
467
+ cwd: hqRoot,
468
+ argv: opts.argv,
469
+ label: opts.label,
470
+ binaryFingerprint: () => binaryFingerprint(opts.watch),
471
+ now: () => Date.now(),
440
472
  run: d.run,
441
473
  pollMs: 5_000,
442
474
  signal: controller.signal,
@@ -455,6 +487,24 @@ async function runServiceRemoteControl(d, opts) {
455
487
  process.off("SIGINT", stop);
456
488
  }
457
489
  }
490
+ async function runServiceRemoteControl(d, opts) {
491
+ const { TMUX_SESSION, remoteControlArgs } = await import("../lib/daemon/remote-control.js");
492
+ await runTmuxService(d, {
493
+ session: TMUX_SESSION,
494
+ argv: [opts.claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
495
+ label: "claude remote-control",
496
+ watch: opts.claude,
497
+ });
498
+ }
499
+ async function runServiceCodexRemoteControl(d, opts) {
500
+ const { CODEX_TMUX_SESSION } = await import("../lib/daemon/remote-control.js");
501
+ await runTmuxService(d, {
502
+ session: CODEX_TMUX_SESSION,
503
+ argv: ["bash", opts.script],
504
+ label: "codex remote-control",
505
+ watch: opts.codex,
506
+ });
507
+ }
458
508
  export function registerDaemonCommand(program, over = {}) {
459
509
  let resolved = null;
460
510
  const deps = () => (resolved ??= loadDefaults(over));
@@ -477,6 +527,7 @@ export function registerDaemonCommand(program, over = {}) {
477
527
  .description("Detect this machine's type, install the daemon, and start its default services")
478
528
  .option("--machine-type <type>", "desktop | self-hosted | outpost | agent-box | agent-kit (skips detection)")
479
529
  .option("--hq-root <path>", "HQ folder the services work against")
530
+ .option("--user <login>", "on a box: the login user the daemon runs as (never root)")
480
531
  .action(wrap((d, opts) => runInstall(d, opts)));
481
532
  daemon
482
533
  .command("uninstall")
@@ -540,7 +591,7 @@ export function registerDaemonCommand(program, over = {}) {
540
591
  .description("Entry points the daemon runs for services that have no command of their own");
541
592
  service
542
593
  .command("self-update")
543
- .description("Update the hq CLI (and Codex), then restart the daemon onto the new version")
594
+ .description("Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI")
544
595
  .action(wrap((d) => runServiceSelfUpdate(d)));
545
596
  service
546
597
  .command("remote-control")
@@ -549,6 +600,12 @@ export function registerDaemonCommand(program, over = {}) {
549
600
  .requiredOption("--name <name>", "remote-control session name")
550
601
  .option("--permission-mode <mode>", "claude --permission-mode")
551
602
  .action(wrap((d, opts) => runServiceRemoteControl(d, opts)));
603
+ service
604
+ .command("codex-remote-control")
605
+ .description("Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update")
606
+ .requiredOption("--script <path>", "the box's Codex runner script")
607
+ .requiredOption("--codex <path>", "codex binary, watched for updates")
608
+ .action(wrap((d, opts) => runServiceCodexRemoteControl(d, opts)));
552
609
  return daemon;
553
610
  }
554
611
  //# sourceMappingURL=daemon.js.map