@indigoai-us/hq-cli 5.319.0 → 5.320.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.320.0] — 2026-10-02
6
+ - Write session-journal titles as escaped YAML strings so free-form titles remain parseable.
7
+ - Added a default-off hq-flags gate that lets lane list pollers skip remote owner refreshes to reduce poll latency.
8
+ - Publish per-process package-use leases so Desktop updates can wait for active hq-cli work without native locks.
9
+
5
10
  ## [5.319.0] — 2026-10-02
6
11
  - Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
7
12
  - Lead autopilot on an Outpost can wake leads that were created on that Outpost, behind the default-off `lanes.autopilot-outpost-native-leads` flag. Before, those leads were skipped as a host mismatch and got no wakes.
@@ -43,6 +43,41 @@ slugify() {
43
43
  | cut -c1-40
44
44
  }
45
45
 
46
+ yaml_double_quoted() {
47
+ local value="$1" output='"' character next codepoint hex
48
+ local LC_ALL=C index
49
+ for ((index = 0; index < ${#value}; index++)); do
50
+ character="${value:index:1}"
51
+ if [[ "$character" == $'\xc2' ]] && (( index + 1 < ${#value} )); then
52
+ next="${value:index+1:1}"
53
+ printf -v codepoint '%d' "'${next}"
54
+ if (( codepoint >= 128 && codepoint <= 159 )); then
55
+ printf -v hex '%04x' "$codepoint"
56
+ output+="\\u$hex"
57
+ index=$((index + 1))
58
+ continue
59
+ fi
60
+ fi
61
+ case "$character" in
62
+ \\) output+='\\' ;;
63
+ '"') output+='\"' ;;
64
+ $'\n') output+='\n' ;;
65
+ $'\r') output+='\r' ;;
66
+ $'\t') output+='\t' ;;
67
+ *)
68
+ if [[ "$character" == [[:cntrl:]] ]]; then
69
+ printf -v codepoint '%d' "'${character}"
70
+ printf -v hex '%02x' "$codepoint"
71
+ output+="\\x$hex"
72
+ else
73
+ output+="$character"
74
+ fi
75
+ ;;
76
+ esac
77
+ done
78
+ printf '%s"' "$output"
79
+ }
80
+
46
81
  # Counters are per-session. A date-global counter lets concurrent Claude and
47
82
  # Codex tasks trip (and reset) each other's journal reminders.
48
83
  counter_key() {
@@ -140,6 +175,9 @@ case "$cmd" in
140
175
 
141
176
  slug="$(slugify "$title")"
142
177
  [ -z "$slug" ] && slug="entry"
178
+ yaml_title="$(yaml_double_quoted "$title")"
179
+ yaml_slug="$slug"
180
+ [[ "$slug" != *$'\n'* ]] || yaml_slug="$(yaml_double_quoted "$slug")"
143
181
  JOURNAL_WRITE_LOCK=""
144
182
  acquire_journal_write_lock "$dir" || { warn "write: failed to acquire journal lock"; exit 0; }
145
183
  trap release_journal_write_lock EXIT
@@ -161,8 +199,8 @@ case "$cmd" in
161
199
  {
162
200
  printf -- '---\n'
163
201
  printf 'ts: %s\n' "$(now_iso_z)"
164
- printf 'title: %s\n' "$title"
165
- printf 'slug: %s\n' "$slug"
202
+ printf 'title: %s\n' "$yaml_title"
203
+ printf 'slug: %s\n' "$yaml_slug"
166
204
  if [ -n "$files_csv" ]; then
167
205
  printf 'files:\n'
168
206
  old_ifs="$IFS"
@@ -11,7 +11,9 @@
11
11
  * non-interactively.
12
12
  */
13
13
  import "../node-preflight.js";
14
+ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
14
15
  import "../node-network-compat.js";
15
- import { refreshCachedSession } from "../utils/cognito-session.js";
16
+ import type { refreshCachedSession } from "../utils/cognito-session.js";
16
17
  export declare function runAuthRefresh(refreshSession?: typeof refreshCachedSession): Promise<number>;
18
+ export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
17
19
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -13,15 +13,16 @@
13
13
  // MUST be first: guard the Node version before any dependency that needs a
14
14
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
15
15
  import "../node-preflight.js";
16
+ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
16
17
  import "../node-network-compat.js";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import path from "node:path";
19
- import { finishSentrySession, initSentry, Sentry } from "../sentry.js";
20
- import { refreshCachedSession } from "../utils/cognito-session.js";
21
- export async function runAuthRefresh(refreshSession = refreshCachedSession) {
20
+ export async function runAuthRefresh(refreshSession) {
21
+ const { finishSentrySession, Sentry } = await import("../sentry.js");
22
+ const { refreshCachedSession: refresh } = await import("../utils/cognito-session.js");
22
23
  let exitCode = 1;
23
24
  try {
24
- const result = await refreshSession();
25
+ const result = await (refreshSession ?? refresh)();
25
26
  if (result.refreshed) {
26
27
  exitCode = 0;
27
28
  }
@@ -41,8 +42,42 @@ export async function runAuthRefresh(refreshSession = refreshCachedSession) {
41
42
  }
42
43
  return exitCode;
43
44
  }
44
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
45
+ function isPackageUseUpdatePendingError(error) {
46
+ return Boolean(error &&
47
+ typeof error === "object" &&
48
+ error.name === "PackageUseUpdatePendingError");
49
+ }
50
+ function packageUseErrorClass(error) {
51
+ if (!error || typeof error !== "object")
52
+ return "unknown";
53
+ const value = error;
54
+ if (typeof value.code === "string")
55
+ return value.code;
56
+ if (value.cause && typeof value.cause === "object") {
57
+ const cause = value.cause;
58
+ if (typeof cause.code === "string")
59
+ return cause.code;
60
+ if (typeof cause.name === "string")
61
+ return cause.name;
62
+ }
63
+ return typeof value.name === "string" ? value.name : "unknown";
64
+ }
65
+ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
66
+ try {
67
+ acquireLease();
68
+ }
69
+ catch (error) {
70
+ if (isPackageUseUpdatePendingError(error)) {
71
+ process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
72
+ return 1;
73
+ }
74
+ process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
75
+ }
76
+ const { initSentry } = await import("../sentry.js");
45
77
  initSentry();
46
- void runAuthRefresh().then((exitCode) => process.exit(exitCode));
78
+ return runAuthRefresh();
79
+ }
80
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
81
+ void startAuthRefresh().then((exitCode) => process.exit(exitCode));
47
82
  }
48
83
  //# sourceMappingURL=hq-auth-refresh.js.map
@@ -24,7 +24,35 @@ const counterKey = (value) => {
24
24
  return key.length > 96 ? key.slice(0, 96) : key;
25
25
  };
26
26
  const slugify = (value) => value.toLowerCase()
27
- .replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40);
27
+ .split("\n")
28
+ .map((line) => line.replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40))
29
+ .join("\n");
30
+ function yamlDoubleQuoted(value) {
31
+ let escaped = "";
32
+ for (const character of value) {
33
+ const codePoint = character.codePointAt(0) ?? 0;
34
+ if (character === "\\")
35
+ escaped += "\\\\";
36
+ else if (character === '"')
37
+ escaped += '\\"';
38
+ else if (character === "\n")
39
+ escaped += "\\n";
40
+ else if (character === "\r")
41
+ escaped += "\\r";
42
+ else if (character === "\t")
43
+ escaped += "\\t";
44
+ else if (codePoint < 0x20 || codePoint === 0x7f) {
45
+ escaped += `\\x${codePoint.toString(16).padStart(2, "0")}`;
46
+ }
47
+ else if (codePoint >= 0x80 && codePoint <= 0x9f) {
48
+ escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
49
+ }
50
+ else {
51
+ escaped += character;
52
+ }
53
+ }
54
+ return `"${escaped}"`;
55
+ }
28
56
  const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
29
57
  const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
30
58
  const hasFile = (file) => {
@@ -211,6 +239,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
211
239
  return 0;
212
240
  }
213
241
  const slug = slugify(title) || "entry";
242
+ const yamlSlug = slug.includes("\n") ? yamlDoubleQuoted(slug) : slug;
214
243
  const body = bodyFile && hasFile(bodyFile)
215
244
  ? fs.readFileSync(bodyFile, "utf8")
216
245
  : "## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n";
@@ -226,7 +255,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
226
255
  for (let attempt = 0; attempt < 10_000; attempt += 1) {
227
256
  seq = nextSequence(dir);
228
257
  output = path.join(dir, `${seq}-${slug}.md`);
229
- const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${title}\n`, `slug: ${slug}\n`];
258
+ const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${yamlDoubleQuoted(title)}\n`, `slug: ${yamlSlug}\n`];
230
259
  if (files) {
231
260
  lines.push("files:\n");
232
261
  for (const file of files.split(","))
@@ -35,7 +35,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
35
35
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
36
36
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
37
37
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
38
- import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG } from "../lib/lanes/flag-keys.js";
38
+ import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, } from "../lib/lanes/flag-keys.js";
39
39
  import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
40
40
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
41
41
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -1337,6 +1337,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1337
1337
  updated: new Map(),
1338
1338
  unverifiedRemote: new Map(),
1339
1339
  missingOnOwner: new Map(),
1340
+ skippedRemoteRefresh: new Set(),
1340
1341
  };
1341
1342
  const now = ctx.now();
1342
1343
  const nowMs = now.getTime();
@@ -1378,6 +1379,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1378
1379
  if (candidates.length === 0)
1379
1380
  return result;
1380
1381
  const enabledByCompany = new Map();
1382
+ const skipRefreshByCompany = new Map();
1381
1383
  const enabledCandidates = [];
1382
1384
  for (const lane of candidates) {
1383
1385
  const company = resolveLaneCompany(hqRoot, lane).company;
@@ -1392,8 +1394,20 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1392
1394
  });
1393
1395
  enabledByCompany.set(companyKey, enabled);
1394
1396
  }
1395
- if (enabled)
1396
- enabledCandidates.push(lane);
1397
+ if (!enabled)
1398
+ continue;
1399
+ if (options.skipRemoteRefreshForCompany) {
1400
+ let skipRefresh = skipRefreshByCompany.get(companyKey);
1401
+ if (skipRefresh === undefined) {
1402
+ skipRefresh = await options.skipRemoteRefreshForCompany(company);
1403
+ skipRefreshByCompany.set(companyKey, skipRefresh);
1404
+ }
1405
+ if (skipRefresh) {
1406
+ result.skippedRemoteRefresh.add(lane.lane_id);
1407
+ continue;
1408
+ }
1409
+ }
1410
+ enabledCandidates.push(lane);
1397
1411
  }
1398
1412
  if (enabledCandidates.length === 0)
1399
1413
  return result;
@@ -4950,7 +4964,12 @@ async function runList(ctx, opts) {
4950
4964
  }
4951
4965
  const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
4952
4966
  const remoteRefresh = remoteRead
4953
- ? { updated: new Map(), unverifiedRemote: new Map(), missingOnOwner: new Map() }
4967
+ ? {
4968
+ updated: new Map(),
4969
+ unverifiedRemote: new Map(),
4970
+ missingOnOwner: new Map(),
4971
+ skippedRemoteRefresh: new Set(),
4972
+ }
4954
4973
  : await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(laneSource
4955
4974
  .filter((lane) => matchesLaneFilters(lane, false))
4956
4975
  .map((lane) => lane.lane_id)), {
@@ -4958,6 +4977,7 @@ async function runList(ctx, opts) {
4958
4977
  laneIds: new Set(laneSource
4959
4978
  .filter((lane) => matchesLaneFilters(lane, false))
4960
4979
  .map((lane) => lane.lane_id)),
4980
+ skipRemoteRefreshForCompany: (company) => readLanesRegistryFlagFailOff(LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, { company, env: ctx.env }),
4961
4981
  });
4962
4982
  for (const [laneId, updated] of remoteRefresh.updated) {
4963
4983
  const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
@@ -4995,6 +5015,7 @@ async function runList(ctx, opts) {
4995
5015
  const ownerEligibleLaneIds = new Set();
4996
5016
  for (const lane of selectedLanes) {
4997
5017
  if (remoteRead ||
5018
+ remoteRefresh.skippedRemoteRefresh.has(lane.lane_id) ||
4998
5019
  lane.host.kind !== "outpost" ||
4999
5020
  isTerminalLaneState(lane.state) ||
5000
5021
  laneRunsHere(lane, host)) {
@@ -5034,6 +5055,7 @@ async function runList(ctx, opts) {
5034
5055
  const livenessBySession = new Map();
5035
5056
  let livenessDeps;
5036
5057
  const lanes = selectedLanes.map((lane) => {
5058
+ const overdue = Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now);
5037
5059
  const remoteQueued = lane.state === "queued" &&
5038
5060
  lane.host.kind === "outpost" &&
5039
5061
  !laneRunsHere(lane, host);
@@ -5052,7 +5074,7 @@ async function runList(ctx, opts) {
5052
5074
  queued_details: lane.queued_reason === "low_disk" ? lane.queued_details : undefined,
5053
5075
  }
5054
5076
  : {}),
5055
- overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
5077
+ overdue,
5056
5078
  stalled: Boolean(lane.stalled),
5057
5079
  // Listing is observational. Reconcile owns durable liveness transitions.
5058
5080
  senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
@@ -7873,6 +7895,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7873
7895
  updated: new Map(),
7874
7896
  unverifiedRemote: new Map(),
7875
7897
  missingOnOwner: new Map(),
7898
+ skippedRemoteRefresh: new Set(),
7876
7899
  }
7877
7900
  : await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot));
7878
7901
  const remoteLost = [];
package/dist/index.d.ts CHANGED
@@ -3,8 +3,17 @@ import "./node-preflight.js";
3
3
  import "./node-network-compat-fast.js";
4
4
  import { isVersionRequest } from "./version-request.js";
5
5
  import { isFastCoreRequest } from "./fast-scaffold.js";
6
+ type PackageUseLeaseLoader = () => Promise<void>;
7
+ declare function isNpmPackageEntry(entryUrl: string): boolean;
8
+ declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
9
+ declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
6
10
  export declare const __test__: {
7
11
  isVersionRequest: typeof isVersionRequest;
8
12
  isFastCoreRequest: typeof isFastCoreRequest;
13
+ dispatchWithPackageUseLease: typeof dispatchWithPackageUseLease;
14
+ runCliEntry: typeof runCliEntry;
15
+ isNpmPackageEntry: typeof isNpmPackageEntry;
16
+ acquirePackageUseLease: (prefix: string, version: string, stateDirectory?: string) => Promise<import("./utils/package-use-lease.js").PackageUseLease>;
9
17
  };
18
+ export {};
10
19
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -17,77 +17,149 @@ import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.
17
17
  // command never run (HQ-CLI-W). Sets process.exitCode only, never process.exit,
18
18
  // so runCli's finally still finalizes release health and flushes Sentry.
19
19
  installProcessRejectionBoundary();
20
- if (isVersionRequest(process.argv)) {
21
- // Bare `--version` is the CLI semver only (the contract field updaters
22
- // compare). `hq version` and `--json` print the labelled HQ/CLI report.
23
- // Lazy so this path still skips the command graph.
24
- if (wantsLabelledVersionReport(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));
20
+ function dispatchCliEntry() {
21
+ if (isVersionRequest(process.argv)) {
22
+ // Bare `--version` is the CLI semver only (the contract field updaters
23
+ // compare). `hq version` and `--json` print the labelled HQ/CLI report.
24
+ // Lazy so this path still skips the command graph.
25
+ if (wantsLabelledVersionReport(process.argv)) {
26
+ void import("./lib/flag-registry.js").then(({ kickFlagRegistryReadiness }) => {
27
+ kickFlagRegistryReadiness();
28
+ return import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
29
+ });
30
+ }
31
+ else {
32
+ // Keep the benchmarked updater fast route limited to the installed semver.
33
+ // Registry checks and flag resolution belong to the regular command path.
34
+ void import("./cli-version.js").then(({ CLI_VERSION }) => {
35
+ process.stdout.write(`${CLI_VERSION}\n`);
36
+ });
37
+ }
38
+ }
39
+ else if (parseFastCore(process.argv)?.entry.kind === "native" &&
40
+ (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
41
+ process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
42
+ // An explicit enabled override has already decided the global gate. Skip the
43
+ // snapshot reader import on hot native plumbing paths; runFastCore still
44
+ // checks the command-family flag before choosing native or shell rollback.
45
+ void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
46
+ }
47
+ else if (parseFastCore(process.argv)?.entry.kind === "native") {
48
+ // Native routes are default-off. Resolve the company-scoped persisted flag
49
+ // without loading the command graph; a miss or stale snapshot takes the
50
+ // existing full-graph route.
51
+ void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
52
+ if (await nativeFastRouteEnabled(process.argv)) {
53
+ const { runFastCore } = await import("./fast-core.js");
54
+ await runFastCore(process.argv);
55
+ }
56
+ else {
57
+ const { runCli } = await import("./main.js");
58
+ await runCli();
59
+ }
28
60
  });
29
61
  }
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.
33
- void import("./cli-version.js").then(({ CLI_VERSION }) => {
34
- process.stdout.write(`${CLI_VERSION}\n`);
62
+ else if (parseSecretsEnvFastRequest(process.argv)) {
63
+ const request = parseSecretsEnvFastRequest(process.argv);
64
+ void import("./secrets-env-fast-route.js").then(async (route) => {
65
+ await route.dispatchSecretsEnvFastRequest(request, {
66
+ isEnabled: () => route.secretsEnvFastRouteEnabled(request),
67
+ runFast: async () => {
68
+ const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
69
+ return runSecretsEnvFastPath(request);
70
+ },
71
+ runCurrent: async () => {
72
+ const { runCli } = await import("./main.js");
73
+ await runCli();
74
+ },
75
+ });
35
76
  });
36
77
  }
78
+ else if (isFastCoreRequest(process.argv)) {
79
+ // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
80
+ // dispatches straight to the bundled-script runner, so it never pays for the
81
+ // ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
82
+ void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
83
+ }
84
+ else {
85
+ // Keep the command lifecycle detached from module evaluation, as it was
86
+ // before the fast --version split. Some best-effort teardown work uses
87
+ // unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
88
+ // successful command into exit 13 when that teardown promise remains pending.
89
+ // A floated rejection here no longer exits 0 silently: the boundary installed
90
+ // above turns it into a non-zero exit (HQ-CLI-W).
91
+ void import("./main.js").then(({ runCli }) => runCli());
92
+ }
37
93
  }
38
- else if (parseFastCore(process.argv)?.entry.kind === "native" &&
39
- (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
40
- process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
41
- // An explicit enabled override has already decided the global gate. Skip the
42
- // snapshot reader import on hot native plumbing paths; runFastCore still
43
- // checks the command-family flag before choosing native or shell rollback.
44
- void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
94
+ function isNpmPackageEntry(entryUrl) {
95
+ return /\/node_modules\/@indigoai-us\/hq-cli\/dist\/index\.js$/i.test(entryUrl);
45
96
  }
46
- else if (parseFastCore(process.argv)?.entry.kind === "native") {
47
- // Native routes are default-off. Resolve the company-scoped persisted flag
48
- // without loading the command graph; a miss or stale snapshot takes the
49
- // existing full-graph route.
50
- void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
51
- if (await nativeFastRouteEnabled(process.argv)) {
52
- const { runFastCore } = await import("./fast-core.js");
53
- await runFastCore(process.argv);
54
- }
55
- else {
56
- const { runCli } = await import("./main.js");
57
- await runCli();
58
- }
59
- });
97
+ function isPackageUseUpdatePendingError(error) {
98
+ return Boolean(error &&
99
+ typeof error === "object" &&
100
+ error.name === "PackageUseUpdatePendingError");
60
101
  }
61
- else if (parseSecretsEnvFastRequest(process.argv)) {
62
- const request = parseSecretsEnvFastRequest(process.argv);
63
- void import("./secrets-env-fast-route.js").then(async (route) => {
64
- await route.dispatchSecretsEnvFastRequest(request, {
65
- isEnabled: () => route.secretsEnvFastRouteEnabled(request),
66
- runFast: async () => {
67
- const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
68
- return runSecretsEnvFastPath(request);
69
- },
70
- runCurrent: async () => {
71
- const { runCli } = await import("./main.js");
72
- await runCli();
73
- },
74
- });
75
- });
102
+ function packageUseErrorClass(error) {
103
+ if (!error || typeof error !== "object")
104
+ return "unknown";
105
+ const value = error;
106
+ if (typeof value.code === "string")
107
+ return value.code;
108
+ if (value.cause && typeof value.cause === "object") {
109
+ const cause = value.cause;
110
+ if (typeof cause.code === "string")
111
+ return cause.code;
112
+ if (typeof cause.name === "string")
113
+ return cause.name;
114
+ }
115
+ return typeof value.name === "string" ? value.name : "unknown";
76
116
  }
77
- else if (isFastCoreRequest(process.argv)) {
78
- // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
79
- // dispatches straight to the bundled-script runner, so it never pays for the
80
- // ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
81
- void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
117
+ function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () => {
118
+ const { acquireCurrentPackageUseLease } = await import("./utils/package-use-lease.js");
119
+ acquireCurrentPackageUseLease();
120
+ }) {
121
+ if (!isNpmPackageEntry(entryUrl)) {
122
+ dispatch();
123
+ return;
124
+ }
125
+ return loadLease().then(dispatch, (error) => {
126
+ if (isPackageUseUpdatePendingError(error))
127
+ throw error;
128
+ process.stderr.write(`hq-cli package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
129
+ dispatch();
130
+ });
82
131
  }
83
- else {
84
- // Keep the command lifecycle detached from module evaluation, as it was
85
- // before the fast --version split. Some best-effort teardown work uses
86
- // unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
87
- // successful command into exit 13 when that teardown promise remains pending.
88
- // A floated rejection here no longer exits 0 silently: the boundary installed
89
- // above turns it into a non-zero exit (HQ-CLI-W).
90
- void import("./main.js").then(({ runCli }) => runCli());
132
+ function runCliEntry(entryUrl, dispatch, loadLease) {
133
+ const handlePendingUpdate = (error) => {
134
+ if (!isPackageUseUpdatePendingError(error))
135
+ throw error;
136
+ const message = error instanceof Error
137
+ ? error.message
138
+ : "The HQ CLI package is being updated";
139
+ process.stderr.write(`hq-cli: ${message}\n`);
140
+ process.exitCode = 1;
141
+ };
142
+ try {
143
+ const result = dispatchWithPackageUseLease(entryUrl, dispatch, loadLease);
144
+ return result?.catch(handlePendingUpdate);
145
+ }
146
+ catch (error) {
147
+ handlePendingUpdate(error);
148
+ }
91
149
  }
92
- export const __test__ = { isVersionRequest, isFastCoreRequest };
150
+ // Local builds and benchmark fixtures are not installed in an npm prefix.
151
+ // Keep their fast route free of package-use filesystem work; npm installs still
152
+ // publish a lease before dispatch.
153
+ void runCliEntry(import.meta.url, dispatchCliEntry);
154
+ export const __test__ = {
155
+ isVersionRequest,
156
+ isFastCoreRequest,
157
+ dispatchWithPackageUseLease,
158
+ runCliEntry,
159
+ isNpmPackageEntry,
160
+ acquirePackageUseLease: async (prefix, version, stateDirectory) => {
161
+ const lease = await import("./utils/package-use-lease.js");
162
+ return lease.acquirePackageUseLease(prefix, version, stateDirectory);
163
+ },
164
+ };
93
165
  //# sourceMappingURL=index.js.map
@@ -12,6 +12,8 @@ import { type HostApp } from "./supervisor.js";
12
12
  export declare const LOG_ROTATE_BYTES: number;
13
13
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
14
14
  export declare const RESTART_EXIT_CODE = 75;
15
+ /** Route an updater request through the daemon's existing graceful handoff. */
16
+ export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
15
17
  export interface RunDaemonOptions {
16
18
  foreground?: boolean;
17
19
  /** Started by the OS unit, which relaunches the daemon whenever it exits. */
@@ -12,6 +12,7 @@ import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
14
14
  import { CLI_VERSION } from "../../cli-version.js";
15
+ import { currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
15
16
  import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
16
17
  import { agentKitPaths } from "../agent-kit/paths.js";
17
18
  import { readBotConfig } from "../bot/config.js";
@@ -39,6 +40,11 @@ import { userToolDirs } from "./tools.js";
39
40
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
40
41
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
41
42
  export const RESTART_EXIT_CODE = 75;
43
+ /** Route an updater request through the daemon's existing graceful handoff. */
44
+ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
45
+ if (requested)
46
+ restartDaemon();
47
+ }
42
48
  const LEGACY_PROBE_TTL_MS = 60_000;
43
49
  /** A flag read that has not answered by then counts as off. */
44
50
  const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
@@ -603,7 +609,10 @@ export async function runDaemon(opts) {
603
609
  log("info", `rotated ${n} log file(s)`);
604
610
  return true;
605
611
  },
606
- takeRequests: () => takeControlRequests(paths.requestsDir),
612
+ takeRequests: () => {
613
+ handoffForPackageUpdateRequest(currentPackageUseUpdateRequested(), restartDaemon);
614
+ return takeControlRequests(paths.requestsDir);
615
+ },
607
616
  restartDaemon: () => restartDaemon(),
608
617
  writeState: (state) => writeJsonAtomic(paths.statePath, state),
609
618
  log,
@@ -4,6 +4,9 @@ export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-bala
4
4
  export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
5
5
  /** Count only children with live work against their senior's hire grant. */
6
6
  export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
7
+ /** Skips owner refreshes during ordinary lane list polling when explicitly enabled. */
8
+ export declare const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
9
+ export declare const LANE_LIST_SKIP_REMOTE_REFRESH_DEFAULT_ENABLED = false;
7
10
  /** Enables missed-deadline lead nudges and second-miss operator escalation. */
8
11
  export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
9
12
  export declare const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
@@ -4,6 +4,9 @@ export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
4
4
  export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
5
5
  /** Count only children with live work against their senior's hire grant. */
6
6
  export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
7
+ /** Skips owner refreshes during ordinary lane list polling when explicitly enabled. */
8
+ export const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
9
+ export const LANE_LIST_SKIP_REMOTE_REFRESH_DEFAULT_ENABLED = false;
7
10
  /** Enables missed-deadline lead nudges and second-miss operator escalation. */
8
11
  export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
9
12
  export const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
@@ -0,0 +1,25 @@
1
+ export declare class PackageUseUpdatePendingError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface PackageUseLease {
5
+ readonly leasePath: string;
6
+ release(): void;
7
+ }
8
+ export interface PackageUseLeaseRecord {
9
+ pid: number;
10
+ start_time_ms: number;
11
+ hq_version: string;
12
+ }
13
+ export declare function npmPrefix(root: string, platform?: NodeJS.Platform): string | null;
14
+ export declare function packageUseLeaseDirectory(prefix: string, stateDirectory?: string): string;
15
+ export declare function packageUseUpdateRequestPath(prefix: string, stateDirectory?: string): string;
16
+ declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
17
+ export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string): PackageUseLease;
18
+ export declare function acquireCurrentPackageUseLease(): PackageUseLease | null;
19
+ /** True while the desktop updater owns the per-prefix update request. */
20
+ export declare function currentPackageUseUpdateRequested(): boolean;
21
+ export declare const __test__: {
22
+ waitForUpdateRequest: typeof waitForUpdateRequest;
23
+ };
24
+ export {};
25
+ //# sourceMappingURL=package-use-lease.d.ts.map
@@ -0,0 +1,213 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { performance } from "node:perf_hooks";
7
+ const CLI_NAME = "@indigoai-us/hq-cli";
8
+ const UPDATE_REQUEST = "update.pending.json";
9
+ export class PackageUseUpdatePendingError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "PackageUseUpdatePendingError";
13
+ }
14
+ }
15
+ function errorLabel(error) {
16
+ if (error && typeof error === "object") {
17
+ const value = error;
18
+ if (typeof value.code === "string")
19
+ return value.code;
20
+ if (typeof value.name === "string")
21
+ return value.name;
22
+ }
23
+ return "unknown error";
24
+ }
25
+ function readPackage(directory) {
26
+ try {
27
+ return JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8"));
28
+ }
29
+ catch (error) {
30
+ const code = error.code;
31
+ if (code === "ENOENT" || code === "ENOTDIR")
32
+ return null;
33
+ throw new Error(`Unable to inspect the HQ CLI install (${errorLabel(error)})`, { cause: error });
34
+ }
35
+ }
36
+ function findCliPackage(directory) {
37
+ while (true) {
38
+ const pkg = readPackage(directory);
39
+ if (pkg?.name === CLI_NAME) {
40
+ return { root: directory, version: typeof pkg.version === "string" ? pkg.version : "unknown" };
41
+ }
42
+ const parent = path.dirname(directory);
43
+ if (parent === directory)
44
+ return null;
45
+ directory = parent;
46
+ }
47
+ }
48
+ function isPnpmGlobal(root) {
49
+ const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "");
50
+ const parts = normalized.split("/").filter(Boolean);
51
+ for (let i = 0; i < parts.length - 1; i += 1) {
52
+ if (parts[i] === "global" && (/^\d+$/.test(parts[i + 1]) || /^v\d+$/.test(parts[i + 1])))
53
+ return true;
54
+ }
55
+ const home = process.env.PNPM_HOME?.replace(/\\/g, "/").replace(/\/+$/, "");
56
+ const globalRoot = home ? `${home}/global` : null;
57
+ const left = process.platform === "win32" ? normalized.toLowerCase() : normalized;
58
+ const right = globalRoot && process.platform === "win32" ? globalRoot.toLowerCase() : globalRoot;
59
+ return Boolean(right && (left === right || left.startsWith(`${right}/`)));
60
+ }
61
+ function isBunGlobal(root) {
62
+ const parts = root.replace(/\\/g, "/").split("/").filter(Boolean);
63
+ return parts.some((part, i) => part === "install" && parts[i + 1] === "global" && parts[i + 2] === "node_modules");
64
+ }
65
+ export function npmPrefix(root, platform = process.platform) {
66
+ const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "");
67
+ const parts = normalized.split("/");
68
+ const index = parts.lastIndexOf("node_modules");
69
+ if (index < 0)
70
+ return null;
71
+ const end = parts[index - 1] === "lib" ? index - 1 : index;
72
+ const prefix = parts.slice(0, end).join("/");
73
+ if (platform === "win32" && /^[a-z]:$/i.test(prefix))
74
+ return `${prefix}/`;
75
+ if (!prefix && normalized.startsWith("/"))
76
+ return "/";
77
+ return prefix || null;
78
+ }
79
+ function packageUseStateDirectory() {
80
+ if (process.platform === "win32") {
81
+ const localAppData = process.env.LOCALAPPDATA;
82
+ if (!localAppData)
83
+ throw new Error("LOCALAPPDATA is unavailable for the HQ CLI package-use lease");
84
+ return path.join(localAppData, "hq-cli", "state", "package-use");
85
+ }
86
+ return path.join(process.env.XDG_STATE_HOME?.trim() || path.join(os.homedir(), ".local", "state"), "hq-cli", "package-use");
87
+ }
88
+ function normalizedPrefix(prefix) {
89
+ let canonical = realpathSync.native(prefix).replace(/\\/g, "/");
90
+ canonical = canonical.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/, "");
91
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
92
+ }
93
+ export function packageUseLeaseDirectory(prefix, stateDirectory = packageUseStateDirectory()) {
94
+ const digest = createHash("sha256").update(normalizedPrefix(prefix), "utf8").digest("hex");
95
+ return path.join(stateDirectory, digest);
96
+ }
97
+ export function packageUseUpdateRequestPath(prefix, stateDirectory) {
98
+ return path.join(packageUseLeaseDirectory(prefix, stateDirectory), UPDATE_REQUEST);
99
+ }
100
+ function waitForUpdateRequest(requestPath, timeoutMs) {
101
+ const started = Date.now();
102
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
103
+ while (true) {
104
+ try {
105
+ const request = JSON.parse(readFileSync(requestPath, "utf8"));
106
+ const pid = request.pid;
107
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
108
+ throw new Error("invalid update request");
109
+ }
110
+ try {
111
+ process.kill(pid, 0);
112
+ }
113
+ catch (error) {
114
+ if (error.code === "ESRCH") {
115
+ rmSync(requestPath, { force: true });
116
+ return;
117
+ }
118
+ }
119
+ }
120
+ catch (error) {
121
+ if (error.code === "ENOENT")
122
+ return;
123
+ throw new Error(`Unable to read the HQ CLI update request (${errorLabel(error)})`, { cause: error });
124
+ }
125
+ if (Date.now() - started >= timeoutMs) {
126
+ throw new PackageUseUpdatePendingError("The HQ CLI package is being updated; retry after the update finishes");
127
+ }
128
+ Atomics.wait(sleeper, 0, 0, 100);
129
+ }
130
+ }
131
+ export function acquirePackageUseLease(prefix, hqVersion, stateDirectory) {
132
+ const directory = packageUseLeaseDirectory(prefix, stateDirectory);
133
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
134
+ const requestPath = path.join(directory, UPDATE_REQUEST);
135
+ const record = {
136
+ pid: process.pid,
137
+ start_time_ms: Math.floor(performance.timeOrigin),
138
+ hq_version: hqVersion,
139
+ };
140
+ const leasePath = path.join(directory, `${record.pid}-${record.start_time_ms}.json`);
141
+ const tempPath = `${leasePath}.${Math.random().toString(36).slice(2)}.tmp`;
142
+ let released = false;
143
+ const release = () => {
144
+ if (released)
145
+ return;
146
+ released = true;
147
+ process.removeListener("exit", releaseOnExit);
148
+ try {
149
+ rmSync(leasePath, { force: true });
150
+ }
151
+ catch (error) {
152
+ process.stderr.write(`hq-cli package-use lease release failed (${errorLabel(error)})\n`);
153
+ }
154
+ };
155
+ const releaseOnExit = () => release();
156
+ for (let attempt = 0; attempt < 2; attempt += 1) {
157
+ waitForUpdateRequest(requestPath, 30_000);
158
+ try {
159
+ writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { flag: "wx", mode: 0o600 });
160
+ renameSync(tempPath, leasePath);
161
+ }
162
+ catch (error) {
163
+ rmSync(tempPath, { force: true });
164
+ throw new Error(`Unable to publish the HQ CLI package-use lease (${errorLabel(error)})`, { cause: error });
165
+ }
166
+ if (!exists(requestPath)) {
167
+ process.once("exit", releaseOnExit);
168
+ return { leasePath, release };
169
+ }
170
+ rmSync(leasePath, { force: true });
171
+ }
172
+ throw new PackageUseUpdatePendingError("An HQ CLI update is waiting for the package lease; retry when it finishes");
173
+ }
174
+ function exists(file) {
175
+ try {
176
+ readFileSync(file);
177
+ return true;
178
+ }
179
+ catch (error) {
180
+ if (error.code === "ENOENT")
181
+ return false;
182
+ throw error;
183
+ }
184
+ }
185
+ export function acquireCurrentPackageUseLease() {
186
+ const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
187
+ if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
188
+ return null;
189
+ const prefix = npmPrefix(pkg.root);
190
+ if (!prefix)
191
+ return null;
192
+ if (process.platform !== "win32") {
193
+ const parts = pkg.root.replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
194
+ const nodeModules = parts.lastIndexOf("node_modules");
195
+ if (nodeModules > 0 && parts[nodeModules - 1] !== "lib")
196
+ return null;
197
+ }
198
+ return acquirePackageUseLease(prefix, pkg.version);
199
+ }
200
+ /** True while the desktop updater owns the per-prefix update request. */
201
+ export function currentPackageUseUpdateRequested() {
202
+ const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
203
+ if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
204
+ return false;
205
+ const prefix = npmPrefix(pkg.root);
206
+ if (!prefix)
207
+ return false;
208
+ return exists(packageUseUpdateRequestPath(prefix));
209
+ }
210
+ export const __test__ = {
211
+ waitForUpdateRequest,
212
+ };
213
+ //# sourceMappingURL=package-use-lease.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.319.0",
3
+ "version": "5.320.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {