@indigoai-us/hq-cli 5.345.47 → 5.345.49

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 (49) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -1
  3. package/assets/scaffold/core/scripts/hq-session.sh +113 -7
  4. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
  5. package/dist/command-catalog.generated.d.ts +12 -0
  6. package/dist/command-catalog.generated.js +15 -0
  7. package/dist/commands/anywhere-runtime-flag.js +5 -3
  8. package/dist/commands/core-hq-session-native.d.ts +2 -0
  9. package/dist/commands/core-hq-session-native.js +242 -52
  10. package/dist/commands/core-resolve-company-native.d.ts +1 -0
  11. package/dist/commands/core-resolve-company-native.js +41 -11
  12. package/dist/commands/feedback.d.ts +10 -0
  13. package/dist/commands/feedback.js +45 -0
  14. package/dist/commands/lanes-message.js +2 -2
  15. package/dist/commands/lanes.js +2 -2
  16. package/dist/commands/mcp-serve.js +17 -4
  17. package/dist/commands/secrets.d.ts +5 -0
  18. package/dist/commands/secrets.js +181 -2
  19. package/dist/commands/workers.d.ts +4 -1
  20. package/dist/commands/workers.js +57 -15
  21. package/dist/lib/anywhere-person-setting.d.ts +1 -0
  22. package/dist/lib/anywhere-person-setting.js +12 -0
  23. package/dist/lib/anywhere-runtime-fast-flag.js +5 -3
  24. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -1
  25. package/dist/lib/anywhere-runtime-flag-key.js +6 -1
  26. package/dist/lib/cli-flag-keys.d.ts +1 -0
  27. package/dist/lib/cli-flag-keys.js +1 -0
  28. package/dist/lib/core-utils/session-id.d.ts +6 -0
  29. package/dist/lib/core-utils/session-id.js +35 -0
  30. package/dist/lib/daemon/catalog.js +1 -1
  31. package/dist/lib/daemon/flag.d.ts +1 -1
  32. package/dist/lib/daemon/flag.js +20 -9
  33. package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
  34. package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
  35. package/dist/lib/daemon/run.d.ts +6 -0
  36. package/dist/lib/daemon/run.js +36 -9
  37. package/dist/lib/daemon/server.d.ts +1 -1
  38. package/dist/lib/daemon/supervisor.d.ts +7 -1
  39. package/dist/lib/daemon/supervisor.js +50 -4
  40. package/dist/lib/lanes/reconcile.js +6 -4
  41. package/dist/lib/lanes/registry-flag.d.ts +11 -1
  42. package/dist/lib/lanes/registry-flag.js +68 -29
  43. package/dist/lib/lanes/watcher.js +63 -2
  44. package/dist/utils/resident-package-preload.js +8 -1
  45. package/dist/utils/self-update.js +6 -5
  46. package/dist/utils/version-gate.d.ts +11 -4
  47. package/dist/utils/version-gate.js +132 -41
  48. package/docs/anywhere-runtime.md +5 -3
  49. package/package.json +1 -1
@@ -8,6 +8,7 @@ export function readLanesFlagLocalOverride(flagKey, env = process.env) {
8
8
  }
9
9
  const registryReads = new Map();
10
10
  const reportedThrownFlagReads = new Set();
11
+ const reportedRegistryFailureFallbacks = new Set();
11
12
  class MissingFlagTokenError extends Error {
12
13
  constructor() {
13
14
  super("No cached HQ identity token is available");
@@ -17,13 +18,13 @@ class MissingFlagTokenError extends Error {
17
18
  function errorClass(error) {
18
19
  return error instanceof Error ? error.name : "UnknownError";
19
20
  }
20
- function reportReadFailure(flagKey, error) {
21
+ function reportReadFailure(flagKey, error, fallback) {
21
22
  const suffix = flagKey.replace(/^lanes\./, "").replaceAll(/[^a-zA-Z0-9]+/g, "_");
22
23
  process.stderr.write(`${JSON.stringify({
23
24
  event: `lanes.${suffix}.flag_read_failed`,
24
25
  flag: flagKey,
25
26
  error: errorClass(error),
26
- fallback: false,
27
+ fallback,
27
28
  })}\n`);
28
29
  }
29
30
  function reportInvalidCompanyUid(flagKey, source) {
@@ -46,27 +47,42 @@ function cacheKey(flagKey, company, endpoint) {
46
47
  }
47
48
  /**
48
49
  * Read a lanes flag once for each company identity in this CLI process. The
49
- * cache contains only an in-flight or settled boolean promise; it is never
50
+ * cache contains only an in-flight or settled decision result; it is never
50
51
  * written to disk. A local override is checked on every call and wins first.
51
52
  */
52
53
  export async function readLanesRegistryFlag(flagKey, options = {}) {
54
+ const result = await readLanesRegistryFlagResult(flagKey, options);
55
+ return result.state === "resolved" ? result.value : false;
56
+ }
57
+ /** Read a lanes flag while preserving registry failure as an explicit unknown state. */
58
+ export async function readLanesRegistryFlagResult(flagKey, options = {}) {
59
+ const outcome = await readRegistryFlagOutcome(flagKey, options, false);
60
+ return outcome.result;
61
+ }
62
+ async function readRegistryFlagOutcome(flagKey, options, failOpen) {
53
63
  const env = options.env ?? process.env;
54
64
  const localOverride = readLanesFlagLocalOverride(flagKey, env);
55
65
  if (localOverride !== null)
56
- return localOverride;
66
+ return { result: { state: "resolved", value: localOverride } };
57
67
  const companyUid = options.company?.uid?.trim();
58
68
  if (companyUid && !companyUid.startsWith("cmp_")) {
59
69
  reportInvalidCompanyUid(flagKey, options.companyUidSource ?? "company.uid");
60
- return false;
70
+ return { result: { state: "resolved", value: false } };
61
71
  }
62
72
  const endpoint = resolveFlagRegistryEndpoint(undefined, env);
63
73
  const key = cacheKey(flagKey, options.company, endpoint);
64
74
  const cached = registryReads.get(key);
65
- if (cached)
66
- return cached;
67
- const pending = readRegistryValue(flagKey, endpoint, options);
68
- registryReads.set(key, pending);
69
- return pending;
75
+ const pending = cached ?? readRegistryValue(flagKey, endpoint, options);
76
+ if (!cached)
77
+ registryReads.set(key, pending);
78
+ const outcome = await pending;
79
+ reportRegistryFailure(flagKey, key, outcome, options, failOpen);
80
+ return outcome;
81
+ }
82
+ /** Delivery and wake call sites opt in to availability when the registry is unreadable. */
83
+ export async function readLanesRegistryFlagFailOpen(flagKey, options = {}) {
84
+ const outcome = await readRegistryFlagOutcome(flagKey, options, true);
85
+ return outcome.result.state === "unknown" ? true : outcome.result.value;
70
86
  }
71
87
  /**
72
88
  * Flag readers normally report their own failures and return the safe default.
@@ -97,15 +113,10 @@ async function readRegistryValue(flagKey, endpoint, options) {
97
113
  const companyUid = company?.uid?.trim();
98
114
  const companySlug = company?.slug?.trim();
99
115
  let client;
100
- let enabled = false;
101
- let failed = false;
102
- let reported = false;
103
- const reportFailure = (error) => {
104
- failed = true;
105
- if (reported)
106
- return;
107
- reported = true;
108
- (options.reportFailure ?? ((cause) => reportReadFailure(flagKey, cause)))(error);
116
+ let resolution;
117
+ let failure;
118
+ const rememberFailure = (error) => {
119
+ failure ??= { error };
109
120
  };
110
121
  try {
111
122
  const acquireToken = options.getToken ?? (async () => {
@@ -135,30 +146,58 @@ async function readRegistryValue(flagKey, endpoint, options) {
135
146
  requestTimeoutMs: LANES_FLAG_READ_TIMEOUT_MS,
136
147
  ...(companyUid ? { companyUid } : {}),
137
148
  ...(companySlug ? { companyIdentifiers: [companySlug] } : {}),
138
- onError: reportFailure,
149
+ onError: rememberFailure,
139
150
  });
140
- await client.ready();
141
- const resolution = client.explain(flagKey, { fallback: false });
142
- if (failed)
143
- return false;
144
- if (resolution.source === "snapshot" || resolution.source === "local-override") {
145
- enabled = resolution.value;
151
+ try {
152
+ await client.ready();
153
+ }
154
+ catch (error) {
155
+ rememberFailure(error);
156
+ }
157
+ try {
158
+ resolution = client.explain(flagKey, { fallback: false });
159
+ }
160
+ catch (error) {
161
+ rememberFailure(error);
146
162
  }
147
163
  }
148
164
  catch (error) {
149
- reportFailure(error);
165
+ rememberFailure(error);
150
166
  }
151
167
  finally {
152
168
  try {
153
169
  client?.close();
154
170
  }
155
171
  catch (error) {
156
- reportFailure(error);
172
+ rememberFailure(error);
157
173
  }
158
174
  }
159
- return failed ? false : enabled;
175
+ if (resolution?.source === "snapshot" || resolution?.source === "local-override") {
176
+ return { result: { state: "resolved", value: resolution.value }, ...(failure ? { failure } : {}) };
177
+ }
178
+ if (failure)
179
+ return { result: { state: "unknown" }, failure };
180
+ return { result: { state: "resolved", value: resolution?.value ?? false } };
181
+ }
182
+ function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
183
+ if (!outcome.failure)
184
+ return;
185
+ const fallback = outcome.result.state === "resolved"
186
+ ? outcome.result.value
187
+ : failOpen;
188
+ const reportKey = `${key}:${fallback}`;
189
+ if (reportedRegistryFailureFallbacks.has(reportKey))
190
+ return;
191
+ reportedRegistryFailureFallbacks.add(reportKey);
192
+ const reportFailure = options.reportFailure;
193
+ if (reportFailure) {
194
+ reportFailure(outcome.failure.error);
195
+ return;
196
+ }
197
+ reportReadFailure(flagKey, outcome.failure.error, fallback);
160
198
  }
161
199
  export function _resetLanesRegistryFlagCacheForTests() {
162
200
  registryReads.clear();
201
+ reportedRegistryFailureFallbacks.clear();
163
202
  }
164
203
  //# sourceMappingURL=registry-flag.js.map
@@ -398,8 +398,69 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
398
398
  current.state === "interrupted" ||
399
399
  current.state === "cancelled" ||
400
400
  current.state === "killed";
401
- if (terminal)
402
- return current;
401
+ if (terminal) {
402
+ // A terminal state may have been recorded before the worker's final
403
+ // report arrived. The report is durable output from work that already
404
+ // ran, so preserve it without reopening the lane or rechecking admission.
405
+ if (opts.envelope === undefined || opts.envelope === null)
406
+ return current;
407
+ const approvalGate = enforceRegistryApprovalGate(current, opts.envelope);
408
+ effectiveEnvelope = approvalGate.envelope;
409
+ effectiveClassification = approvalGate.violation
410
+ ? { state: "blocked", reason: "approval_gate_violated" }
411
+ : terminalClassification;
412
+ const envelope = effectiveEnvelope;
413
+ const storySwitch = applyStorySwitchEnvelope(current, envelope, at, envelopeRef);
414
+ const next = {
415
+ ...current,
416
+ last_envelope: {
417
+ ref: envelopeRef,
418
+ decision: envelope.decision,
419
+ at,
420
+ artifacts: envelope.artifacts,
421
+ ...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
422
+ ...(envelope.next_check_at !== undefined ? { next_check_at: envelope.next_check_at } : {}),
423
+ },
424
+ ...(envelope.decision !== "blocked" ? { keep_alive_blocked_resume_streak: 0 } : {}),
425
+ envelope_validation_errors: opts.envelopeValidationErrors,
426
+ };
427
+ if (!approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
428
+ next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
429
+ event: "consumed",
430
+ approval_id: approvalId,
431
+ envelope_ref: envelopeRef,
432
+ at,
433
+ }), current.registry_worker_approval_history ?? []);
434
+ }
435
+ if (current.state === "cancelled" || current.state === "killed") {
436
+ return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
437
+ }
438
+ const storyStatus = storySwitch.suppressCompletion
439
+ ? "in_progress"
440
+ : terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
441
+ if (storyStatus !== undefined) {
442
+ next.story_status = storySwitch.suppressCompletion ? "in_progress" : envelope.story_status;
443
+ next.story_complete = storySwitch.suppressCompletion ? false : envelope.story_complete;
444
+ next.story_complete_reason = storySwitch.suppressCompletion ? undefined : envelope.story_complete_reason;
445
+ next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
446
+ ? {
447
+ ...story,
448
+ status: storyStatus,
449
+ updated_at: at,
450
+ ...(envelope.decision === "blocked"
451
+ ? { blocked_reason: envelope.blocked_reason ?? "" }
452
+ : { blocked_reason: undefined }),
453
+ }
454
+ : story);
455
+ }
456
+ if (storySwitch.clearPending)
457
+ next.story_switch_pending = undefined;
458
+ if (storySwitch.storySwitchUnacknowledged !== undefined) {
459
+ next.story_switch_unacknowledged = storySwitch.storySwitchUnacknowledged;
460
+ next.attribution_history = storySwitch.attributionHistory ?? current.attribution_history;
461
+ }
462
+ return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
463
+ }
403
464
  if (current.state === "awaiting_input" || current.state === "blocked") {
404
465
  if (current.state === effectiveClassification.state)
405
466
  return current;
@@ -40,7 +40,7 @@ export const RESIDENT_PACKAGE_MODULES = {
40
40
  "@indigoai-us/hq-cloud",
41
41
  ],
42
42
  "bot-logs-follow": [],
43
- mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
43
+ mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
44
44
  childWrapper: ["../main.js"],
45
45
  coreJobRun: ["../lib/flag-registry.js"],
46
46
  "daemon-call": ["../lib/daemon/protocol.js"],
@@ -76,6 +76,7 @@ export const RESIDENT_PACKAGE_MODULES = {
76
76
  "../utils/plan-gate-error.js",
77
77
  "open",
78
78
  "../commands/mcp-serve.js",
79
+ "../lib/anywhere-person-setting.js",
79
80
  "../lib/daemon/mcp-oauth.js",
80
81
  "../lib/mcp/tools.js",
81
82
  "../lib/mcp/prompts.js",
@@ -83,6 +84,8 @@ export const RESIDENT_PACKAGE_MODULES = {
83
84
  "../utils/cli-telemetry.js",
84
85
  "../utils/cognito-session.js",
85
86
  "../lib/first-sync-next-step.js",
87
+ "../lib/process-flag-company-context.js",
88
+ "../lib/flag-snapshot-file.js",
86
89
  "@indigoai-us/hq-cloud",
87
90
  ],
88
91
  };
@@ -185,6 +188,7 @@ const LOADERS = {
185
188
  mcp: [
186
189
  () => import("../lib/mcp/tools.js"),
187
190
  () => import("../lib/mcp/prompts.js"),
191
+ () => import("../lib/anywhere-person-setting.js"),
188
192
  () => import("../utils/cognito-session.js"),
189
193
  () => import("@indigoai-us/hq-cloud"),
190
194
  async () => {
@@ -345,6 +349,7 @@ const LOADERS = {
345
349
  () => import("../utils/plan-gate-error.js"),
346
350
  () => import("open"),
347
351
  () => import("../commands/mcp-serve.js"),
352
+ () => import("../lib/anywhere-person-setting.js"),
348
353
  () => import("../lib/daemon/mcp-oauth.js"),
349
354
  () => import("../lib/mcp/tools.js"),
350
355
  () => import("../lib/mcp/prompts.js"),
@@ -352,6 +357,8 @@ const LOADERS = {
352
357
  () => import("../utils/cli-telemetry.js"),
353
358
  () => import("../utils/cognito-session.js"),
354
359
  () => import("../lib/first-sync-next-step.js"),
360
+ () => import("../lib/process-flag-company-context.js"),
361
+ () => import("../lib/flag-snapshot-file.js"),
355
362
  () => import("@indigoai-us/hq-cloud"),
356
363
  ],
357
364
  };
@@ -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, pnpmPathConflict, 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, resolvePnpmPathInstall, 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";
@@ -926,17 +926,18 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
926
926
  // actually running would stay exactly as stale as it started while the CLI
927
927
  // reported success and re-exec'd into the same build. That is the shape of
928
928
  // the 2026-09-02 gate loop — see isLocalDependencyInstall in version-gate.ts.
929
- const install = (deps.resolveInstall ?? resolveRunningInstall)();
929
+ let install = (deps.resolveInstall ?? resolveRunningInstall)();
930
930
  if (isLocalDependencyInstall(install)) {
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
934
  if (install.manager === "pnpm") {
935
- const conflict = pnpmPathConflict(install);
936
- if (conflict) {
937
- console.error(chalk.yellow(`⚠ hq self-update refused: ${conflict}`));
935
+ const pathTarget = resolvePnpmPathInstall(install);
936
+ if (pathTarget.refusal) {
937
+ console.error(chalk.yellow(`⚠ hq self-update refused: ${pathTarget.refusal}`));
938
938
  return { action: "skipped", latest };
939
939
  }
940
+ install = pathTarget.install;
940
941
  }
941
942
  const releaseLock = flavor.lock ? (deps.acquireLock ?? acquireUpdateLock)() : () => { };
942
943
  if (!releaseLock)
@@ -194,6 +194,8 @@ export declare function isPrefixWritable(prefix: string, access?: (target: strin
194
194
  * existing ancestor: npm can create a child only if that ancestor is writable.
195
195
  */
196
196
  export declare function canWriteNpmInstall(prefix: string, packageRoot: string, access?: (target: string, mode: number) => void, platform?: NodeJS.Platform): boolean;
197
+ /** Check whether pnpm can replace its global package and PATH shim. */
198
+ export declare function canWritePnpmInstall(install: RunningInstall, access?: (target: string, mode: number) => void): boolean;
197
199
  /** Claim the one local warning/report for this installed version and prefix. */
198
200
  export declare function claimNonWritableNpmPrefixNotice(install: Pick<RunningInstall, "manager" | "prefix" | "packageRoot">, fromVersion: string, homeDir?: string, kind?: "user" | "report"): boolean;
199
201
  /** Emit one path-free warning event for a handled non-writable npm prefix. */
@@ -223,10 +225,12 @@ export declare function derivePnpmHome(install: RunningInstall): string | null;
223
225
  * Environment for the pnpm self-update spawn. pnpm requires its configured
224
226
  * global bin directory to be on PATH; pnpm 12.5.1 reports
225
227
  * `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
226
- * may also lack PNPM_HOME, so derive it from the running install when needed.
228
+ * may also lack PNPM_HOME, so derive it from the selected install when needed.
227
229
  *
228
- * A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
229
- * `undefined` only when the inherited environment already has both values.
230
+ * A no-op for npm/bun installs. The selected install's home owns its global
231
+ * bin directory, so it takes precedence over an inherited PNPM_HOME after the
232
+ * updater retargets the install. Returns `undefined` only when the inherited
233
+ * environment already has the required values.
230
234
  */
231
235
  export declare function pnpmUpdateEnv(install: RunningInstall, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined;
232
236
  export declare function buildPrefixedInstallArgv(prefix: string): string[];
@@ -436,7 +440,10 @@ declare function probeCliVersion(bin: string): string | null;
436
440
  * shims, so writing one home cannot change a different home that appears first
437
441
  * on PATH.
438
442
  */
439
- export declare function pnpmPathConflict(install: RunningInstall): string | null;
443
+ export declare function resolvePnpmPathInstall(install: RunningInstall, canWrite?: (target: RunningInstall) => boolean): {
444
+ install: RunningInstall;
445
+ refusal: string | null;
446
+ };
440
447
  /**
441
448
  * Read-your-writes convergence check, run after an install reports success.
442
449
  *
@@ -101,11 +101,11 @@ function findRunningPackageRoot() {
101
101
  return findCliPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
102
102
  }
103
103
  /** Resolve an on-PATH executable back to the installed hq-cli package it loads. */
104
- function findPackageRootForExecutable(bin) {
104
+ function resolvePackageRootForExecutable(bin) {
105
105
  try {
106
106
  const packageRoot = findCliPackageRoot(path.dirname(realpathSync(bin)));
107
107
  if (packageRoot)
108
- return packageRoot;
108
+ return { packageRoot, candidates: [packageRoot] };
109
109
  }
110
110
  catch {
111
111
  // A missing or inaccessible executable may still be recoverable from the
@@ -115,25 +115,26 @@ function findPackageRootForExecutable(bin) {
115
115
  // resolves symlinks, but a generated shim is a regular file, so walking up
116
116
  // from its real path never reaches the package it invokes. Look up only the
117
117
  // hq-cli package under the matching PNPM_HOME/global layout instead.
118
- const binDir = path.dirname(bin);
118
+ const shimBinDir = path.dirname(bin);
119
119
  const configuredHome = process.env.PNPM_HOME;
120
- const inferredHome = path.basename(binDir).toLowerCase() === "bin"
121
- ? path.dirname(binDir)
122
- : binDir;
120
+ const inferredHome = path.basename(shimBinDir).toLowerCase() === "bin"
121
+ ? path.dirname(shimBinDir)
122
+ : shimBinDir;
123
123
  const candidateHomes = [configuredHome, inferredHome].filter((home, index, homes) => Boolean(home) && homes.findIndex((candidate) => candidate === home) === index);
124
- const home = candidateHomes.find((candidate) => samePath(binDir, candidate) ||
125
- samePath(binDir, path.join(candidate, "bin")));
124
+ const home = candidateHomes.find((candidate) => samePath(shimBinDir, candidate) ||
125
+ samePath(shimBinDir, path.join(candidate, "bin")));
126
126
  if (!home)
127
- return null;
127
+ return { packageRoot: null, candidates: [] };
128
128
  const globalRoot = path.join(home, "global");
129
129
  let storeVersions;
130
130
  try {
131
131
  storeVersions = readdirSync(globalRoot);
132
132
  }
133
133
  catch {
134
- return null;
134
+ return { packageRoot: null, candidates: [] };
135
135
  }
136
136
  const packageSegments = CLI_NAME.split("/");
137
+ const candidates = new Set();
137
138
  for (const storeVersion of storeVersions) {
138
139
  const storeRoot = path.join(globalRoot, storeVersion);
139
140
  const packageRoots = [
@@ -156,11 +157,38 @@ function findPackageRootForExecutable(bin) {
156
157
  if (packageRoot &&
157
158
  samePath(packageRoot, candidate) &&
158
159
  isPnpmManagedPackageDir(packageRoot)) {
159
- return packageRoot;
160
+ candidates.add(packageRoot);
160
161
  }
161
162
  }
162
163
  }
163
- return null;
164
+ const roots = [...candidates];
165
+ if (roots.length <= 1)
166
+ return { packageRoot: roots[0] ?? null, candidates: roots };
167
+ // pnpm's generated shim identifies the package entry point it will execute.
168
+ // Use that target to select among v11 hash directories instead of letting
169
+ // directory enumeration order choose the global project and virtual store.
170
+ let shimContents;
171
+ try {
172
+ shimContents = readFileSync(bin, "utf8").replace(/\\/g, "/");
173
+ }
174
+ catch {
175
+ return { packageRoot: null, candidates: roots };
176
+ }
177
+ const selected = roots.filter((root) => {
178
+ const entryPoint = installedHqEntryPoint({ manager: "pnpm", prefix: null, packageRoot: root });
179
+ if (!entryPoint)
180
+ return false;
181
+ const absolute = entryPoint.replace(/\\/g, "/");
182
+ const relative = path.relative(shimBinDir, entryPoint).replace(/\\/g, "/");
183
+ return shimContents.includes(absolute) || shimContents.includes(relative);
184
+ });
185
+ return {
186
+ packageRoot: selected.length === 1 ? selected[0] : null,
187
+ candidates: roots,
188
+ };
189
+ }
190
+ function findPackageRootForExecutable(bin) {
191
+ return resolvePackageRootForExecutable(bin).packageRoot;
164
192
  }
165
193
  function samePath(left, right) {
166
194
  const canonical = (value) => {
@@ -507,6 +535,39 @@ export function canWriteNpmInstall(prefix, packageRoot, access = (target, mode)
507
535
  }
508
536
  });
509
537
  }
538
+ /** Check whether pnpm can replace its global package and PATH shim. */
539
+ export function canWritePnpmInstall(install, access = (target, mode) => accessSync(target, mode)) {
540
+ const home = derivePnpmHome(install);
541
+ if (!home || !install.packageRoot)
542
+ return false;
543
+ const mode = fsConstants.W_OK | fsConstants.X_OK;
544
+ const dirs = pnpmInstallDirs(install);
545
+ const targets = [
546
+ home,
547
+ path.join(home, "global"),
548
+ path.join(home, "store"),
549
+ ...(dirs ? [dirs.globalDir, dirs.storeDir, dirs.virtualStoreDir] : []),
550
+ path.dirname(install.packageRoot),
551
+ ];
552
+ return targets.every((target) => {
553
+ let candidate = path.resolve(target);
554
+ while (true) {
555
+ try {
556
+ access(candidate, mode);
557
+ return true;
558
+ }
559
+ catch (error) {
560
+ if (error?.code !== "ENOENT")
561
+ return false;
562
+ // pnpm creates missing targets under the nearest existing writable ancestor.
563
+ const parent = path.dirname(candidate);
564
+ if (parent === candidate)
565
+ return false;
566
+ candidate = parent;
567
+ }
568
+ }
569
+ });
570
+ }
510
571
  const nonWritablePrefixNotices = new Set();
511
572
  /** Claim the one local warning/report for this installed version and prefix. */
512
573
  export function claimNonWritableNpmPrefixNotice(install, fromVersion, homeDir = os.homedir(), kind = "user") {
@@ -601,18 +662,22 @@ export function derivePnpmHome(install) {
601
662
  * Environment for the pnpm self-update spawn. pnpm requires its configured
602
663
  * global bin directory to be on PATH; pnpm 12.5.1 reports
603
664
  * `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
604
- * may also lack PNPM_HOME, so derive it from the running install when needed.
665
+ * may also lack PNPM_HOME, so derive it from the selected install when needed.
605
666
  *
606
- * A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
607
- * `undefined` only when the inherited environment already has both values.
667
+ * A no-op for npm/bun installs. The selected install's home owns its global
668
+ * bin directory, so it takes precedence over an inherited PNPM_HOME after the
669
+ * updater retargets the install. Returns `undefined` only when the inherited
670
+ * environment already has the required values.
608
671
  */
609
672
  export function pnpmUpdateEnv(install, base = process.env) {
610
673
  if (install.manager !== "pnpm")
611
674
  return undefined;
612
- const home = base.PNPM_HOME || derivePnpmHome(install);
675
+ const home = derivePnpmHome(install) ?? base.PNPM_HOME;
613
676
  if (!home)
614
677
  return undefined;
615
- const binDir = path.join(home, "bin");
678
+ // pnpm's own `bin -g` command reports PNPM_HOME itself as the global bin
679
+ // directory; it must be that directory, not a guessed `<home>/bin` child.
680
+ const binDir = home;
616
681
  const pathKey = Object.keys(base).find((key) => key.toLowerCase() === "path") ?? "PATH";
617
682
  const currentPath = base[pathKey] ?? "";
618
683
  const normalizedBinDir = path.resolve(binDir);
@@ -633,7 +698,7 @@ export function pnpmUpdateEnv(install, base = process.env) {
633
698
  COREPACK_DEFAULT_TO_LATEST: "0",
634
699
  COREPACK_ENABLE_AUTO_PIN: "0",
635
700
  };
636
- if (!base.PNPM_HOME)
701
+ if (base.PNPM_HOME !== home)
637
702
  result.PNPM_HOME = home;
638
703
  if (!alreadyOnPath)
639
704
  result[pathKey] = [currentPath, binDir].filter(Boolean).join(path.delimiter);
@@ -1401,36 +1466,62 @@ function probeCliVersion(bin) {
1401
1466
  * shims, so writing one home cannot change a different home that appears first
1402
1467
  * on PATH.
1403
1468
  */
1404
- export function pnpmPathConflict(install) {
1469
+ export function resolvePnpmPathInstall(install, canWrite = canWritePnpmInstall) {
1405
1470
  if (install.manager !== "pnpm" || !install.packageRoot)
1406
- return null;
1471
+ return { install, refusal: null };
1407
1472
  const bin = resolveHqOnPath();
1408
1473
  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.
1474
+ return { install, refusal: null };
1419
1475
  const runningHome = derivePnpmHome(install);
1476
+ const packageRootLookup = resolvePackageRootForExecutable(bin);
1477
+ const pathPackageRoot = packageRootLookup.packageRoot;
1478
+ if (!pathPackageRoot) {
1479
+ if (packageRootLookup.candidates.length > 1) {
1480
+ return {
1481
+ install,
1482
+ refusal: `could not identify which pnpm package PATH binary ${bin} resolves to; matching global packages: ${packageRootLookup.candidates.join(", ")}`,
1483
+ };
1484
+ }
1485
+ const binDirectory = path.dirname(bin);
1486
+ const binHome = path.basename(binDirectory).toLowerCase() === "bin"
1487
+ ? path.dirname(binDirectory)
1488
+ : binDirectory;
1489
+ if (runningHome && binHome && samePath(runningHome, binHome)) {
1490
+ if (canWrite(install))
1491
+ return { install, refusal: null };
1492
+ return {
1493
+ install,
1494
+ refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
1495
+ };
1496
+ }
1497
+ return { install, refusal: `could not identify the pnpm-managed install resolved by PATH at ${bin}` };
1498
+ }
1499
+ const pathManager = globalInstallManagerForPath(bin, pathPackageRoot);
1500
+ if (pathManager !== "pnpm") {
1501
+ if (pathManager === "npm" || pathManager === "bun") {
1502
+ // Updating the running pnpm install still makes progress when a known
1503
+ // npm/Bun install shadows it; refusing would guarantee no convergence.
1504
+ return { install, refusal: null };
1505
+ }
1506
+ return { install, refusal: `PATH resolves ${bin} to an install that is not verified as pnpm-managed` };
1507
+ }
1508
+ // The resolver uses the generated shim's target when a pnpm v11 home has
1509
+ // multiple package hashes. Its selected package root must drive the global
1510
+ // directory and virtual store used by the update.
1420
1511
  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);
1512
+ if (!runningHome || !pathHome) {
1513
+ return { install, refusal: `could not identify the PNPM_HOME for PATH binary ${bin}` };
1426
1514
  }
1427
- catch {
1428
- // Keep the PATH entry when a platform shim cannot be canonicalized.
1515
+ const targetInstall = samePath(runningHome, pathHome)
1516
+ ? install
1517
+ : { manager: "pnpm", prefix: null, packageRoot: pathPackageRoot };
1518
+ if (!canWrite(targetInstall)) {
1519
+ return {
1520
+ install,
1521
+ refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
1522
+ };
1429
1523
  }
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.");
1524
+ return { install: targetInstall, refusal: null };
1434
1525
  }
1435
1526
  /**
1436
1527
  * Read-your-writes convergence check, run after an install reports success.
@@ -5,7 +5,9 @@ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
5
5
  PowerShell pipe host that applies and verifies a user-only named-pipe DACL
6
6
  before becoming ready. A per-user
7
7
  registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
8
- is the operator kill switch and is off by default. Runtime access also
8
+ is an operator kill switch that defaults on when its registry row is absent,
9
+ archived, or unreadable. An explicit registry false or the local
10
+ `HQ_FLAG_HQ_ANYWHERE_RUNTIME=false|0` override turns it off. Runtime access also
9
11
  requires the current person to enable the `hq-anywhere` setting in HQ. If that
10
12
  setting cannot be read, runtime access stays off.
11
13
 
@@ -34,8 +36,8 @@ stored in a repository or synced HQ folder.
34
36
  Windows requires Windows PowerShell 5.1 or PowerShell 7 for the bundled pipe
35
37
  host. If it is unavailable, the pipe cannot be created with a verified
36
38
  current-user-only DACL, or another process already owns the pipe name, hqd
37
- refuses to start. The `hq-anywhere-runtime` flag remains off by default on all
38
- platforms.
39
+ refuses to start. The flag's missing-row fallback is on across platforms; an
40
+ explicit false remains the operator kill switch.
39
41
 
40
42
  On macOS and Linux, hqd checks the socket directory and socket path with
41
43
  `lstat` before serving. It refuses symlinked, shared, or foreign-owned paths.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.47",
3
+ "version": "5.345.49",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {