@indigoai-us/hq-cli 5.345.112 → 5.345.113

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.113] — 2026-10-13
6
+ - `hq daemon sync now --company <slug>` asks the running sync daemon to sync one company right away, without restarting sync. It needs hq-cloud 6.18.68 or later in the daemon; with an older daemon it falls back to the previous restart. `--json` reports which path it took. The desktop app uses this for Sync on the "Added to" banner.
7
+
8
+ - `hq capacity` no longer ranks a Claude account the ledger marks as stale
9
+ (sign-in expired and no reading since). The router skips it with "no fresh
10
+ reading (sign-in expired)" and prints the account's fix command.
11
+ - A Claude home listed under `accounts` now clears an old revoked marker
12
+ once a new sign-in lands, including when a fresh usage poll would
13
+ otherwise skip reading the credential. A marker that still matches the
14
+ current refresh token keeps the account blocked as revoked.
15
+ - After a token refresh, the account stays blocked as "missing profile
16
+ scope" when the refreshed grant lacks that scope, instead of being
17
+ cleared.
18
+ - The live-lane gate finds the `hq` CLI from the running process instead of
19
+ `PATH`, so the refresh gate works when `PATH` is empty or minimal.
20
+ - `hq capacity doctor` renames the "TOKEN AGE" column to "SINCE REFRESH".
21
+
5
22
  ## [5.345.112] — 2026-10-13
6
23
  - The bundled sync engine now uses hq-cloud 6.18.71. The hq-core update no longer replaces pack-installed worker links (for example `core/workers/public/qa-tester`) with a copy of the release folder.
7
24
 
@@ -15484,6 +15484,10 @@ export declare const COMMAND_CATALOG: readonly [{
15484
15484
  readonly helpText: readonly [];
15485
15485
  readonly arguments: readonly [];
15486
15486
  readonly options: readonly [{
15487
+ readonly flags: "--company <slug>";
15488
+ readonly description: "Sync one company";
15489
+ readonly hidden: false;
15490
+ }, {
15487
15491
  readonly flags: "--json";
15488
15492
  readonly description: "Print machine-readable JSON";
15489
15493
  readonly hidden: false;
@@ -18848,6 +18848,11 @@ export const COMMAND_CATALOG = [
18848
18848
  "helpText": [],
18849
18849
  "arguments": [],
18850
18850
  "options": [
18851
+ {
18852
+ "flags": "--company <slug>",
18853
+ "description": "Sync one company",
18854
+ "hidden": false
18855
+ },
18851
18856
  {
18852
18857
  "flags": "--json",
18853
18858
  "description": "Print machine-readable JSON",
@@ -260,7 +260,7 @@ export function formatDoctor(report) {
260
260
  : report.providers.map((provider) => `${provider.provider} ${provider.state}`).join("\n");
261
261
  const claudeAccounts = report.claudeAccounts.length === 0
262
262
  ? "none"
263
- : formatTable(["ACCOUNT", "LAST ATTEMPT", "OUTCOME", "TOKEN AGE", "EXPIRES IN", "BACKOFF UNTIL"], report.claudeAccounts.map((account) => [
263
+ : formatTable(["ACCOUNT", "LAST ATTEMPT", "OUTCOME", "SINCE REFRESH", "EXPIRES IN", "BACKOFF UNTIL"], report.claudeAccounts.map((account) => [
264
264
  account.label,
265
265
  account.lastRefreshAttempt ?? "—",
266
266
  account.lastRefreshOutcome ?? "—",
@@ -19,6 +19,8 @@ import type { HostApp } from "../lib/daemon/supervisor.js";
19
19
  import { type SyncModeVaultClient } from "./sync-mode.js";
20
20
  import type { RunCommand } from "../lib/service-manager/launchd.js";
21
21
  type DaemonRunnerModule = typeof import("../lib/daemon/run.js");
22
+ /** First hq-cloud release whose watch runner accepts pass-request files. */
23
+ export declare const PASS_REQUEST_MIN_HQ_CLOUD = "6.18.68";
22
24
  /** Load every lazy daemon runtime module before releasing its package-use lease. */
23
25
  export declare function loadDaemonRunner(release?: () => void, load?: () => Promise<DaemonRunnerModule>, preload?: () => Promise<void>, reportPreloadFailure?: (error: unknown) => void): Promise<DaemonRunnerModule["runDaemon"]>;
24
26
  export interface DaemonCommandDeps {
@@ -16,6 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
+ import { randomBytes } from "node:crypto";
19
20
  import { spawn, spawnSync } from "node:child_process";
20
21
  import { once } from "node:events";
21
22
  import { compare as semverCompare, major as semverMajor, valid as validSemver } from "semver";
@@ -38,6 +39,7 @@ import { acknowledgeConflictNotice, conflictNoticeStateDir } from "../lib/daemon
38
39
  import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
39
40
  import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
40
41
  import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
42
+ import { assertSafeCompanySlug } from "../utils/people.js";
41
43
  import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
42
44
  import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
43
45
  import { parseStdinMachineCredentials } from "../lib/daemon/machine-login.js";
@@ -45,6 +47,8 @@ import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
45
47
  import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
46
48
  import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, runResidentCommandWithPackageLease, } from "../utils/resident-package-preload.js";
47
49
  import { followLogFile } from "../lib/log-follow.js";
50
+ /** First hq-cloud release whose watch runner accepts pass-request files. */
51
+ export const PASS_REQUEST_MIN_HQ_CLOUD = "6.18.68";
48
52
  /** Load every lazy daemon runtime module before releasing its package-use lease. */
49
53
  export async function loadDaemonRunner(release = releaseCurrentPackageUseLease, load = () => import("../lib/daemon/run.js"), preload = () => preloadResidentPackageModules("daemon"), reportPreloadFailure = reportResidentPackagePreloadFailure) {
50
54
  const { runDaemon } = await load();
@@ -545,6 +549,11 @@ function daemonSyncStatus(d) {
545
549
  syncOwner: effectiveSyncOwner(config, host.detected.type),
546
550
  owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
547
551
  runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
552
+ passRequestDir: syncUnit?.status === "running"
553
+ && syncUnit.pid !== undefined
554
+ && (ownerStatus === null || ownerStatus.pid === syncUnit.pid)
555
+ ? syncUnit.passRequestDir ?? null
556
+ : null,
548
557
  hqCloudVersion,
549
558
  hqCloudVersionSource,
550
559
  runnerMatchesPin,
@@ -592,6 +601,53 @@ function queueDaemonSyncControl(d, request) {
592
601
  handOver(d, host);
593
602
  return runningDaemonPid(host.paths.lockPath, d.daemonAlive) !== null;
594
603
  }
604
+ function writeSyncPassRequest(paths, company, source) {
605
+ ensurePrivateDir(paths.syncPassRequestsDir);
606
+ const request = {
607
+ version: 1,
608
+ company,
609
+ requestedAt: new Date().toISOString(),
610
+ source,
611
+ };
612
+ const encoded = JSON.stringify(request);
613
+ for (let attempt = 0; attempt < 10; attempt += 1) {
614
+ const basename = `${Date.now()}-${randomBytes(4).toString("hex")}`;
615
+ const temporary = path.join(paths.syncPassRequestsDir, `${basename}.tmp`);
616
+ const destination = path.join(paths.syncPassRequestsDir, `${basename}.json`);
617
+ try {
618
+ fs.writeFileSync(temporary, encoded, { encoding: "utf8", mode: 0o600, flag: "wx" });
619
+ fs.renameSync(temporary, destination);
620
+ return;
621
+ }
622
+ catch (error) {
623
+ try {
624
+ fs.unlinkSync(temporary);
625
+ }
626
+ catch (cleanupError) {
627
+ if (cleanupError.code !== "ENOENT")
628
+ throw cleanupError;
629
+ }
630
+ if (error.code === "EEXIST")
631
+ continue;
632
+ throw error;
633
+ }
634
+ }
635
+ throw new DaemonCommandError("could not allocate a unique sync pass request filename");
636
+ }
637
+ /**
638
+ * The version alone does not prove the runner reads requests: a runner adopted
639
+ * across a CLI update keeps the env it was started with. The daemon records the
640
+ * request folder for a child it started with one.
641
+ */
642
+ function supportsPassRequests(status, paths) {
643
+ const version = status.hqCloudVersion;
644
+ return status.running
645
+ && status.passRequestDir !== null
646
+ && path.resolve(status.passRequestDir) === path.resolve(paths.syncPassRequestsDir)
647
+ && version !== null
648
+ && validSemver(version) !== null
649
+ && semverCompare(version, PASS_REQUEST_MIN_HQ_CLOUD) >= 0;
650
+ }
595
651
  async function runDaemonSyncNow(d, opts) {
596
652
  const host = currentHost(d);
597
653
  const config = readDaemonConfig(host.paths.configPath);
@@ -600,8 +656,20 @@ async function runDaemonSyncNow(d, opts) {
600
656
  if (!enabledServices(host.detected.type, config).includes("sync")) {
601
657
  throw new DaemonCommandError("sync is disabled for this machine; run `hq daemon enable sync` first");
602
658
  }
659
+ const company = opts.company ?? null;
660
+ if (company !== null)
661
+ assertSafeCompanySlug(company);
662
+ const status = daemonSyncStatus(d);
663
+ if (supportsPassRequests(status, host.paths)) {
664
+ const source = d.env.HQ_SYNC_PASS_REQUEST_SOURCE === "desktop" ? "desktop" : "cli";
665
+ writeSyncPassRequest(host.paths, company, source);
666
+ handOver(d, host);
667
+ const result = { accepted: true, action: "pass-request", company, daemonRunning: true };
668
+ d.out(opts.json ? JSON.stringify(result, null, 2) : "Asked the daemon to run a fresh sync pass.");
669
+ return;
670
+ }
603
671
  const running = queueDaemonSyncControl(d, { action: "restart", unit: "sync" });
604
- const result = { accepted: true, action: "restart-sync-runner", daemonRunning: running };
672
+ const result = { accepted: true, action: "restart-sync-runner", company, daemonRunning: running };
605
673
  d.out(opts.json ? JSON.stringify(result, null, 2) : running ? "Asked the daemon to start a fresh sync pass." : "Queued a fresh sync pass for the next daemon start.");
606
674
  }
607
675
  async function runDaemonSyncInstantSyncSet(d, enabled, opts) {
@@ -1336,6 +1404,7 @@ export function registerDaemonCommand(program, over = {}) {
1336
1404
  sync
1337
1405
  .command("now")
1338
1406
  .description("Start a fresh sync pass immediately")
1407
+ .option("--company <slug>", "Sync one company")
1339
1408
  .option("--json", "Print machine-readable JSON")
1340
1409
  .action(wrap((d, opts) => runDaemonSyncNow(d, opts)));
1341
1410
  const instantSync = sync.command("instant-sync").description("Control daemon Instant Sync");
@@ -332,6 +332,22 @@ interface RawAccountRow {
332
332
  auth_state?: unknown;
333
333
  provider?: unknown;
334
334
  }
335
+ /**
336
+ * How to re-invoke the running hq CLI without a PATH lookup. When argv[1] is
337
+ * the entry of an installed hq-cli package, run it with the same node binary;
338
+ * the collector runs under launchd and lane workers where PATH may be empty.
339
+ * Falls back to `hq` on PATH when this process is not the hq CLI (tests,
340
+ * embedders). Exported for tests.
341
+ */
342
+ export declare function resolveHqInvocation(argv?: readonly string[], execPath?: string): {
343
+ command: string;
344
+ args: string[];
345
+ };
346
+ /** Exported for tests; `invocation` defaults to {@link resolveHqInvocation}. */
347
+ export declare function execHqJson(args: readonly string[], invocation?: {
348
+ command: string;
349
+ args: string[];
350
+ }, env?: NodeJS.ProcessEnv): unknown;
335
351
  /**
336
352
  * Parse `hq lanes list --json` payload into typed rows.
337
353
  * Exported for tests; fail-closed semantics live in defaultListLiveLanes.
@@ -251,7 +251,12 @@ export async function collectClaudeOAuth(options = {}) {
251
251
  });
252
252
  if (refreshResult?.outcome === "refreshed" && refreshResult.newAccessToken) {
253
253
  currentToken = refreshResult.newAccessToken;
254
- currentBlocked = undefined;
254
+ // A refresh renews the token but does not grant scopes the sign-in
255
+ // lacked. Re-check the profile scope before clearing the block.
256
+ const refreshedScopes = refreshResult.newScopes && refreshResult.newScopes.length > 0
257
+ ? refreshResult.newScopes
258
+ : meta.scopes;
259
+ currentBlocked = hasProfileScope(refreshedScopes) ? undefined : "unavailable: missing_scope";
255
260
  }
256
261
  else if (refreshResult?.outcome === "revoked" || refreshResult?.outcome === "skipped-revoked") {
257
262
  currentBlocked = SIGN_IN_REVOKED_REASON;
@@ -410,13 +415,17 @@ async function collectEnumeratedClaude(options, scoped, root, clock, cliVersion)
410
415
  if (refreshResult) {
411
416
  if (refreshResult.outcome === "refreshed" && refreshResult.newAccessToken) {
412
417
  account.token = refreshResult.newAccessToken;
413
- account.blocked = null;
414
418
  if (refreshResult.newExpiresAt !== undefined)
415
419
  account.expiresAt = refreshResult.newExpiresAt;
416
420
  if (refreshResult.newRefreshToken)
417
421
  account.refreshToken = refreshResult.newRefreshToken;
418
422
  if (refreshResult.newScopes && refreshResult.newScopes.length > 0)
419
423
  account.scopes = refreshResult.newScopes;
424
+ // A refresh renews the token but does not grant scopes the sign-in
425
+ // lacked. Re-check the profile scope before clearing the block.
426
+ account.blocked = hasProfileScope(account.scopes)
427
+ ? null
428
+ : credentialBlockReason("unavailable: missing_scope");
420
429
  }
421
430
  else if (refreshResult.outcome === "revoked" || refreshResult.outcome === "skipped-revoked") {
422
431
  account.blocked = SIGN_IN_REVOKED_REASON;
@@ -537,7 +546,10 @@ async function readEnumeratedHome(group, options, nowMs, root) {
537
546
  platform,
538
547
  };
539
548
  }
540
- if (pollIsFresh(root, accountId, nowMs)) {
549
+ // A fresh poll normally skips the credential read. When a revoked marker
550
+ // exists, read anyway: the marker check needs the current refresh token to
551
+ // tell a re-sign-in (fingerprint changed) from the same revoked token.
552
+ if (pollIsFresh(root, accountId, nowMs) && revokedMarkerForCurrentCredential(root, accountId, null) === "none") {
541
553
  return {
542
554
  accountId,
543
555
  labels,
@@ -1623,6 +1635,10 @@ const defaultWriteClaudeCredentials = async (request) => {
1623
1635
  return { ok: false, raceDetected: true };
1624
1636
  }
1625
1637
  const body = JSON.stringify(request.document);
1638
+ // `security add-generic-password` only takes the secret as `-w <value>`
1639
+ // on argv, so the credential JSON is visible to `ps` for the duration of
1640
+ // this call. Claude Code writes its own Keychain item the same way; this
1641
+ // matches that exposure rather than adding a new one.
1626
1642
  const writeArgs = selector === "s"
1627
1643
  ? ["add-generic-password", "-U", "-s", request.service, "-w", body]
1628
1644
  : ["add-generic-password", "-U", "-s", request.service, "-a", username, "-w", body];
@@ -1670,8 +1686,44 @@ const defaultWriteClaudeCredentials = async (request) => {
1670
1686
  return { ok: false, error: message };
1671
1687
  }
1672
1688
  };
1673
- function execHqJson(args) {
1674
- const result = spawnSync("hq", [...args], {
1689
+ const HQ_CLI_PACKAGE_NAME = "@indigoai-us/hq-cli";
1690
+ /**
1691
+ * How to re-invoke the running hq CLI without a PATH lookup. When argv[1] is
1692
+ * the entry of an installed hq-cli package, run it with the same node binary;
1693
+ * the collector runs under launchd and lane workers where PATH may be empty.
1694
+ * Falls back to `hq` on PATH when this process is not the hq CLI (tests,
1695
+ * embedders). Exported for tests.
1696
+ */
1697
+ export function resolveHqInvocation(argv = process.argv, execPath = process.execPath) {
1698
+ const entry = argv[1];
1699
+ if (entry) {
1700
+ try {
1701
+ const real = fs.realpathSync(entry);
1702
+ let dir = path.dirname(real);
1703
+ for (let depth = 0; depth < 4; depth += 1) {
1704
+ const manifest = path.join(dir, "package.json");
1705
+ if (fs.existsSync(manifest)) {
1706
+ const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
1707
+ if (parsed.name === HQ_CLI_PACKAGE_NAME)
1708
+ return { command: execPath, args: [real] };
1709
+ break;
1710
+ }
1711
+ const parent = path.dirname(dir);
1712
+ if (parent === dir)
1713
+ break;
1714
+ dir = parent;
1715
+ }
1716
+ }
1717
+ catch {
1718
+ // Unresolvable entry: fall through to the PATH lookup below.
1719
+ }
1720
+ }
1721
+ return { command: "hq", args: [] };
1722
+ }
1723
+ /** Exported for tests; `invocation` defaults to {@link resolveHqInvocation}. */
1724
+ export function execHqJson(args, invocation = resolveHqInvocation(), env = process.env) {
1725
+ const result = spawnSync(invocation.command, [...invocation.args, ...args], {
1726
+ env,
1675
1727
  encoding: "utf8",
1676
1728
  timeout: 5_000,
1677
1729
  windowsHide: true,
@@ -250,6 +250,17 @@ export function pickAccount(input) {
250
250
  });
251
251
  continue;
252
252
  }
253
+ // The ledger sets usageStale when the sign-in expired and no reading has
254
+ // landed since the expiry. The limits on file describe the old token, so
255
+ // the account cannot be ranked on them.
256
+ if (account.usageStale === true) {
257
+ skipped.push({
258
+ ...base,
259
+ reason: "no fresh reading (sign-in expired)",
260
+ fix: account.fixCommand ?? null,
261
+ });
262
+ continue;
263
+ }
253
264
  const applicable = (account.limits ?? []).filter((l) => limitApplies(providerId, l, model));
254
265
  const viewRows = applicable.map((limit) => {
255
266
  const resetMs = limit.resetsAt ? Date.parse(limit.resetsAt) : NaN;
@@ -87,6 +87,8 @@ export interface PlanContext {
87
87
  platform: NodeJS.Platform;
88
88
  home: string;
89
89
  hqRoot: string | undefined;
90
+ /** Daemon-owned request directory consumed by the hq-cloud watch runner. */
91
+ syncPassRequestsDir: string;
90
92
  /** hq-cloud's sync-runner.js, or null when it cannot be found. */
91
93
  syncRunnerPath: string | null;
92
94
  /** hq-cloud's `hq-outpost-heartbeat` bin, when the hq-cloud in use has one. */
@@ -142,7 +142,7 @@ function planService(service, ctx) {
142
142
  SYNC_RUNNER_OWNER,
143
143
  ],
144
144
  },
145
- env: { ...ctx.legacySyncEnv },
145
+ env: { ...ctx.legacySyncEnv, HQ_SYNC_PASS_REQUEST_DIR: ctx.syncPassRequestsDir },
146
146
  heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
147
147
  });
148
148
  let ownerBlock;
@@ -11,6 +11,8 @@ export interface DaemonPaths {
11
11
  lockPath: string;
12
12
  logsDir: string;
13
13
  requestsDir: string;
14
+ /** One-shot sync passes queued for the running hq-cloud watch runner. */
15
+ syncPassRequestsDir: string;
14
16
  supervisorLogPath: string;
15
17
  unitLogPath: (unitId: string) => string;
16
18
  /** The FIFO a unit that speaks the sync protocol writes its stdout to (see protocol-tail.ts); kept out of logs/ so nothing reading logs drains it. */
@@ -17,6 +17,7 @@ export function daemonPaths(home = os.homedir(), env = process.env) {
17
17
  lockPath: path.join(root, "daemon.pid"),
18
18
  logsDir,
19
19
  requestsDir: path.join(root, "requests"),
20
+ syncPassRequestsDir: path.resolve(root, "sync-pass-requests"),
20
21
  supervisorLogPath: path.join(logsDir, "daemon.log"),
21
22
  unitLogPath: (unitId) => path.join(logsDir, `${unitId.replace(/[^A-Za-z0-9_.-]/g, "-")}.log`),
22
23
  unitProtocolPath: (unitId) => path.join(root, "protocol", `${unitId.replace(/[^A-Za-z0-9_.-]/g, "-")}.fifo`),
@@ -451,6 +451,7 @@ export async function runDaemon(opts) {
451
451
  const home = opts.home ?? os.homedir();
452
452
  const platform = process.platform;
453
453
  const paths = daemonPaths(home, env);
454
+ ensurePrivateDir(paths.syncPassRequestsDir);
454
455
  const log = makeLogger(paths, opts.foreground === true);
455
456
  const syncRunnerBundleDirectory = opts.syncRunnerBundleDirectory ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../hq-cloud-sync-runner");
456
457
  const cliPackageRoot = opts.cliPackageRoot === undefined ? resolveRunningInstall().packageRoot : opts.cliPackageRoot;
@@ -835,6 +836,7 @@ export async function runDaemon(opts) {
835
836
  platform,
836
837
  home,
837
838
  hqRoot,
839
+ syncPassRequestsDir: paths.syncPassRequestsDir,
838
840
  syncRunnerPath: runners.syncRunner,
839
841
  heartbeatRunnerPath: runners.heartbeat,
840
842
  sessionHostRunnerPath: runners.sessionHost,
@@ -63,6 +63,8 @@ interface UnitState {
63
63
  lastResult?: "ok" | "failed";
64
64
  lastHeartbeatAt?: string;
65
65
  lastPassResult?: ReturnType<typeof syncPassResult>;
66
+ /** The pass-request folder the running child was started with; absent when it was not. */
67
+ passRequestDir?: string;
66
68
  }
67
69
  /** An app that runs the daemon as its own child process instead of an OS unit. */
68
70
  export type HostApp = "desktop";
@@ -37,6 +37,33 @@ const MESH_OWNER_LOG_INTERVAL_MS = 10 * 60_000;
37
37
  export const HANDOFF_STAGGER_MS = 15_000;
38
38
  /** How long a draining unit may take to finish its work before it gets SIGTERM. */
39
39
  export const HANDOFF_DRAIN_MAX_MS = 15 * 60_000;
40
+ const PASS_REQUEST_DIR_ENV = "HQ_SYNC_PASS_REQUEST_DIR";
41
+ function passRequestDirOf(env) {
42
+ const dir = env?.[PASS_REQUEST_DIR_ENV];
43
+ return dir ? dir : undefined;
44
+ }
45
+ /** An adopted child carries no env, only the key it was started with (see planKey). */
46
+ function passRequestDirFromKey(key) {
47
+ try {
48
+ const fields = JSON.parse(key);
49
+ if (!Array.isArray(fields))
50
+ return undefined;
51
+ const env = fields[6];
52
+ if (!env || typeof env !== "object" || Array.isArray(env))
53
+ return undefined;
54
+ const dir = env[PASS_REQUEST_DIR_ENV];
55
+ return typeof dir === "string" && dir ? dir : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function recordPassRequestDir(state, dir) {
62
+ if (dir)
63
+ state.passRequestDir = dir;
64
+ else
65
+ delete state.passRequestDir;
66
+ }
40
67
  /** Plan reads in a row that must find a running remote-control host blocked before it is paused. */
41
68
  const BLOCKED_READS_BEFORE_PAUSE = 2;
42
69
  function backoffMs(attempt) {
@@ -560,6 +587,7 @@ export function createSupervisor(deps) {
560
587
  }
561
588
  slot.child = child;
562
589
  slot.childKey = slot.key;
590
+ recordPassRequestDir(slot.state, passRequestDirOf(slot.plan.env));
563
591
  slot.childStartedAt = now();
564
592
  slot.state.status = "running";
565
593
  slot.state.startedAt = slot.childStartedAt;
@@ -651,6 +679,7 @@ export function createSupervisor(deps) {
651
679
  }
652
680
  slot.child = child;
653
681
  slot.childKey = slot.key;
682
+ recordPassRequestDir(slot.state, passRequestDirOf(slot.plan.env));
654
683
  slot.childStartedAt = now();
655
684
  slot.state.status = "running";
656
685
  delete slot.state.reason;
@@ -880,6 +909,7 @@ export function createSupervisor(deps) {
880
909
  slot.adopted = true;
881
910
  slot.child = unit.child;
882
911
  slot.childKey = unit.key;
912
+ recordPassRequestDir(slot.state, passRequestDirFromKey(unit.key));
883
913
  slot.childStartedAt = now();
884
914
  slot.state.status = "running";
885
915
  slot.state.startedAt = slot.childStartedAt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.112",
3
+ "version": "5.345.113",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {