@indigoai-us/hq-cli 5.304.0 → 5.306.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 (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/command-catalog.generated.d.ts +3 -3
  3. package/dist/command-catalog.generated.js +5 -3
  4. package/dist/command-registration-plan.js +8 -0
  5. package/dist/commands/daemon.js +24 -23
  6. package/dist/commands/lanes.js +13 -0
  7. package/dist/commands/mcp-status.js +1 -0
  8. package/dist/commands/members.js +0 -1
  9. package/dist/commands/people.d.ts +3 -0
  10. package/dist/commands/people.js +27 -3
  11. package/dist/index.js +6 -1
  12. package/dist/lazy-commands.js +5 -2
  13. package/dist/lib/aws-sdk-node-warning-output.d.ts +12 -0
  14. package/dist/lib/aws-sdk-node-warning-output.js +22 -0
  15. package/dist/lib/company-errors-sdk-warning-flag.d.ts +5 -0
  16. package/dist/lib/company-errors-sdk-warning-flag.js +8 -0
  17. package/dist/lib/daemon/catalog.js +6 -2
  18. package/dist/lib/daemon/remote-control.d.ts +19 -8
  19. package/dist/lib/daemon/remote-control.js +62 -12
  20. package/dist/lib/daemon/supervisor.d.ts +2 -0
  21. package/dist/lib/daemon/supervisor.js +11 -0
  22. package/dist/lib/doctor/checks/work-context.js +2 -2
  23. package/dist/lib/lanes/keep-alive.js +13 -2
  24. package/dist/lib/mesh/live/daemon/doctor.js +2 -2
  25. package/dist/lib/mesh/live/flush.d.ts +2 -2
  26. package/dist/lib/mesh/live/flush.js +49 -29
  27. package/dist/lib/mesh/live/spool.d.ts +6 -0
  28. package/dist/lib/mesh/live/spool.js +203 -4
  29. package/dist/lib/work-context/bind-best.js +13 -1
  30. package/dist/main.js +16 -3
  31. package/dist/startup-registration.js +9 -3
  32. package/dist/utils/install-tree-torn.d.ts +13 -6
  33. package/dist/utils/install-tree-torn.js +42 -16
  34. package/dist/utils/self-update.d.ts +4 -0
  35. package/dist/utils/self-update.js +46 -9
  36. package/dist/utils/sentry-fingerprint.js +1 -0
  37. package/dist/utils/version-check.d.ts +7 -0
  38. package/dist/utils/version-check.js +30 -1
  39. package/dist/utils/version-gate.d.ts +28 -10
  40. package/dist/utils/version-gate.js +159 -29
  41. package/dist/utils/version-report.d.ts +2 -1
  42. package/dist/utils/version-report.js +8 -1
  43. package/package.json +1 -1
  44. package/dist/lib/work-context/index.d.ts +0 -26
  45. package/dist/lib/work-context/index.js +0 -22
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.306.0] — 2026-10-01
6
+ - End long install-tree waits early when a named retired npm sibling's deep activity scan is incomplete, report the recovery as unverified, and give safe wait-and-repair guidance.
7
+
8
+ ## [5.305.0] — 2026-10-01
9
+
10
+ - Work Mesh project discovery skips projects whose matching local lifecycle state is archived or retired.
11
+
12
+ - `hq lanes show` now treats a newer terminal primary mirror as authoritative when an Outpost owner snapshot is behind, avoiding a misleading refresh warning while preserving warnings for nonterminal conflicts.
13
+ - Self-updates detect an unwritable npm global prefix before install, show the command to repair that install, and report the handled permission state as a rate-limited warning.
14
+ - Keep Work Mesh held events unique by event ID across retries, including interrupted overflow recovery.
15
+ - Added `hq mcp list` as an alias for the existing MCP status command. `hq version` can show cached CLI update guidance on stderr when the default-off `cli.version-update-nudge` flag is enabled. Bare `hq --version` prints only the installed CLI semver and starts no flag or network work.
16
+ - Keep-alive no longer lets a newer terminal lane or a same-profile lane under a distinct lane senior supersede a running lane.
17
+ - With `cli.company-errors-sdk-warning` enabled, `hq people` uses shared active-company guidance when local metadata is missing, and files commands show the AWS SDK Node-version notice only with verbose diagnostics.
18
+ - Remove the unrelated secret-sharing hint from active member listings.
19
+
5
20
  ## [5.304.0] — 2026-10-01
6
21
  - `hq lanes show` skips owner refresh only when a failed Outpost dispatch explicitly recorded `pickup_persisted: false` and no owner heartbeat; it reports `remote_refresh: "skipped_undispatched"` in that case.
7
22
 
@@ -46,6 +61,7 @@
46
61
 
47
62
  ## [5.300.0] — 2026-09-30
48
63
  - `hq files unshare --with agt_...` removes a grant made to an agent. Before, agent uids were rejected as invalid principals.
64
+ - `hq daemon` no longer drops remote-control sessions when claude or codex is reinstalled at the same version or when hq-cli is released. The daemon's plan leaves out the tool's path (the host finds it when it starts), the host restarts a session only for a new tool version, and a tool that looks missing for one plan read does not pause the session. On outpost primary, a long-running Claude session's auto-updater reinstalled claude every few minutes and each reinstall restarted remote-control two or three times.
49
65
  - With the default-off hq-flags gate `files.share-empty-prefix-warning`, `hq files share` warns when a granted prefix has no files. A failed or timed-out list check does not block the grant.
50
66
  - Recognize pnpm v11+ global installs and handle Windows drive-path tar archives without waiting for the asynchronous feature-flag snapshot.
51
67
  - Self-update failure reports now include the npm error code and a sanitized reason.
@@ -3622,7 +3622,7 @@ export declare const COMMAND_CATALOG: readonly [{
3622
3622
  readonly subcommands: readonly [{
3623
3623
  readonly name: "status";
3624
3624
  readonly description: "Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)";
3625
- readonly aliases: readonly [];
3625
+ readonly aliases: readonly ["list"];
3626
3626
  readonly hidden: false;
3627
3627
  readonly usage: "[options]";
3628
3628
  readonly arguments: readonly [];
@@ -9664,7 +9664,7 @@ export declare const COMMAND_CATALOG: readonly [{
9664
9664
  readonly arguments: readonly [];
9665
9665
  readonly options: readonly [{
9666
9666
  readonly flags: "--claude <path>";
9667
- readonly description: "claude binary";
9667
+ readonly description: "claude binary (default: the one installed for this user)";
9668
9668
  }, {
9669
9669
  readonly flags: "--name <name>";
9670
9670
  readonly description: "remote-control session name";
@@ -9685,7 +9685,7 @@ export declare const COMMAND_CATALOG: readonly [{
9685
9685
  readonly description: "the box's Codex runner script";
9686
9686
  }, {
9687
9687
  readonly flags: "--codex <path>";
9688
- readonly description: "codex binary, watched for updates";
9688
+ readonly description: "codex binary, watched for updates (default: the one installed for this user)";
9689
9689
  }];
9690
9690
  readonly subcommands: readonly [];
9691
9691
  }];
@@ -4674,7 +4674,9 @@ export const COMMAND_CATALOG = [
4674
4674
  {
4675
4675
  "name": "status",
4676
4676
  "description": "Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)",
4677
- "aliases": [],
4677
+ "aliases": [
4678
+ "list"
4679
+ ],
4678
4680
  "hidden": false,
4679
4681
  "usage": "[options]",
4680
4682
  "arguments": [],
@@ -12455,7 +12457,7 @@ export const COMMAND_CATALOG = [
12455
12457
  "options": [
12456
12458
  {
12457
12459
  "flags": "--claude <path>",
12458
- "description": "claude binary"
12460
+ "description": "claude binary (default: the one installed for this user)"
12459
12461
  },
12460
12462
  {
12461
12463
  "flags": "--name <name>",
@@ -12482,7 +12484,7 @@ export const COMMAND_CATALOG = [
12482
12484
  },
12483
12485
  {
12484
12486
  "flags": "--codex <path>",
12485
- "description": "codex binary, watched for updates"
12487
+ "description": "codex binary, watched for updates (default: the one installed for this user)"
12486
12488
  }
12487
12489
  ],
12488
12490
  "subcommands": []
@@ -85,6 +85,7 @@ export const REGISTRATION_PLAN = [
85
85
  ];
86
86
  async function executePlan(program, steps) {
87
87
  const parents = new Map([["program", program]]);
88
+ let filesRuntimeConfigured = false;
88
89
  for (const step of steps) {
89
90
  if (step.type === "group") {
90
91
  const group = program.command(step.name, step.hidden ? { hidden: true } : {});
@@ -94,6 +95,13 @@ async function executePlan(program, steps) {
94
95
  const parent = parents.get(step.parent);
95
96
  if (!parent)
96
97
  throw new Error(`Missing command parent '${step.parent}' for '${step.exportName}'`);
98
+ if (step.root === "files" && !filesRuntimeConfigured) {
99
+ // The SDK emits its Node support notice during module evaluation. Set
100
+ // its narrow suppression switch before loading any files command module.
101
+ const { configureAwsSdkNodeVersionWarningOutput } = await import("./lib/aws-sdk-node-warning-output.js");
102
+ configureAwsSdkNodeVersionWarningOutput();
103
+ filesRuntimeConfigured = true;
104
+ }
97
105
  const imported = await import(step.module);
98
106
  const register = imported[step.exportName];
99
107
  if (!register)
@@ -627,25 +627,9 @@ async function runServiceSelfUpdate(d) {
627
627
  if (!result.ok)
628
628
  process.exitCode = 1;
629
629
  }
630
- /** Size, mtime and real path of a binary: changes whenever a new version is installed over it. */
631
- function binaryFingerprint(bin) {
632
- try {
633
- const real = fs.realpathSync(bin);
634
- const st = fs.statSync(real);
635
- return `${real}:${st.size}:${st.mtimeMs}`;
636
- }
637
- catch {
638
- return null;
639
- }
640
- }
641
630
  async function runTmuxService(d, opts) {
642
- const { installFingerprint, runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
631
+ const { runRemoteControlHost, toolFingerprint } = await import("../lib/daemon/remote-control.js");
643
632
  const { findOnPath } = await import("../lib/daemon/run.js");
644
- const { resolveRunningInstall } = await import("../utils/version-gate.js");
645
- // The daemon keeps this host running across a CLI update, so a new hq-cli
646
- // install also counts as an update and restarts the session once it is quiet.
647
- const cliRoot = resolveRunningInstall().packageRoot;
648
- const watched = cliRoot ? [opts.watch, path.join(cliRoot, "package.json")] : [opts.watch];
649
633
  const hqRoot = d.env.HQ_ROOT?.trim();
650
634
  if (!hqRoot)
651
635
  throw new DaemonCommandError(`${opts.label} needs HQ_ROOT (the daemon sets it from the saved HQ folder)`);
@@ -663,7 +647,9 @@ async function runTmuxService(d, opts) {
663
647
  cwd: hqRoot,
664
648
  argv: opts.argv,
665
649
  label: opts.label,
666
- binaryFingerprint: () => installFingerprint(watched, binaryFingerprint),
650
+ // Only a new version of the tool restarts the session: not a same-version
651
+ // reinstall, and not an hq-cli release (the host's own code rarely matters).
652
+ binaryFingerprint: () => toolFingerprint(opts.watch),
667
653
  now: () => Date.now(),
668
654
  run: d.run,
669
655
  pollMs: 5_000,
@@ -683,22 +669,37 @@ async function runTmuxService(d, opts) {
683
669
  process.off("SIGINT", stop);
684
670
  }
685
671
  }
672
+ /** The claude or codex a remote-control host runs: --claude/--codex, else the one installed now. */
673
+ async function hostTool(d, name, given) {
674
+ const [{ serviceTool }, { findUserBinary }] = await Promise.all([
675
+ import("../lib/daemon/remote-control.js"),
676
+ import("../lib/daemon/run.js"),
677
+ ]);
678
+ try {
679
+ return serviceTool(name, given, (tool) => findUserBinary(tool, d.env, d.home));
680
+ }
681
+ catch (err) {
682
+ throw new DaemonCommandError(err.message);
683
+ }
684
+ }
686
685
  async function runServiceRemoteControl(d, opts) {
687
686
  const { TMUX_SESSION, remoteControlArgs } = await import("../lib/daemon/remote-control.js");
687
+ const claude = await hostTool(d, "claude", opts.claude);
688
688
  await runTmuxService(d, {
689
689
  session: TMUX_SESSION,
690
- argv: [opts.claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
690
+ argv: [claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
691
691
  label: "claude remote-control",
692
- watch: opts.claude,
692
+ watch: claude,
693
693
  });
694
694
  }
695
695
  async function runServiceCodexRemoteControl(d, opts) {
696
696
  const { CODEX_TMUX_SESSION } = await import("../lib/daemon/remote-control.js");
697
+ const codex = await hostTool(d, "codex", opts.codex);
697
698
  await runTmuxService(d, {
698
699
  session: CODEX_TMUX_SESSION,
699
700
  argv: ["bash", opts.script],
700
701
  label: "codex remote-control",
701
- watch: opts.codex,
702
+ watch: codex,
702
703
  });
703
704
  }
704
705
  export function registerDaemonCommand(program, over = {}) {
@@ -839,7 +840,7 @@ export function registerDaemonCommand(program, over = {}) {
839
840
  service
840
841
  .command("remote-control")
841
842
  .description("Keep `claude remote-control` running in a tmux session in the HQ folder")
842
- .requiredOption("--claude <path>", "claude binary")
843
+ .option("--claude <path>", "claude binary (default: the one installed for this user)")
843
844
  .requiredOption("--name <name>", "remote-control session name")
844
845
  .option("--permission-mode <mode>", "claude --permission-mode")
845
846
  .action(wrap((d, opts) => runServiceRemoteControl(d, opts)));
@@ -847,7 +848,7 @@ export function registerDaemonCommand(program, over = {}) {
847
848
  .command("codex-remote-control")
848
849
  .description("Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update")
849
850
  .requiredOption("--script <path>", "the box's Codex runner script")
850
- .requiredOption("--codex <path>", "codex binary, watched for updates")
851
+ .option("--codex <path>", "codex binary, watched for updates (default: the one installed for this user)")
851
852
  .action(wrap((d, opts) => runServiceCodexRemoteControl(d, opts)));
852
853
  return daemon;
853
854
  }
@@ -5065,6 +5065,19 @@ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
5065
5065
  return syncLaneRecordFromOwner(ctx.hqRoot(), laneId, outpostId, ownerLane, undefined, { allowNonTerminal: true });
5066
5066
  }
5067
5067
  catch (err) {
5068
+ if (isLanesError(err) && err.error === "outpost_lane_sync_conflict") {
5069
+ const ownerVersion = err.details.owner_version;
5070
+ const primaryVersion = err.details.primary_version;
5071
+ if (typeof ownerVersion === "number" &&
5072
+ typeof primaryVersion === "number" &&
5073
+ ownerVersion < primaryVersion) {
5074
+ const currentPrimary = readLane(ctx.hqRoot(), laneId);
5075
+ if (currentPrimary.version === primaryVersion &&
5076
+ isTerminalLaneState(currentPrimary.state)) {
5077
+ return currentPrimary;
5078
+ }
5079
+ }
5080
+ }
5068
5081
  const detail = isLanesError(err)
5069
5082
  ? `${err.error}: ${err.message}`
5070
5083
  : `outpost_lane_sync_unavailable: ${dispatchErrorMessage(err)}`;
@@ -374,6 +374,7 @@ export function registerMcpCommand(program) {
374
374
  .description('MCP pack observability (read-only) across Claude + Codex');
375
375
  mcp
376
376
  .command('status')
377
+ .alias('list')
377
378
  .description('Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)')
378
379
  .option('--json', 'Emit the structured report as JSON (for CI / scripting)')
379
380
  .action((opts) => {
@@ -672,7 +672,6 @@ export function registerMembersCommand(program) {
672
672
  const name = m.personName ?? m.personSlug ?? "";
673
673
  console.log([email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "));
674
674
  }
675
- console.log(chalk.gray("Share a secret with a member: hq secrets share <path> --with <email>"));
676
675
  }
677
676
  catch (err) {
678
677
  if (err instanceof InviteHttpError) {
@@ -17,6 +17,9 @@ import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
17
17
  export type FetchPeopleRoster = (hqRoot: string, companySlug: string) => Promise<PersonRecord[]>;
18
18
  interface PeopleCommandDeps {
19
19
  fetchRoster?: FetchPeopleRoster;
20
+ /** Test seam for the shared active-membership ambiguity resolver. */
21
+ resolveMissingManifestMemberships?: () => Promise<void>;
22
+ companySelectionGuidanceEnabled?: () => boolean;
20
23
  }
21
24
  interface PeopleLookupOpts {
22
25
  localOnly?: boolean;
@@ -19,6 +19,7 @@ import { findHqRoot } from "../utils/manifest.js";
19
19
  import { manifestPath } from "./cloud-provision.js";
20
20
  import { ensureCognitoToken } from "../utils/cognito-session.js";
21
21
  import { getCompanyUid } from "../utils/vault-api.js";
22
+ import { companyErrorsSdkWarningEnabled } from "../lib/company-errors-sdk-warning-flag.js";
22
23
  import { listActiveMembers } from "./members.js";
23
24
  import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
24
25
  function slugify(value) {
@@ -108,6 +109,29 @@ export function resolveCompanySlug(hqRoot, explicit) {
108
109
  throw new Error("Multiple companies found — re-run with --company <slug> to pick one:\n" +
109
110
  slugs.map((s) => ` --company ${s}`).join("\n"));
110
111
  }
112
+ async function resolvePeopleCompanySlug(hqRoot, explicit, localOnly, deps) {
113
+ try {
114
+ return resolveCompanySlug(hqRoot, explicit);
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ const ambiguous = message.includes("companies/manifest.yaml not found") ||
119
+ message.startsWith("Multiple companies found");
120
+ const enabled = (deps.companySelectionGuidanceEnabled ?? companyErrorsSdkWarningEnabled)();
121
+ if (!explicit && !localOnly && enabled && ambiguous) {
122
+ const resolveMemberships = deps.resolveMissingManifestMemberships ??
123
+ (async () => {
124
+ const token = await ensureCognitoToken();
125
+ await getCompanyUid(token, undefined);
126
+ });
127
+ // The shared resolver throws members-style guidance for multiple active
128
+ // companies. A single membership cannot provide a local slug without a
129
+ // manifest, so preserve the existing missing-manifest error in that case.
130
+ await resolveMemberships();
131
+ }
132
+ throw error;
133
+ }
134
+ }
111
135
  /** Resolve the HQ root: explicit override (tests) → cwd walk-up. */
112
136
  function resolveHqRoot(opts) {
113
137
  return opts.hqRoot ?? findHqRoot();
@@ -192,7 +216,7 @@ export function registerPeopleCommand(program, deps = {}) {
192
216
  try {
193
217
  const scope = people.opts();
194
218
  const hqRoot = resolveHqRoot(scope);
195
- const slug = resolveCompanySlug(hqRoot, scope.company);
219
+ const slug = await resolvePeopleCompanySlug(hqRoot, scope.company, opts.localOnly, deps);
196
220
  let records = listCompanyPeople(hqRoot, slug);
197
221
  if (!opts.localOnly) {
198
222
  const roster = await tryFetchRoster(deps.fetchRoster ?? fetchMembershipRoster, hqRoot, slug);
@@ -223,7 +247,7 @@ export function registerPeopleCommand(program, deps = {}) {
223
247
  try {
224
248
  const scope = people.opts();
225
249
  const hqRoot = resolveHqRoot(scope);
226
- const slug = resolveCompanySlug(hqRoot, scope.company);
250
+ const slug = await resolvePeopleCompanySlug(hqRoot, scope.company, opts.localOnly, deps);
227
251
  const matches = await searchPeopleWithRosterFallback({
228
252
  hqRoot,
229
253
  slug,
@@ -255,7 +279,7 @@ export function registerPeopleCommand(program, deps = {}) {
255
279
  try {
256
280
  const scope = people.opts();
257
281
  const hqRoot = resolveHqRoot(scope);
258
- const slug = resolveCompanySlug(hqRoot, scope.company);
282
+ const slug = await resolvePeopleCompanySlug(hqRoot, scope.company, opts.localOnly, deps);
259
283
  const result = await resolvePersonWithRosterFallback({
260
284
  hqRoot,
261
285
  slug,
package/dist/index.js CHANGED
@@ -22,9 +22,14 @@ if (isVersionRequest(process.argv)) {
22
22
  // compare). `hq version` and `--json` print the labelled HQ/CLI report.
23
23
  // Lazy so this path still skips the command graph.
24
24
  if (wantsLabelledVersionReport(process.argv)) {
25
- void import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
25
+ void import("./lib/flag-registry.js").then(({ kickFlagRegistryReadiness }) => {
26
+ kickFlagRegistryReadiness();
27
+ return import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
28
+ });
26
29
  }
27
30
  else {
31
+ // Keep the benchmarked updater fast route limited to the installed semver.
32
+ // Registry checks and flag resolution belong to the regular command path.
28
33
  void import("./cli-version.js").then(({ CLI_VERSION }) => {
29
34
  process.stdout.write(`${CLI_VERSION}\n`);
30
35
  });
@@ -55,8 +55,11 @@ function commandForToken(token) {
55
55
  * preserve the complete help and unknown-command recovery surfaces.
56
56
  */
57
57
  export function findLazyCommand(argv) {
58
- const first = argv[2];
59
- const token = first === "help" ? argv[3] : first;
58
+ let commandIndex = 2;
59
+ while (argv[commandIndex] === "--verbose")
60
+ commandIndex += 1;
61
+ const first = argv[commandIndex];
62
+ const token = first === "help" ? argv[commandIndex + 1] : first;
60
63
  if (typeof token !== "string" || token.startsWith("-"))
61
64
  return null;
62
65
  const command = commandForToken(token);
@@ -0,0 +1,12 @@
1
+ export declare const AWS_SDK_NODE_VERSION_WARNING_ENV = "AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED";
2
+ export interface AwsSdkWarningOutputOptions {
3
+ argv?: readonly string[];
4
+ env?: NodeJS.ProcessEnv;
5
+ enabled?: boolean;
6
+ }
7
+ /**
8
+ * Configure the AWS SDK's documented, warning-specific switch before loading
9
+ * the files command and its SDK clients. No other SDK warnings are affected.
10
+ */
11
+ export declare function configureAwsSdkNodeVersionWarningOutput(options?: AwsSdkWarningOutputOptions): void;
12
+ //# sourceMappingURL=aws-sdk-node-warning-output.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { companyErrorsSdkWarningEnabled } from "./company-errors-sdk-warning-flag.js";
2
+ export const AWS_SDK_NODE_VERSION_WARNING_ENV = "AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED";
3
+ /**
4
+ * Configure the AWS SDK's documented, warning-specific switch before loading
5
+ * the files command and its SDK clients. No other SDK warnings are affected.
6
+ */
7
+ export function configureAwsSdkNodeVersionWarningOutput(options = {}) {
8
+ const env = options.env ?? process.env;
9
+ const argv = options.argv ?? process.argv;
10
+ const enabled = options.enabled ?? companyErrorsSdkWarningEnabled();
11
+ if (!enabled)
12
+ return;
13
+ const verbose = argv.includes("--verbose");
14
+ const debug = /^(1|true|yes)$/i.test(env.HQ_DEBUG?.trim() ?? "");
15
+ if (verbose || debug) {
16
+ delete env[AWS_SDK_NODE_VERSION_WARNING_ENV];
17
+ }
18
+ else {
19
+ env[AWS_SDK_NODE_VERSION_WARNING_ENV] = "true";
20
+ }
21
+ }
22
+ //# sourceMappingURL=aws-sdk-node-warning-output.js.map
@@ -0,0 +1,5 @@
1
+ export declare const COMPANY_ERRORS_SDK_WARNING_FLAG = "cli.company-errors-sdk-warning";
2
+ export declare const COMPANY_ERRORS_SDK_WARNING_FALLBACK = false;
3
+ /** One default-off rollout gate for the two US-163 CLI compatibility fixes. */
4
+ export declare function companyErrorsSdkWarningEnabled(): boolean;
5
+ //# sourceMappingURL=company-errors-sdk-warning-flag.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { resolveProcessFlagGate } from "./flag-registry.js";
2
+ export const COMPANY_ERRORS_SDK_WARNING_FLAG = "cli.company-errors-sdk-warning";
3
+ export const COMPANY_ERRORS_SDK_WARNING_FALLBACK = false;
4
+ /** One default-off rollout gate for the two US-163 CLI compatibility fixes. */
5
+ export function companyErrorsSdkWarningEnabled() {
6
+ return resolveProcessFlagGate(COMPANY_ERRORS_SDK_WARNING_FLAG, {}, () => COMPANY_ERRORS_SDK_WARNING_FALLBACK);
7
+ }
8
+ //# sourceMappingURL=company-errors-sdk-warning-flag.js.map
@@ -283,7 +283,9 @@ function planService(service, ctx) {
283
283
  base(service, {
284
284
  command: {
285
285
  type: "hq",
286
- args: ["daemon", "service", "codex-remote-control", "--script", script ?? "", "--codex", ctx.codexPath ?? ""],
286
+ // No codex path: the host finds codex itself, so a reinstall that
287
+ // briefly moves the link cannot change the plan and restart it.
288
+ args: ["daemon", "service", "codex-remote-control", "--script", script ?? ""],
287
289
  },
288
290
  // Restarting the host ends the tmux session people are using.
289
291
  keepAcrossCliUpdates: true,
@@ -303,7 +305,9 @@ function planService(service, ctx) {
303
305
  // Outposts keep the runner's session name and permission mode.
304
306
  const outpost = ctx.machineType === "outpost";
305
307
  const name = ctx.remoteControlName ?? (outpost ? "outpost-1" : ctx.hostName);
306
- const args = ["daemon", "service", "remote-control", "--claude", ctx.claudePath ?? "", "--name", name];
308
+ // No claude path: the host finds claude itself, so a reinstall that
309
+ // briefly moves the link cannot change the plan and restart the session.
310
+ const args = ["daemon", "service", "remote-control", "--name", name];
307
311
  if (outpost)
308
312
  args.push("--permission-mode", "bypassPermissions");
309
313
  return [
@@ -10,10 +10,12 @@
10
10
  * - the session ends -> the host exits 1 and the daemon restarts it with
11
11
  * backoff (replacing the runner's `while true` loop and its watchdogs);
12
12
  * - the daemon stops the host -> the host kills the session and exits 0;
13
- * - the tool's binary changes on disk (the updater installed a new version) ->
14
- * once the session has been quiet for 10 minutes, or after 2 hours at the
15
- * latest, the host kills the session and exits 0, and the daemon starts it
16
- * again on the new version.
13
+ * - a new version of the tool is installed (the updater, or the tool's own
14
+ * auto-updater) -> once the session has been quiet for 10 minutes, or after
15
+ * 2 hours at the latest, the host kills the session and exits 0, and the
16
+ * daemon starts it again on the new version. Reinstalling the same version
17
+ * (a long-running Claude session's auto-updater does that every few
18
+ * minutes) and hq-cli releases leave the session alone.
17
19
  *
18
20
  * The tmux server is a private socket (`-L hq-daemon`), so it never touches a
19
21
  * user's own tmux sessions.
@@ -32,11 +34,20 @@ export declare function remoteControlArgs(opts: {
32
34
  */
33
35
  export declare function runnerSessionName(script: string): string | null;
34
36
  /**
35
- * Fingerprint of what a host runs: the tool first, then other installs it
36
- * depends on (the hq-cli package). Null while the tool cannot be read; an
37
- * unreadable extra install is left out rather than hiding a tool update.
37
+ * Which version of a tool is installed: the npm package that installs the
38
+ * binary (`name@version`), else the binary's real path and size. A native
39
+ * install's link points at a versioned file, so its real path is the version.
40
+ * The mtime is left out on purpose: reinstalling the same version rewrites
41
+ * the file, and that must not restart the session. Null while the tool
42
+ * cannot be read.
38
43
  */
39
- export declare function installFingerprint(paths: readonly string[], fingerprint: (file: string) => string | null): string | null;
44
+ export declare function toolFingerprint(bin: string): string | null;
45
+ /**
46
+ * The tool a host runs: the path it was given, else the one found now. The
47
+ * daemon's plan leaves the path out, so a reinstall that briefly removes the
48
+ * tool's link cannot change the plan and restart the session.
49
+ */
50
+ export declare function serviceTool(name: string, given: string | undefined, find: (name: string) => string | null): string;
40
51
  /** A tool updated on disk restarts its session once quiet this long... */
41
52
  export declare const IDLE_BEFORE_RESTART_MS: number;
42
53
  /** ...or after this long, however busy the session is. */
@@ -10,14 +10,18 @@
10
10
  * - the session ends -> the host exits 1 and the daemon restarts it with
11
11
  * backoff (replacing the runner's `while true` loop and its watchdogs);
12
12
  * - the daemon stops the host -> the host kills the session and exits 0;
13
- * - the tool's binary changes on disk (the updater installed a new version) ->
14
- * once the session has been quiet for 10 minutes, or after 2 hours at the
15
- * latest, the host kills the session and exits 0, and the daemon starts it
16
- * again on the new version.
13
+ * - a new version of the tool is installed (the updater, or the tool's own
14
+ * auto-updater) -> once the session has been quiet for 10 minutes, or after
15
+ * 2 hours at the latest, the host kills the session and exits 0, and the
16
+ * daemon starts it again on the new version. Reinstalling the same version
17
+ * (a long-running Claude session's auto-updater does that every few
18
+ * minutes) and hq-cli releases leave the session alone.
17
19
  *
18
20
  * The tmux server is a private socket (`-L hq-daemon`), so it never touches a
19
21
  * user's own tmux sessions.
20
22
  */
23
+ import * as fs from "node:fs";
24
+ import * as path from "node:path";
21
25
  export const TMUX_SOCKET = "hq-daemon";
22
26
  export const TMUX_SESSION = "remote-control";
23
27
  export const CODEX_TMUX_SESSION = "codex-remote-control";
@@ -35,17 +39,63 @@ export function runnerSessionName(script) {
35
39
  const m = /remote-control\b[^\n]*?--name[= ]+"?([^\s"]+)"?/.exec(script);
36
40
  return m ? m[1] : null;
37
41
  }
42
+ /** How far above the binary to look for the package that installs it (bin/claude.exe sits one level down). */
43
+ const PACKAGE_SEARCH_DEPTH = 3;
38
44
  /**
39
- * Fingerprint of what a host runs: the tool first, then other installs it
40
- * depends on (the hq-cli package). Null while the tool cannot be read; an
41
- * unreadable extra install is left out rather than hiding a tool update.
45
+ * Which version of a tool is installed: the npm package that installs the
46
+ * binary (`name@version`), else the binary's real path and size. A native
47
+ * install's link points at a versioned file, so its real path is the version.
48
+ * The mtime is left out on purpose: reinstalling the same version rewrites
49
+ * the file, and that must not restart the session. Null while the tool
50
+ * cannot be read.
42
51
  */
43
- export function installFingerprint(paths, fingerprint) {
44
- const [tool, ...rest] = paths;
45
- const toolFp = tool === undefined ? null : fingerprint(tool);
46
- if (toolFp === null)
52
+ export function toolFingerprint(bin) {
53
+ let real;
54
+ let size;
55
+ try {
56
+ real = fs.realpathSync(bin);
57
+ size = fs.statSync(real).size;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ let dir = path.dirname(real);
63
+ for (let i = 0; i < PACKAGE_SEARCH_DEPTH; i++) {
64
+ const version = packageVersionFor(dir, real);
65
+ if (version)
66
+ return version;
67
+ const parent = path.dirname(dir);
68
+ if (parent === dir)
69
+ break;
70
+ dir = parent;
71
+ }
72
+ return `${real}:${size}`;
73
+ }
74
+ /** `name@version` when `<dir>/package.json` declares `real` as one of its bins. */
75
+ function packageVersionFor(dir, real) {
76
+ let pkg;
77
+ try {
78
+ pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
79
+ }
80
+ catch {
47
81
  return null;
48
- return [toolFp, ...rest.map(fingerprint).filter((fp) => fp !== null)].join("|");
82
+ }
83
+ if (typeof pkg.name !== "string" || typeof pkg.version !== "string")
84
+ return null;
85
+ const bins = typeof pkg.bin === "string" ? [pkg.bin] : pkg.bin && typeof pkg.bin === "object" ? Object.values(pkg.bin) : [];
86
+ const installs = bins.some((b) => typeof b === "string" && path.resolve(dir, b) === real);
87
+ return installs ? `${pkg.name}@${pkg.version}` : null;
88
+ }
89
+ /**
90
+ * The tool a host runs: the path it was given, else the one found now. The
91
+ * daemon's plan leaves the path out, so a reinstall that briefly removes the
92
+ * tool's link cannot change the plan and restart the session.
93
+ */
94
+ export function serviceTool(name, given, find) {
95
+ const tool = given?.trim() || find(name);
96
+ if (!tool)
97
+ throw new Error(`the ${name} CLI is not installed`);
98
+ return tool;
49
99
  }
50
100
  /** A tool updated on disk restarts its session once quiet this long... */
51
101
  export const IDLE_BEFORE_RESTART_MS = 10 * 60_000;
@@ -123,6 +123,8 @@ export interface Supervisor {
123
123
  /** Stop supervising without signalling any child; returns the long-running units left running. */
124
124
  release(): ReleasedUnit[];
125
125
  }
126
+ /** Plan reads in a row that must find a running remote-control host blocked before it is paused. */
127
+ export declare const BLOCKED_READS_BEFORE_PAUSE = 2;
126
128
  export declare function backoffMs(attempt: number): number;
127
129
  /**
128
130
  * What a unit's process was started from; a unit whose key changes is
@@ -33,6 +33,8 @@ const MESH_OWNER_LOG_INTERVAL_MS = 10 * 60_000;
33
33
  export const HANDOFF_STAGGER_MS = 15_000;
34
34
  /** How long a draining unit may take to finish its work before it gets SIGTERM. */
35
35
  export const HANDOFF_DRAIN_MAX_MS = 15 * 60_000;
36
+ /** Plan reads in a row that must find a running remote-control host blocked before it is paused. */
37
+ export const BLOCKED_READS_BEFORE_PAUSE = 2;
36
38
  export function backoffMs(attempt) {
37
39
  return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attempt));
38
40
  }
@@ -505,6 +507,15 @@ export function createSupervisor(deps) {
505
507
  continue;
506
508
  }
507
509
  next.set(plan.id, existing);
510
+ // A remote-control host is what people's sessions run in. Its tool can
511
+ // look missing for one plan read while it is being reinstalled, so it is
512
+ // only paused once the tool is missing on two reads in a row.
513
+ if (plan.blocked && existing.plan.blocked === undefined && existing.child && plan.keepAcrossCliUpdates) {
514
+ existing.blockedReads = (existing.blockedReads ?? 0) + 1;
515
+ if (existing.blockedReads < BLOCKED_READS_BEFORE_PAUSE)
516
+ continue;
517
+ }
518
+ existing.blockedReads = 0;
508
519
  const adopted = existing.adopted === true;
509
520
  existing.adopted = false;
510
521
  const key = planKey(plan, deps.cliVersion);
@@ -10,7 +10,7 @@ import { readDeviceConfig, } from "../../work-context/config.js";
10
10
  import { outboxStats, listOutboxOperations } from "../../work-context/outbox.js";
11
11
  import { outboxQuarantineStatusEnabled } from "../../work-context/outbox-quarantine-status-flag.js";
12
12
  import { workContextRoot, workContextSessionsDir } from "../../work-context/paths.js";
13
- import { countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
13
+ import { countDistinctHeldEvents, countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
14
14
  import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
15
15
  import { collectDaemonDoctor, formatDaemonDoctor, } from "../../mesh/live/daemon/doctor.js";
16
16
  export const WORK_CONTEXT_FAMILY_ID = "work-context";
@@ -139,7 +139,7 @@ function runChecks(deps) {
139
139
  const heldPath = workMeshHeldPath(meshRoot);
140
140
  const deadLetterPath = workMeshDeadLetterPath(meshRoot);
141
141
  const spoolDepth = countJsonlLines(spoolPath);
142
- const heldDepth = countJsonlLines(heldPath);
142
+ const heldDepth = countDistinctHeldEvents(heldPath);
143
143
  results.push({
144
144
  status: "PASS",
145
145
  checkId: "work-context.spool-depth",