@indigoai-us/hq-cli 5.345.46 → 5.345.48

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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/scaffold/core/scripts/hq-session.sh +113 -7
  3. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
  4. package/dist/command-catalog.generated.d.ts +4 -0
  5. package/dist/command-catalog.generated.js +5 -0
  6. package/dist/commands/core-hq-session-native.d.ts +2 -0
  7. package/dist/commands/core-hq-session-native.js +242 -52
  8. package/dist/commands/core-resolve-company-native.d.ts +1 -0
  9. package/dist/commands/core-resolve-company-native.js +41 -11
  10. package/dist/commands/files.js +4 -1
  11. package/dist/commands/install-global.d.ts +2 -0
  12. package/dist/commands/install-global.js +38 -4
  13. package/dist/commands/secrets.d.ts +5 -0
  14. package/dist/commands/secrets.js +181 -2
  15. package/dist/commands/workers.d.ts +4 -1
  16. package/dist/commands/workers.js +57 -15
  17. package/dist/lib/cli-flag-keys.d.ts +1 -0
  18. package/dist/lib/cli-flag-keys.js +1 -0
  19. package/dist/lib/core-utils/session-id.d.ts +6 -0
  20. package/dist/lib/core-utils/session-id.js +35 -0
  21. package/dist/lib/daemon/catalog.js +1 -1
  22. package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
  23. package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
  24. package/dist/lib/daemon/run.d.ts +6 -0
  25. package/dist/lib/daemon/run.js +36 -9
  26. package/dist/lib/daemon/supervisor.d.ts +7 -1
  27. package/dist/lib/daemon/supervisor.js +50 -4
  28. package/dist/lib/install/provider-wrappers.d.ts +9 -0
  29. package/dist/lib/install/provider-wrappers.js +190 -0
  30. package/dist/lib/lanes/provider-rejection.d.ts +5 -5
  31. package/dist/lib/lanes/provider-rejection.js +15 -10
  32. package/dist/lib/lanes/spawn.js +9 -4
  33. package/dist/lib/lanes/watcher.d.ts +6 -2
  34. package/dist/lib/lanes/watcher.js +37 -17
  35. package/dist/utils/resident-package-preload.js +4 -0
  36. package/dist/utils/self-update.js +8 -1
  37. package/dist/utils/version-gate.d.ts +7 -0
  38. package/dist/utils/version-gate.js +37 -0
  39. package/package.json +1 -1
@@ -106,11 +106,19 @@ export function classifyLaneExit(input) {
106
106
  throw new LanesError("unknown_envelope_decision", `Unknown envelope decision ${JSON.stringify(input.envelope.decision)}. Accepted: done, ask, blocked`, { accepted: ["done", "ask", "blocked"], value: input.envelope.decision });
107
107
  }
108
108
  if (input.providerRejection) {
109
+ const rejection = input.providerRejection;
110
+ const reason = isProviderLoginFailure(rejection)
111
+ ? "account_not_logged_in"
112
+ : rejection.status === 402
113
+ ? "provider_billing_required"
114
+ : rejection.status === 401 || rejection.status === 403
115
+ ? "provider_auth_failed"
116
+ : rejection.status === 429 || /\b(?:rate limit|too many requests)\b/i.test(rejection.message)
117
+ ? "provider_rate_limited"
118
+ : PROVIDER_REJECTED_REASON;
109
119
  return {
110
- state: "failed",
111
- reason: isProviderLoginFailure(input.providerRejection)
112
- ? "account_not_logged_in"
113
- : PROVIDER_REJECTED_REASON,
120
+ state: rejection.retryable && input.hasProviderSession === true ? "interrupted" : "failed",
121
+ reason,
114
122
  };
115
123
  }
116
124
  if (input.invalidEnvelope) {
@@ -129,15 +137,14 @@ export function classifyLaneExit(input) {
129
137
  }
130
138
  /**
131
139
  * One decision for an exited worker's output. An explicit provider status
132
- * wins over the quota text heuristics: a 429 from any provider (including
133
- * codex `turn.failed` and grok error objects, which the claude-shaped quota
134
- * reader never sees) becomes a quota signal; any other explicit status is a
135
- * rejection even when its text sounds like a quota message (a 402 "requires
136
- * usage credits" is a rejection, not a hold). A failure with no status at
137
- * all is matched against the quota text heuristics first: a status-less
138
- * "usage limit" / "rate limit" turn.failed still creates the account hold;
139
- * any other status-less message is a rejection. Only when the stream
140
- * carries no failure object does the quota reader's own read apply.
140
+ * wins over the quota text heuristics: a 429 with quota-specific text or a
141
+ * usage-limit error type becomes an account hold; a generic rate-limit
142
+ * response stays a retryable provider rejection. Any other explicit status
143
+ * is a rejection even when its text sounds like a quota message (a 402
144
+ * "requires usage credits" is a billing refusal, not a hold). A status-less
145
+ * usage-limit message can create an account hold, while a status-less
146
+ * rate-limit message remains a retryable provider rejection. Only when the
147
+ * stream carries no failure object does the quota reader's own read apply.
141
148
  */
142
149
  function validFailureOutputTime(value, fallback) {
143
150
  const parsed = new Date(value);
@@ -146,21 +153,31 @@ function validFailureOutputTime(value, fallback) {
146
153
  : fallback;
147
154
  }
148
155
  export function routeProviderFailure(failure, readQuota, model, now) {
149
- if (failure && failure.status === QUOTA_STATUS) {
156
+ if (failure && failure.status === QUOTA_STATUS && isAccountQuotaFailure(failure)) {
150
157
  return {
151
158
  quota: quotaSignalFromFields({ api_error_status: QUOTA_STATUS, is_error: true, result: failure.message }, model, failure.output_at ? validFailureOutputTime(failure.output_at, now) : now),
152
159
  rejection: null,
153
160
  };
154
161
  }
155
- if (failure && failure.status === null) {
162
+ if (failure && failure.status === null && isAccountQuotaFailure(failure)) {
156
163
  const quota = quotaSignalFromFields({ api_error_status: undefined, is_error: true, result: failure.message }, model, failure.output_at ? validFailureOutputTime(failure.output_at, now) : now);
157
164
  if (quota)
158
165
  return { quota, rejection: null };
159
166
  }
160
- if (failure)
161
- return { quota: null, rejection: failure };
167
+ if (failure) {
168
+ return {
169
+ quota: null,
170
+ rejection: failure.status === QUOTA_STATUS
171
+ ? { ...failure, retryable: true }
172
+ : failure,
173
+ };
174
+ }
162
175
  return { quota: readQuota(), rejection: null };
163
176
  }
177
+ function isAccountQuotaFailure(failure) {
178
+ return failure.error_type === "usage_limit_reached" ||
179
+ /weekly limit|usage limit|quota|usage credits|hit your .+ limit/i.test(failure.message);
180
+ }
164
181
  export function tickWatcher(input) {
165
182
  const silenceMs = input.silenceMs ?? SILENCE_MS;
166
183
  const at = new Date(input.nowMs).toISOString();
@@ -171,6 +188,7 @@ export function tickWatcher(input) {
171
188
  quota: input.quota,
172
189
  invalidEnvelope: input.invalidEnvelope,
173
190
  providerRejection: input.providerRejection,
191
+ hasProviderSession: input.hasProviderSession,
174
192
  });
175
193
  return {
176
194
  emit: { kind: "terminal", at, classification },
@@ -1578,6 +1596,8 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1578
1596
  quota,
1579
1597
  invalidEnvelope,
1580
1598
  providerRejection,
1599
+ hasProviderSession: typeof quotaLane?.provider_session_id === "string" &&
1600
+ quotaLane.provider_session_id.trim().length > 0,
1581
1601
  });
1582
1602
  if (processAlive) {
1583
1603
  maybeHeartbeatLane(hqRoot, laneId, now());
@@ -83,6 +83,8 @@ export const RESIDENT_PACKAGE_MODULES = {
83
83
  "../utils/cli-telemetry.js",
84
84
  "../utils/cognito-session.js",
85
85
  "../lib/first-sync-next-step.js",
86
+ "../lib/process-flag-company-context.js",
87
+ "../lib/flag-snapshot-file.js",
86
88
  "@indigoai-us/hq-cloud",
87
89
  ],
88
90
  };
@@ -352,6 +354,8 @@ const LOADERS = {
352
354
  () => import("../utils/cli-telemetry.js"),
353
355
  () => import("../utils/cognito-session.js"),
354
356
  () => import("../lib/first-sync-next-step.js"),
357
+ () => import("../lib/process-flag-company-context.js"),
358
+ () => import("../lib/flag-snapshot-file.js"),
355
359
  () => import("@indigoai-us/hq-cloud"),
356
360
  ],
357
361
  };
@@ -65,7 +65,7 @@ import chalk from "chalk";
65
65
  import { Sentry } from "../sentry.js";
66
66
  import { isCliTelemetryDisabled } from "./cli-telemetry.js";
67
67
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
68
- import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
68
+ import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmPathConflict, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
69
69
  import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
70
70
  import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
71
71
  import { markLatestIneffective } from "./version-check.js";
@@ -931,6 +931,13 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
931
931
  console.error(chalk.dim(`hq-cli ${latest} is available, but this copy is a local dependency (${install.packageRoot}) — update the project that owns it.`));
932
932
  return { action: "skipped", latest };
933
933
  }
934
+ if (install.manager === "pnpm") {
935
+ const conflict = pnpmPathConflict(install);
936
+ if (conflict) {
937
+ console.error(chalk.yellow(`⚠ hq self-update refused: ${conflict}`));
938
+ return { action: "skipped", latest };
939
+ }
940
+ }
934
941
  const releaseLock = flavor.lock ? (deps.acquireLock ?? acquireUpdateLock)() : () => { };
935
942
  if (!releaseLock)
936
943
  return { action: "skipped", latest };
@@ -430,6 +430,13 @@ declare function resolveHqOnPath(): string | null;
430
430
  export declare function parseReportedCliVersion(stdout: string): string | null;
431
431
  /** `<bin> --version` output (trimmed), or null on any failure/timeout. */
432
432
  declare function probeCliVersion(bin: string): string | null;
433
+ /**
434
+ * Detect a second pnpm global install winning PATH before the updater mutates
435
+ * the running install. pnpm homes have independent global package trees and
436
+ * shims, so writing one home cannot change a different home that appears first
437
+ * on PATH.
438
+ */
439
+ export declare function pnpmPathConflict(install: RunningInstall): string | null;
433
440
  /**
434
441
  * Read-your-writes convergence check, run after an install reports success.
435
442
  *
@@ -1395,6 +1395,43 @@ function probeCliVersion(bin) {
1395
1395
  return null;
1396
1396
  }
1397
1397
  }
1398
+ /**
1399
+ * Detect a second pnpm global install winning PATH before the updater mutates
1400
+ * the running install. pnpm homes have independent global package trees and
1401
+ * shims, so writing one home cannot change a different home that appears first
1402
+ * on PATH.
1403
+ */
1404
+ export function pnpmPathConflict(install) {
1405
+ if (install.manager !== "pnpm" || !install.packageRoot)
1406
+ return null;
1407
+ const bin = resolveHqOnPath();
1408
+ if (!bin)
1409
+ return null;
1410
+ const pathPackageRoot = findPackageRootForExecutable(bin);
1411
+ if (!pathPackageRoot ||
1412
+ globalInstallManagerForPath(bin, pathPackageRoot) !== "pnpm") {
1413
+ return null;
1414
+ }
1415
+ // The fallback resolver scans every hash in a PNPM_HOME because generated
1416
+ // shims do not expose their target. Multiple installs in the SAME home can
1417
+ // therefore produce an arbitrary package root; only a different home means
1418
+ // that updating this install cannot change which global shim wins PATH.
1419
+ const runningHome = derivePnpmHome(install);
1420
+ const pathHome = derivePnpmHome({ manager: "pnpm", prefix: null, packageRoot: pathPackageRoot });
1421
+ if (!runningHome || !pathHome || samePath(runningHome, pathHome))
1422
+ return null;
1423
+ let resolvedBin = bin;
1424
+ try {
1425
+ resolvedBin = realpathSync(bin);
1426
+ }
1427
+ catch {
1428
+ // Keep the PATH entry when a platform shim cannot be canonicalized.
1429
+ }
1430
+ const version = probeCliVersion(bin) ?? "unknown";
1431
+ return (`PATH resolves ${resolvedBin} at version ${version} (manager: pnpm), ` +
1432
+ `but this process is running from a different pnpm install at ${install.packageRoot}. ` +
1433
+ "Refusing to update one pnpm home while another hq-cli install wins PATH resolution.");
1434
+ }
1398
1435
  /**
1399
1436
  * Read-your-writes convergence check, run after an install reports success.
1400
1437
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.46",
3
+ "version": "5.345.48",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {