@indigoai-us/hq-cli 5.342.8 → 5.342.9

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.342.9] — 2026-10-04
6
+ - Child wrappers now release their package-use lease after startup modules are preloaded, so long-running child processes do not block CLI updates.
7
+ - Keep nested HQ child leases and materialize core-job post-exit helpers before releasing the parent package lease.
8
+ - Run the core migration differential check on pull requests only when migration, build, or differential inputs change, while retaining it on main pushes.
9
+ - Reduce startup work on the fast `hq secrets env` route while keeping the server authorization request on every call.
10
+
5
11
  ## [5.342.8] — 2026-10-04
6
12
  - Keep blocker escalation failures visible on the lane and stop retrying the same failed fallback every watcher turn.
7
13
  - Release the daemon's hq-cli package-use lease after all reachable lazy runtime modules preload successfully, allowing CLI updates while preserving the existing restart-on-update path.
@@ -0,0 +1,8 @@
1
+ export interface CoreJobPostExitHelpers {
2
+ notifyBin: string;
3
+ remediateBin: string;
4
+ cleanup(): void;
5
+ }
6
+ /** Copy package-backed helpers before the runner releases its package-use lease. */
7
+ export declare function materializeCoreJobPostExitHelpers(scriptPath: string, env: NodeJS.ProcessEnv): CoreJobPostExitHelpers;
8
+ //# sourceMappingURL=core-job-post-exit-assets.d.ts.map
@@ -0,0 +1,37 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ /** Copy package-backed helpers before the runner releases its package-use lease. */
5
+ export function materializeCoreJobPostExitHelpers(scriptPath, env) {
6
+ const sourceDir = path.dirname(scriptPath);
7
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-job-post-exit-"));
8
+ const materialize = (name, override) => {
9
+ if (override)
10
+ return override;
11
+ const source = path.join(sourceDir, name);
12
+ const target = path.join(tempDir, name);
13
+ fs.copyFileSync(source, target);
14
+ fs.chmodSync(target, fs.statSync(source).mode & 0o777);
15
+ return target;
16
+ };
17
+ try {
18
+ const notifyBin = materialize("hq-job-notify.sh", env.HQ_JOB_NOTIFY_BIN);
19
+ const remediateBin = materialize("hq-job-remediate.sh", env.HQ_JOB_REMEDIATE_BIN);
20
+ let cleaned = false;
21
+ return {
22
+ notifyBin,
23
+ remediateBin,
24
+ cleanup() {
25
+ if (cleaned)
26
+ return;
27
+ cleaned = true;
28
+ fs.rmSync(tempDir, { recursive: true, force: true });
29
+ },
30
+ };
31
+ }
32
+ catch (error) {
33
+ fs.rmSync(tempDir, { recursive: true, force: true });
34
+ throw error;
35
+ }
36
+ }
37
+ //# sourceMappingURL=core-job-post-exit-assets.js.map
@@ -15,6 +15,7 @@ export type ProcessRequest = {
15
15
  env: NodeJS.ProcessEnv;
16
16
  logPath?: string;
17
17
  timeoutMs?: number;
18
+ onChildStart?: () => void;
18
19
  };
19
20
  type LockLease = {
20
21
  release(): Promise<void>;
@@ -9,6 +9,8 @@ import { resolveLiveRoot, resolveBundledAsset } from "../utils/hq-roots.js";
9
9
  import { runBundledScript } from "../utils/run-bundled-script.js";
10
10
  import { describeCoreCommand } from "./core-command-catalog.js";
11
11
  import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
12
+ import { releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
13
+ import { materializeCoreJobPostExitHelpers } from "./core-job-post-exit-assets.js";
12
14
  const RUN_ASSET = "core/scripts/hq-job-run.sh";
13
15
  const RUNNER_NAME = "hq-job-run";
14
16
  export const OUTPOST_IDENTITY_FILE = "/etc/outpost/codex-identity.env";
@@ -152,6 +154,8 @@ export async function spawnProcess(request) {
152
154
  if (exitFile)
153
155
  fs.writeFileSync(exitFile, `EXIT=${request.env.HQ_JOB_RUN_STUB_EXIT_CODE ?? "0"}\n`);
154
156
  }
157
+ if (nestedAgent || (request.file === "tmux" && request.args[0] === "new-session"))
158
+ request.onChildStart?.();
155
159
  const diagnostic = request.env.HQ_JOB_RUN_STUB_DIAGNOSTIC || "";
156
160
  if (nestedAgent && request.logPath && diagnostic)
157
161
  appendLog(request.logPath, `${diagnostic}\n`);
@@ -179,6 +183,8 @@ export async function spawnProcess(request) {
179
183
  stdio: ["ignore", "pipe", "pipe"],
180
184
  detached: processGroup,
181
185
  });
186
+ if (request.onChildStart)
187
+ child.once("spawn", request.onChildStart);
182
188
  child.once("error", reject);
183
189
  child.stdout.on("data", (chunk) => {
184
190
  if (request.logPath) {
@@ -685,8 +691,8 @@ async function ingestRunStatus(jobId, runAt, runId, exit, failureClass, actions,
685
691
  emitLog(`run status ingest failed job=${jobId} http=${status} (logged; not failing run)`);
686
692
  void logFile;
687
693
  }
688
- async function runHelper(binary, args, cwd, env, deps, logPath, timeoutMs) {
689
- return await deps.runProcess({ file: binary, args, cwd, env, ...(logPath ? { logPath } : {}), ...(timeoutMs ? { timeoutMs } : {}) });
694
+ async function runHelper(binary, args, cwd, env, deps, logPath, timeoutMs, onChildStart) {
695
+ return await deps.runProcess({ file: binary, args, cwd, env, ...(logPath ? { logPath } : {}), ...(timeoutMs ? { timeoutMs } : {}), ...(onChildStart ? { onChildStart } : {}) });
690
696
  }
691
697
  async function callNotify(jobId, jobName, outcome, summary, duration, logPath, failureClass, nextAction, owner, notifyMode, hqRoot, notifyBin, deps) {
692
698
  if (deps.env.HQ_JOB_RUN_NO_NOTIFY === "1") {
@@ -876,7 +882,7 @@ function bashPrintfQuote(value) {
876
882
  }
877
883
  return [...value].map((character) => safe.test(character) ? character : `\\${character}`).join("");
878
884
  }
879
- async function runRemoteClaude(jobId, runtime, prompt, runCwd, timeoutSeconds, stamp, logDir, logFile, deps) {
885
+ async function runRemoteClaude(jobId, runtime, prompt, runCwd, timeoutSeconds, stamp, logDir, logFile, deps, onChildStart) {
880
886
  if (!pathExecutable("tmux", deps.env) && deps.env.HQ_JOB_RUN_STUBS !== "1") {
881
887
  deps.appendFile(logFile, "remote surface requested but tmux missing — falling back to headless\n");
882
888
  return 99;
@@ -900,7 +906,7 @@ async function runRemoteClaude(jobId, runtime, prompt, runCwd, timeoutSeconds, s
900
906
  const tmuxEnv = deps.env.HQ_JOB_RUN_STUBS === "1"
901
907
  ? { ...deps.env, HQ_JOB_RUN_REMOTE_DONE_FILE: doneFile, HQ_JOB_RUN_REMOTE_EXIT_FILE: exitFile }
902
908
  : deps.env;
903
- const runTmux = async (args) => await runHelper("tmux", args, deps.cwd, tmuxEnv, deps);
909
+ const runTmux = async (args) => await runHelper("tmux", args, deps.cwd, tmuxEnv, deps, undefined, undefined, args[0] === "new-session" ? onChildStart : undefined);
904
910
  await runTmux(["kill-session", "-t", session]);
905
911
  await runTmux(["new-session", "-d", "-s", session, "-x", "120", "-y", "40", "-c", runCwd, tmuxCommand]);
906
912
  let elapsedSeconds = 0;
@@ -945,13 +951,16 @@ async function runRemoteClaude(jobId, runtime, prompt, runCwd, timeoutSeconds, s
945
951
  await runTmux(["kill-session", "-t", session]);
946
952
  return code;
947
953
  }
948
- async function runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps) {
954
+ async function runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, canReleasePackageLease = true) {
949
955
  if (deps.env.HQ_JOB_RUN_SKIP_EXEC === "1") {
950
956
  deps.appendFile(logFile, "(HQ_JOB_RUN_SKIP_EXEC=1 — not invoking runtime CLI)\n");
951
957
  return 0;
952
958
  }
953
959
  if (runtime === "claude" && surface === "remote") {
954
- const remoteCode = await runRemoteClaude(jobId, runtime, prompt, runCwd, timeoutSeconds, stamp, logDir, logFile, deps);
960
+ const remoteCode = await runRemoteClaude(jobId, runtime, prompt, runCwd, timeoutSeconds, stamp, logDir, logFile, deps, () => {
961
+ if (canReleasePackageLease)
962
+ releaseChildWrapperPackageUseLeaseAfterStart("coreJobRun");
963
+ });
955
964
  if (remoteCode !== 99)
956
965
  return remoteCode;
957
966
  }
@@ -979,7 +988,10 @@ async function runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secret
979
988
  else if (secretNames.length > 0) {
980
989
  deps.appendFile(logFile, "warning: requirements.secrets set but hq CLI missing — running without secrets exec\n");
981
990
  }
982
- const result = await runHelper(file, args, runCwd, deps.env, deps, logFile, timeoutMs);
991
+ const result = await runHelper(file, args, runCwd, deps.env, deps, logFile, timeoutMs, () => {
992
+ if (canReleasePackageLease)
993
+ releaseChildWrapperPackageUseLeaseAfterStart("coreJobRun", file === "hq");
994
+ });
983
995
  return result.code;
984
996
  }
985
997
  async function recordSkip(jobId, jobName, runtime, skipReason, failureClass, action, statusSuffix, notifyOutcome, owner, notifyMode, company, hqRoot, logFile, statusFile, startedAt, startedEpoch, logDir, stamp, notifyBin, remediateBin, deps, authStatus = "") {
@@ -1089,12 +1101,19 @@ export async function runJob(argv, hqRootOverride, injected) {
1089
1101
  catch (error) {
1090
1102
  return emitDie(`failed to acquire global run lock: ${lockPath}${error instanceof Error && error.message.includes("flock") ? ` (${error.message})` : ""}`);
1091
1103
  }
1104
+ let postExitHelpers = null;
1092
1105
  try {
1093
1106
  const startedAt = isoNow(deps);
1094
1107
  const startedEpoch = deps.epochNow();
1095
1108
  deps.appendFile(logFile, `=== hq-job-run ${jobId} ===\nstarted_at=${startedAt}\nruntime=${runtime}\ntimeout_seconds=${timeoutSeconds}\nsurface=${surface}\njob_file=${jobFile}\nhq_root=${hqRoot}\n`);
1096
- const notifyBin = env.HQ_JOB_NOTIFY_BIN || path.join(path.dirname(deps.scriptPath), "hq-job-notify.sh");
1097
- const remediateBin = env.HQ_JOB_REMEDIATE_BIN || path.join(path.dirname(deps.scriptPath), "hq-job-remediate.sh");
1109
+ try {
1110
+ postExitHelpers = materializeCoreJobPostExitHelpers(deps.scriptPath, env);
1111
+ }
1112
+ catch (error) {
1113
+ deps.appendFile(logFile, `post_exit_helpers_unavailable=${error instanceof Error ? error.name : "Error"}; package lease retained\n`);
1114
+ }
1115
+ const notifyBin = postExitHelpers?.notifyBin ?? env.HQ_JOB_NOTIFY_BIN ?? path.join(path.dirname(deps.scriptPath), "hq-job-notify.sh");
1116
+ const remediateBin = postExitHelpers?.remediateBin ?? env.HQ_JOB_REMEDIATE_BIN ?? path.join(path.dirname(deps.scriptPath), "hq-job-remediate.sh");
1098
1117
  const authStatus = readAuthStatus(runtime, env, deps.home, deps);
1099
1118
  if (authStatus !== "ok") {
1100
1119
  const reason = authStatus === "no_cli"
@@ -1116,7 +1135,7 @@ export async function runJob(argv, hqRootOverride, injected) {
1116
1135
  return emitDie(error instanceof Error ? error.message : String(error));
1117
1136
  }
1118
1137
  const secretNames = jobSecretNames(job);
1119
- let exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps);
1138
+ let exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, postExitHelpers !== null);
1120
1139
  let failureClass = "";
1121
1140
  let nextAction = "";
1122
1141
  let retried = 0;
@@ -1138,7 +1157,7 @@ export async function runJob(argv, hqRootOverride, injected) {
1138
1157
  retried = 1;
1139
1158
  deps.appendFile(logFile, "remediate_retry=1\n");
1140
1159
  emitLog(`self-remediate recommended retry for ${failureClass} — retrying once`);
1141
- exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps);
1160
+ exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, postExitHelpers !== null);
1142
1161
  if (exitCode !== 0) {
1143
1162
  failureClass = classifyFailure(exitCode, logFile, "", deps);
1144
1163
  deps.appendFile(logFile, `failure_class_after_retry=${failureClass}\n`);
@@ -1172,6 +1191,7 @@ export async function runJob(argv, hqRootOverride, injected) {
1172
1191
  }
1173
1192
  finally {
1174
1193
  await lease.release();
1194
+ postExitHelpers?.cleanup();
1175
1195
  }
1176
1196
  }
1177
1197
  export function registerCoreJobRunCommand(job) {
@@ -12,6 +12,8 @@ import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
12
12
  import { assertEnvGraphLoaded, resolveEnvValuesOrThrow, EnvGraphLoadError, EnvResolutionError, } from '../run/env-graph-guard.js';
13
13
  import { hadFatalRejection } from '../unhandled-rejection-boundary.js';
14
14
  import { withRunSecretReadTelemetry } from './run-feature-telemetry.js';
15
+ import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from '../utils/child-wrapper-lease.js';
16
+ import { runChildWrapperExitCode } from '../utils/child-wrapper-exit.js';
15
17
  const SECRET_LOAD_TIMEOUT_MS = 30_000;
16
18
  export async function buildRunUsage(scriptPath, scriptId) {
17
19
  if (scriptId && !scriptPath) {
@@ -165,6 +167,7 @@ export function registerRunCommand(program) {
165
167
  stdio: 'inherit',
166
168
  env: { ...process.env, ...resolvedEnv },
167
169
  });
170
+ child.once('spawn', () => releaseChildWrapperPackageUseLeaseAfterStart("childWrapper", isHqCliChild(childCmd, restArgs)));
168
171
  child.on('error', (err) => {
169
172
  process.stderr.write(`Error: failed to start command '${childCmd}': ${err.message}\n`);
170
173
  process.exit(1);
@@ -177,7 +180,7 @@ export function registerRunCommand(program) {
177
180
  // which this explicit exit would otherwise erase. Force a non-zero
178
181
  // code so such a rejection is never masked by a child that exited 0,
179
182
  // while still propagating a genuine non-zero child status.
180
- process.exit(hadFatalRejection() ? (code || 1) : (code ?? 1));
183
+ process.exit(runChildWrapperExitCode(code, hadFatalRejection()));
181
184
  });
182
185
  }
183
186
  catch (err) {
@@ -3,6 +3,8 @@ export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecr
3
3
  import chalk from "chalk";
4
4
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
5
  import { removeCacheEntry, clearAllCache } from "../utils/secrets-cache.js";
6
+ import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
7
+ import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
6
8
  import { isPlanGateError } from "../utils/plan-gate-error.js";
7
9
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
8
10
  import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
@@ -1136,6 +1138,7 @@ export function registerSecretsCommand(program) {
1136
1138
  stdio: "inherit",
1137
1139
  env: { ...process.env, ...secretEnv },
1138
1140
  });
1141
+ child.once("spawn", () => releaseChildWrapperPackageUseLeaseAfterStart("childWrapper", isHqCliChild(childCmd, childCmdArgs)));
1139
1142
  child.on("error", (err) => {
1140
1143
  console.error(chalk.red(`Failed to start command '${childCmd}': ${err.message}`));
1141
1144
  process.exit(1);
@@ -1144,7 +1147,7 @@ export function registerSecretsCommand(program) {
1144
1147
  if (signal) {
1145
1148
  process.kill(process.pid, signal);
1146
1149
  }
1147
- process.exit(code ?? 1);
1150
+ process.exit(secretsExecChildExitCode(code));
1148
1151
  });
1149
1152
  }
1150
1153
  catch (err) {
@@ -0,0 +1,5 @@
1
+ /** Preserve the existing hq run child-exit policy for use by the close handler. */
2
+ export declare function runChildWrapperExitCode(code: number | null, fatalRejection: boolean): number;
3
+ /** Preserve hq secrets exec's direct child exit-code propagation. */
4
+ export declare function secretsExecChildExitCode(code: number | null): number;
5
+ //# sourceMappingURL=child-wrapper-exit.d.ts.map
@@ -0,0 +1,9 @@
1
+ /** Preserve the existing hq run child-exit policy for use by the close handler. */
2
+ export function runChildWrapperExitCode(code, fatalRejection) {
3
+ return fatalRejection ? (code || 1) : (code ?? 1);
4
+ }
5
+ /** Preserve hq secrets exec's direct child exit-code propagation. */
6
+ export function secretsExecChildExitCode(code) {
7
+ return code ?? 1;
8
+ }
9
+ //# sourceMappingURL=child-wrapper-exit.js.map
@@ -0,0 +1,5 @@
1
+ import { type ResidentPackagePath } from "./resident-package-preload.js";
2
+ export declare function isHqCliChild(command: string, args?: readonly string[]): boolean;
3
+ /** A child wrapper can release its own lease only after its child has started. */
4
+ export declare function releaseChildWrapperPackageUseLeaseAfterStart(residentPath: ResidentPackagePath, childIsHqCli?: boolean): void;
5
+ //# sourceMappingURL=child-wrapper-lease.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "./package-use-lease.js";
2
+ import { preloadResidentPackageModules, reportResidentPackagePreloadFailure } from "./resident-package-preload.js";
3
+ import path from "node:path";
4
+ export function isHqCliChild(command, args = []) {
5
+ const base = path.basename(command).toLowerCase();
6
+ if (base === "hq" || base === "hq.exe" || base === "hq.cmd" || base === "hq.bat")
7
+ return true;
8
+ return /(?:^|[\\/])(?:dist[\\/]bin[\\/]hq|hq)\.(?:c|m)?js$/i.test(args[0] ?? "");
9
+ }
10
+ /** A child wrapper can release its own lease only after its child has started. */
11
+ export function releaseChildWrapperPackageUseLeaseAfterStart(residentPath, childIsHqCli = false) {
12
+ // Nested HQ commands must acquire their own lease before an updater can
13
+ // replace the tree. Keep this process's lease through that child's lifetime.
14
+ if (childIsHqCli)
15
+ return;
16
+ void releasePackageUseLeaseAfterResidentPreload(() => preloadResidentPackageModules(residentPath), reportResidentPackagePreloadFailure, releaseCurrentPackageUseLease);
17
+ }
18
+ //# sourceMappingURL=child-wrapper-lease.js.map
@@ -1,4 +1,4 @@
1
- export type ResidentPackagePath = "bot" | "mcp" | "daemon";
1
+ export type ResidentPackagePath = "bot" | "mcp" | "daemon" | "childWrapper" | "coreJobRun";
2
2
  /** Package imports reachable lazily from each resident command's runtime graph. */
3
3
  export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
4
4
  export declare class ResidentPackagePreloadError extends Error {
@@ -1,10 +1,21 @@
1
1
  /** Package imports reachable lazily from each resident command's runtime graph. */
2
2
  export const RESIDENT_PACKAGE_MODULES = {
3
- bot: ["../lib/flag-registry.js", "../commands/access.js", "../sentry.js", "open"],
3
+ bot: [
4
+ "../lib/flag-registry.js",
5
+ "../commands/access.js",
6
+ "../sentry.js",
7
+ "../lib/plan-limit-nag.js",
8
+ "../utils/plan-gate-error.js",
9
+ "open",
10
+ ],
4
11
  mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js"],
12
+ childWrapper: ["../main.js"],
13
+ coreJobRun: ["../lib/flag-registry.js"],
5
14
  daemon: [
6
15
  "../sentry.js",
7
16
  "../utils/cognito-session.js",
17
+ "../lib/plan-limit-nag.js",
18
+ "../utils/plan-gate-error.js",
8
19
  "../lib/daemon/mcp-oauth.js",
9
20
  "@modelcontextprotocol/sdk/client/index.js",
10
21
  "@modelcontextprotocol/sdk/client/streamableHttp.js",
@@ -20,18 +31,28 @@ const LOADERS = {
20
31
  const { preloadSentrySdk } = await import("../sentry.js");
21
32
  preloadSentrySdk();
22
33
  },
34
+ () => import("../lib/plan-limit-nag.js"),
35
+ () => import("../utils/plan-gate-error.js"),
23
36
  () => import("open"),
24
37
  ],
25
38
  mcp: [
26
39
  () => import("../lib/mcp/tools.js"),
27
40
  () => import("../lib/mcp/prompts.js"),
28
41
  ],
42
+ childWrapper: [
43
+ () => import("../main.js"),
44
+ ],
45
+ coreJobRun: [
46
+ () => import("../lib/flag-registry.js"),
47
+ ],
29
48
  daemon: [
30
49
  async () => {
31
50
  const { preloadSentrySdk } = await import("../sentry.js");
32
51
  preloadSentrySdk();
33
52
  },
34
53
  () => import("../utils/cognito-session.js"),
54
+ () => import("../lib/plan-limit-nag.js"),
55
+ () => import("../utils/plan-gate-error.js"),
35
56
  () => import("../lib/daemon/mcp-oauth.js"),
36
57
  () => import("@modelcontextprotocol/sdk/client/index.js"),
37
58
  () => import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
@@ -1,11 +1,8 @@
1
1
  import { DEFAULT_VAULT_API_URL, cognitoCachePrincipalUid } from './cognito-session.js';
2
2
  import { CLI_VERSION } from '../cli-version.js';
3
- import { Sentry } from '../sentry.js';
4
3
  import { AuthError } from './auth-error.js';
5
4
  import { CompanySelectionError } from './company-selection-error.js';
6
- import { recordPlanLimitStatus } from '../lib/plan-limit-nag.js';
7
5
  import { networkTransportErrorCode } from './network-transport-error.js';
8
- import { planGateErrorFromResponse } from './plan-gate-error.js';
9
6
  import { markExpectedUserError } from './expected-cli-error.js';
10
7
  import { redactErrorText } from './redact-error-text.js';
11
8
  import { isSecretLoadUnavailable, isUnavailableSecretLoadHttpStatus, SecretLoadUnavailableError } from './secret-load-availability.js';
@@ -24,6 +21,10 @@ import { readCachedScopeUid, writeCachedScopeUid } from './secrets-cache.js';
24
21
  */
25
22
  const IDENTITY_LOOKUP_TIMEOUT_MS = 15_000;
26
23
  const LAMBDA_INVOKE_RETRY_MAX_DELAY_MS = 100;
24
+ async function addVaultBreadcrumb(breadcrumb) {
25
+ const { Sentry } = await import('../sentry.js');
26
+ Sentry.addBreadcrumb(breadcrumb);
27
+ }
27
28
  function isLambdaInvokeServiceError(response, bodyText) {
28
29
  if (response.status !== 504) {
29
30
  return false;
@@ -128,7 +129,11 @@ async function peekPlanLimitStatus(response, options = {}) {
128
129
  try {
129
130
  const text = new TextDecoder().decode(buf);
130
131
  try {
131
- recordPlanLimitStatus(JSON.parse(text));
132
+ const parsed = JSON.parse(text);
133
+ if (hasPlanLimitStatusPayload(parsed)) {
134
+ const { recordPlanLimitStatus } = await import('../lib/plan-limit-nag.js');
135
+ recordPlanLimitStatus(parsed);
136
+ }
132
137
  }
133
138
  catch {
134
139
  // Malformed JSON must be ignored silently.
@@ -162,6 +167,12 @@ async function peekPlanLimitStatus(response, options = {}) {
162
167
  return response;
163
168
  }
164
169
  }
170
+ function hasPlanLimitStatusPayload(body) {
171
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
172
+ return false;
173
+ }
174
+ return Object.hasOwn(body, 'planLimits') || Object.hasOwn(body, 'thresholdCrossings');
175
+ }
165
176
  /**
166
177
  * Map Cognito JWT paths onto parallel Bearer `hqk_` routes under `/v1/keys/…`.
167
178
  * Returns null when no keyed route exists (caller must fail closed).
@@ -252,7 +263,7 @@ export async function vaultApiFetch(opts) {
252
263
  const { path, url } = resolveVaultApiRequestTarget(opts);
253
264
  const method = opts.method ?? 'GET';
254
265
  const safeUrl = url.search ? `${url.origin}${url.pathname}?<redacted>` : `${url.origin}${url.pathname}`;
255
- Sentry.addBreadcrumb({
266
+ await addVaultBreadcrumb({
256
267
  category: "http",
257
268
  message: `${method} ${path}`,
258
269
  level: "info",
@@ -282,7 +293,7 @@ export async function vaultApiFetch(opts) {
282
293
  // Keep the original transport failure and its bounded, redacted trail on
283
294
  // both the first request and the single retry.
284
295
  const code = networkTransportErrorCode(err);
285
- Sentry.addBreadcrumb({
296
+ await addVaultBreadcrumb({
286
297
  category: "http",
287
298
  message: `${method} ${path} → transport error`,
288
299
  level: "warning",
@@ -293,7 +304,7 @@ export async function vaultApiFetch(opts) {
293
304
  };
294
305
  const response = await fetchWithLambdaInvokeRetry(fetchWithTransportBreadcrumb, method.toUpperCase() === 'GET' && opts.body === undefined, opts.signal);
295
306
  if (!response.ok) {
296
- Sentry.addBreadcrumb({
307
+ await addVaultBreadcrumb({
297
308
  category: "http",
298
309
  message: `${method} ${path} → ${response.status}`,
299
310
  level: "warning",
@@ -308,7 +319,9 @@ export async function vaultApiFetch(opts) {
308
319
  const companyUid = typeof opts.body?.companyUid === "string"
309
320
  ? opts.body.companyUid
310
321
  : /\/(cmp_[^/?]+)/.exec(opts.path)?.[1];
311
- const planGate = await planGateErrorFromResponse(response, companyUid);
322
+ const planGate = response.status === 402
323
+ ? await (await import('./plan-gate-error.js')).planGateErrorFromResponse(response, companyUid)
324
+ : { error: null, response };
312
325
  if (planGate.error)
313
326
  throw planGate.error;
314
327
  return planGate.response;
@@ -346,7 +359,7 @@ export async function vaultApiFetchPublic(opts) {
346
359
  const safeUrl = url.search
347
360
  ? `${url.origin}${url.pathname}?<redacted>`
348
361
  : `${url.origin}${url.pathname}`;
349
- Sentry.addBreadcrumb({
362
+ await addVaultBreadcrumb({
350
363
  category: 'http',
351
364
  message: `GET ${opts.path}`,
352
365
  level: 'info',
@@ -357,7 +370,7 @@ export async function vaultApiFetchPublic(opts) {
357
370
  headers: { 'Content-Type': 'application/json' },
358
371
  }), true);
359
372
  if (!response.ok) {
360
- Sentry.addBreadcrumb({
373
+ await addVaultBreadcrumb({
361
374
  category: 'http',
362
375
  message: `GET ${opts.path} → ${response.status}`,
363
376
  level: 'warning',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.342.8",
3
+ "version": "5.342.9",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {