@indigoai-us/hq-cli 5.345.17 → 5.345.19

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,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.19] — 2026-10-06
6
+ - Fix installation and updates of the published hq-anywhere pack through its built-in alias while continuing to reject reserved names from user-supplied remote sources.
7
+ - With `hq-anywhere-runtime` enabled, hqd now starts on Windows through the user-scoped named-pipe host and refuses service only when secure pipe setup or ACL verification fails. `hq doctor` describes unsafe named-pipe startup as an ACL setup failure and recommends the daemon install or restart action based on service state.
8
+
9
+ ## [5.345.18] — 2026-10-06
10
+ - Git Bash forwarders now keep slash commands intact and convert file path arguments that hq needs on Windows.
11
+ - Lane placement and Claude account-balance refusals now show the requested placement, actual host, balance host when different, and the model values and sources used for an unavailable-account decision.
12
+
5
13
  ## [5.345.17] — 2026-10-06
6
14
  - Git Bash forwarders now keep slash commands intact and convert file path arguments that hq needs on Windows.
7
15
 
@@ -35,16 +35,23 @@ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
35
35
 
36
36
  command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
37
37
 
38
- # jq.exe's binary mode avoids CRT newline conversion without changing real CR bytes.
38
+ # jq.exe on MSYS may use CRT text mode unless --binary is supported.
39
39
  case "${OSTYPE:-}:${MSYSTEM:-}" in
40
40
  msys*:*|*:MINGW*)
41
41
  jq() {
42
- local stdout_file stderr_file status
42
+ local stdout_file stderr_file status binary_supported
43
43
  stdout_file="$(mktemp)" || return $?
44
44
  stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
45
- if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
46
- command sed -b 's/\r$//' "$stdout_file"
47
- command sed -b 's/\r$//' "$stderr_file" >&2
45
+ if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
46
+ if [[ "$binary_supported" == 1 ]]; then
47
+ if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
48
+ command cat "$stdout_file"
49
+ command cat "$stderr_file" >&2
50
+ else
51
+ if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
52
+ command sed -b 's/\r$//' "$stdout_file"
53
+ command sed -b 's/\r$//' "$stderr_file" >&2
54
+ fi
48
55
  rm -f "$stdout_file" "$stderr_file"
49
56
  return "$status"
50
57
  }
@@ -40,16 +40,23 @@ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
40
40
 
41
41
  command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
42
42
 
43
- # jq.exe's binary mode avoids CRT newline conversion without changing real CR bytes.
43
+ # jq.exe on MSYS may use CRT text mode unless --binary is supported.
44
44
  case "${OSTYPE:-}:${MSYSTEM:-}" in
45
45
  msys*:*|*:MINGW*)
46
46
  jq() {
47
- local stdout_file stderr_file status
47
+ local stdout_file stderr_file status binary_supported
48
48
  stdout_file="$(mktemp)" || return $?
49
49
  stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
50
- if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
51
- command sed -b 's/\r$//' "$stdout_file"
52
- command sed -b 's/\r$//' "$stderr_file" >&2
50
+ if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
51
+ if [[ "$binary_supported" == 1 ]]; then
52
+ if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
53
+ command cat "$stdout_file"
54
+ command cat "$stderr_file" >&2
55
+ else
56
+ if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
57
+ command sed -b 's/\r$//' "$stdout_file"
58
+ command sed -b 's/\r$//' "$stderr_file" >&2
59
+ fi
53
60
  rm -f "$stdout_file" "$stderr_file"
54
61
  return "$status"
55
62
  }
@@ -120,16 +120,23 @@ done
120
120
  command -v yq >/dev/null 2>&1 || die "yq is required (mikefarah/yq)"
121
121
  command -v jq >/dev/null 2>&1 || die "jq is required"
122
122
 
123
- # jq.exe's binary mode avoids CRT newline conversion without changing real CR bytes.
123
+ # jq.exe on MSYS may use CRT text mode unless --binary is supported.
124
124
  case "${OSTYPE:-}:${MSYSTEM:-}" in
125
125
  msys*:*|*:MINGW*)
126
126
  jq() {
127
- local stdout_file stderr_file status
127
+ local stdout_file stderr_file status binary_supported
128
128
  stdout_file="$(mktemp)" || return $?
129
129
  stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
130
- if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
131
- command sed -b 's/\r$//' "$stdout_file"
132
- command sed -b 's/\r$//' "$stderr_file" >&2
130
+ if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
131
+ if [[ "$binary_supported" == 1 ]]; then
132
+ if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
133
+ command cat "$stdout_file"
134
+ command cat "$stderr_file" >&2
135
+ else
136
+ if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
137
+ command sed -b 's/\r$//' "$stdout_file"
138
+ command sed -b 's/\r$//' "$stderr_file" >&2
139
+ fi
133
140
  rm -f "$stdout_file" "$stderr_file"
134
141
  return "$status"
135
142
  }
@@ -40,16 +40,23 @@ command -v jq >/dev/null 2>&1 || {
40
40
  exit 2
41
41
  }
42
42
 
43
- # jq.exe's binary mode avoids CRT newline conversion without changing real CR bytes.
43
+ # jq.exe on MSYS may use CRT text mode unless --binary is supported.
44
44
  case "${OSTYPE:-}:${MSYSTEM:-}" in
45
45
  msys*:*|*:MINGW*)
46
46
  jq() {
47
- local stdout_file stderr_file status
47
+ local stdout_file stderr_file status binary_supported
48
48
  stdout_file="$(mktemp)" || return $?
49
49
  stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
50
- if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
51
- command sed -b 's/\r$//' "$stdout_file"
52
- command sed -b 's/\r$//' "$stderr_file" >&2
50
+ if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
51
+ if [[ "$binary_supported" == 1 ]]; then
52
+ if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
53
+ command cat "$stdout_file"
54
+ command cat "$stderr_file" >&2
55
+ else
56
+ if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
57
+ command sed -b 's/\r$//' "$stdout_file"
58
+ command sed -b 's/\r$//' "$stderr_file" >&2
59
+ fi
53
60
  rm -f "$stdout_file" "$stderr_file"
54
61
  return "$status"
55
62
  }
@@ -128,16 +128,65 @@ resolve_session_key() {
128
128
  }
129
129
 
130
130
  # Read-modify-write on the counter must not interleave across concurrent tasks.
131
+ reclaim_stale_counter_lock() {
132
+ local lockdir="$1" owner_pid current_owner claim stale_dir suffix=0
133
+ owner_pid="$(cat "$lockdir/pid" 2>/dev/null || true)"
134
+ case "$owner_pid" in ''|*[!0-9]*) return 1 ;; esac
135
+ [ "$owner_pid" -gt 1 ] 2>/dev/null || return 1
136
+ kill -0 "$owner_pid" 2>/dev/null && return 1
137
+
138
+ claim="${lockdir}.reclaim"
139
+ mkdir "$claim" 2>/dev/null || return 1
140
+ if ! printf '%s\n' "$$" > "$claim/pid" 2>/dev/null; then
141
+ rmdir "$claim" 2>/dev/null || true
142
+ return 1
143
+ fi
144
+
145
+ current_owner="$(cat "$lockdir/pid" 2>/dev/null || true)"
146
+ if [ "$current_owner" = "$owner_pid" ] && ! kill -0 "$owner_pid" 2>/dev/null; then
147
+ stale_dir="${lockdir}.stale.$$.$RANDOM"
148
+ while [ -e "$stale_dir" ]; do
149
+ suffix=$((suffix + 1))
150
+ stale_dir="${lockdir}.stale.$$.$RANDOM.$suffix"
151
+ done
152
+ if mv "$lockdir" "$stale_dir" 2>/dev/null; then
153
+ rm -rf "$stale_dir" 2>/dev/null || true
154
+ rm -rf "$claim" 2>/dev/null || true
155
+ return 0
156
+ fi
157
+ fi
158
+
159
+ rm -rf "$claim" 2>/dev/null || true
160
+ return 1
161
+ }
162
+
131
163
  with_counter_lock() {
132
164
  local lockdir="$1"; shift
133
- local i=0
134
- while ! mkdir "$lockdir" 2>/dev/null; do
165
+ local i=0 acquired=0 rc=0 tries="${HQ_JOURNAL_COUNTER_LOCK_TRIES:-250}"
166
+ case "$tries" in ''|*[!0-9]*) tries=250 ;; esac
167
+ while [ "$i" -lt "$tries" ]; do
168
+ if mkdir "$lockdir" 2>/dev/null; then
169
+ acquired=1
170
+ if ! printf '%s\n' "$$" > "$lockdir/pid" 2>/dev/null; then
171
+ rm -rf "$lockdir" 2>/dev/null || true
172
+ return 0
173
+ fi
174
+ break
175
+ fi
176
+ reclaim_stale_counter_lock "$lockdir" && continue
135
177
  i=$((i + 1))
136
- [ "$i" -ge 50 ] && break
137
- sleep 0.02 2>/dev/null || sleep 1
178
+ [ "$i" -ge "$tries" ] || sleep 0.02 2>/dev/null || sleep 1
138
179
  done
139
- "$@"
140
- rmdir "$lockdir" 2>/dev/null || true
180
+ if [ "$acquired" -ne 1 ]; then
181
+ warn "tool-counter: lock remained busy; skipped update"
182
+ return 0
183
+ fi
184
+
185
+ "$@" || rc=$?
186
+ if [ "$(cat "$lockdir/pid" 2>/dev/null || true)" = "$$" ]; then
187
+ rm -rf "$lockdir" 2>/dev/null || true
188
+ fi
189
+ return "$rc"
141
190
  }
142
191
 
143
192
  counter_read_raw() {
@@ -210,7 +210,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
210
210
  fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
211
211
  }
212
212
  catch (error) {
213
- reportDirectoryWriteFailure(error, 346);
213
+ reportDirectoryWriteFailure(error, 395);
214
214
  }
215
215
  });
216
216
  }
@@ -223,7 +223,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
223
223
  fs.writeFileSync(counter, "0\n");
224
224
  }
225
225
  catch (error) {
226
- reportDirectoryWriteFailure(error, 354);
226
+ reportDirectoryWriteFailure(error, 403);
227
227
  }
228
228
  });
229
229
  }
@@ -197,6 +197,7 @@ export function createLaneAccountsHandlers(deps) {
197
197
  target_host: host.capacity.host,
198
198
  error: isLanesError(err) ? err.error : "claude_account_balance_selection_failed",
199
199
  message: dispatchErrorMessage(err),
200
+ ...(isLanesError(err) ? { details: err.details } : {}),
200
201
  };
201
202
  }
202
203
  });
@@ -3472,14 +3472,20 @@ async function runCreate(ctx, opts) {
3472
3472
  const targetHost = placementBalanceHostOverride ?? placementHostOverride ?? (outpostId !== undefined
3473
3473
  ? { kind: "outpost", id: outpostId }
3474
3474
  : laneHostFromCtx(ctx));
3475
+ const actualTargetHost = placementHostOverride ?? (outpostId !== undefined
3476
+ ? { kind: "outpost", id: outpostId }
3477
+ : { kind: "local", id: ctx.hostname() });
3475
3478
  balanceTargetHost = targetHost;
3476
3479
  balanceHostLoad = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
3477
3480
  const selected = selectClaudeAccount({
3478
3481
  hosts: balanceHostLoad.snapshots,
3479
3482
  targetHost,
3483
+ actualTargetHost,
3484
+ requestedPlacement: placementToApply ?? null,
3480
3485
  config,
3481
3486
  quotaState: loadQuotaState(hqRoot),
3482
3487
  ...(modelToValidate !== undefined ? { model: modelToValidate } : {}),
3488
+ requestedModelSource: cliModel !== undefined ? "flag" : profileModel !== undefined ? "profile" : "default",
3483
3489
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
3484
3490
  now: ctx.now(),
3485
3491
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
@@ -3523,12 +3529,18 @@ async function runCreate(ctx, opts) {
3523
3529
  const hosts = balanceHostLoad.snapshots.map((host) => host.capacity.host.kind === localHost.kind && host.capacity.host.id === localHost.id
3524
3530
  ? refreshedLocal
3525
3531
  : host);
3532
+ const actualTargetHostForPersist = placementHostOverride ?? (outpostId !== undefined
3533
+ ? { kind: "outpost", id: outpostId }
3534
+ : { kind: "local", id: ctx.hostname() });
3526
3535
  const selected = selectClaudeAccount({
3527
3536
  hosts,
3528
3537
  targetHost: balanceTargetHost,
3538
+ actualTargetHost: actualTargetHostForPersist,
3539
+ requestedPlacement: placementToApply ?? null,
3529
3540
  config,
3530
3541
  quotaState: loadQuotaState(hqRoot),
3531
3542
  ...(modelToValidate !== undefined ? { model: modelToValidate } : {}),
3543
+ requestedModelSource: cliModel !== undefined ? "flag" : profileModel !== undefined ? "profile" : "default",
3532
3544
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
3533
3545
  now: ctx.now(),
3534
3546
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
@@ -6802,6 +6814,11 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6802
6814
  // Repair keeps an account-derived model pinned through the move; plain
6803
6815
  // automatic rebalancing resolves each candidate's account default itself.
6804
6816
  const selectionModel = preservedModel ?? pinnedModel ?? profileModel;
6817
+ const selectionModelSource = preservedModel !== undefined || pinnedModel !== undefined
6818
+ ? "lane"
6819
+ : profileModel !== undefined
6820
+ ? "profile"
6821
+ : "default";
6805
6822
  const model = resolveLaneModel({
6806
6823
  cliModel: pinnedModel,
6807
6824
  profileModel,
@@ -6829,11 +6846,14 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6829
6846
  const selectionInput = {
6830
6847
  hosts: balanceHosts.snapshots,
6831
6848
  targetHost: lane.host,
6849
+ actualTargetHost: lane.host,
6850
+ requestedPlacement: null,
6832
6851
  config,
6833
6852
  quotaState: loadQuotaState(hqRoot),
6834
6853
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, lane.company ?? undefined),
6835
6854
  now: ctx.now(),
6836
6855
  ...(selectionModel !== undefined ? { model: selectionModel } : {}),
6856
+ requestedModelSource: selectionModelSource,
6837
6857
  unavailableHosts: balanceHosts.unavailableHosts,
6838
6858
  };
6839
6859
  let selected;
@@ -46,6 +46,8 @@ export interface ResolveLatestOptions {
46
46
  export type Transport = 'npm' | 'git' | 'local' | 'marketplace';
47
47
  /** Prefix that routes a source through the HQ marketplace transport (US-006). */
48
48
  export declare const MARKETPLACE_PREFIX = "marketplace:";
49
+ /** Pinned source allowed to claim the reserved hq-anywhere pack name via its CLI alias. */
50
+ export declare const PINNED_HQ_ANYWHERE_SOURCE = "github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere";
49
51
  export declare function classify(source: string): Transport;
50
52
  /**
51
53
  * Parse a git source's '#<...>' fragment into { url, subpath, ref }.
@@ -411,6 +413,8 @@ export declare function runScanPackages(hqRoot: string, opts?: {
411
413
  }): void;
412
414
  export interface InstallPackOptions {
413
415
  allowHooks?: boolean;
416
+ /** Internal grant used only by the @indigoai-us/hq-anywhere CLI alias. */
417
+ allowPinnedHqAnywhereName?: true;
414
418
  /** Company context for online authorization of install-time MCP secret refs. */
415
419
  company?: string;
416
420
  /**
@@ -112,6 +112,8 @@ async function latestWithDiskCache(cacheKey, opts, refresh) {
112
112
  }
113
113
  /** Prefix that routes a source through the HQ marketplace transport (US-006). */
114
114
  export const MARKETPLACE_PREFIX = 'marketplace:';
115
+ /** Pinned source allowed to claim the reserved hq-anywhere pack name via its CLI alias. */
116
+ export const PINNED_HQ_ANYWHERE_SOURCE = 'github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere';
115
117
  export function classify(source) {
116
118
  // US-006: marketplace:<slug>[@version] is recognized BEFORE the legacy
117
119
  // registry fallback and ahead of every other transport. The slug is resolved
@@ -1727,10 +1729,16 @@ async function installPackWithoutLease(source, opts) {
1727
1729
  });
1728
1730
  break;
1729
1731
  }
1730
- // Only an explicit local directory source may use the reserved bundled
1731
- // pack name. Publisher is self-declared package metadata and is not proof
1732
- // of registry identity, so remote transports never receive this exception.
1733
- const pkg = validateManifest(fetched.payloadDir, hqVersion, { allowReservedName: transport === 'local' });
1732
+ // Local directories retain their existing reserved-name behavior. For
1733
+ // remote sources, publisher is self-declared and cannot prove registry
1734
+ // identity, so accept the reserved name only when the top-level CLI alias
1735
+ // supplied its internal grant for the exact pinned GitHub source.
1736
+ const allowPinnedAliasName = transport === 'git' &&
1737
+ source === PINNED_HQ_ANYWHERE_SOURCE &&
1738
+ opts.allowPinnedHqAnywhereName === true;
1739
+ const pkg = validateManifest(fetched.payloadDir, hqVersion, {
1740
+ allowReservedName: transport === 'local' || allowPinnedAliasName,
1741
+ });
1734
1742
  // M0 — pack-to-pack dependency pre-flight. Runs BEFORE any prompts or writes
1735
1743
  // so a missing/unsatisfied `requires.packs` aborts with no partial state, and
1736
1744
  // before we bother the operator with hook/MCP trust prompts for a pack that
@@ -26,7 +26,7 @@ import chalk from 'chalk';
26
26
  import semverSatisfies from 'semver/functions/satisfies.js';
27
27
  import { findHqRoot } from '../utils/manifest.js';
28
28
  import { resolveHqRoot as resolveDetectedHqRoot } from './doctor.js';
29
- import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
29
+ import { classify, PINNED_HQ_ANYWHERE_SOURCE, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
30
30
  import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
31
31
  import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
32
32
  function resolveRoot(opts) {
@@ -267,6 +267,9 @@ async function runUpdate(name, opts) {
267
267
  allowMcp: opts.yes || opts.allowMcp,
268
268
  followBranch: opts.branch,
269
269
  quiet: wantsJson(opts),
270
+ ...(pname === 'hq-anywhere' && source === PINNED_HQ_ANYWHERE_SOURCE
271
+ ? { allowPinnedHqAnywhereName: true }
272
+ : {}),
270
273
  });
271
274
  }
272
275
  finally {
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
3
- import { MARKETPLACE_PREFIX, installPack, sourceMatchesPackPattern, } from './pack-install.js';
3
+ import { MARKETPLACE_PREFIX, PINNED_HQ_ANYWHERE_SOURCE, installPack, sourceMatchesPackPattern, } from './pack-install.js';
4
4
  import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
5
5
  /**
6
6
  * True when `source` installs through the marketplace — an explicit
@@ -99,11 +99,12 @@ export function registerPackageInstallCommand(parent) {
99
99
  console.error(chalk.red('Install failed:'), 'HQ Anywhere installation is disabled by the hq-anywhere-runtime flag.');
100
100
  process.exit(1);
101
101
  }
102
- await installPack('github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere', {
102
+ await installPack(PINNED_HQ_ANYWHERE_SOURCE, {
103
103
  company: opts.company,
104
104
  allowHooks: opts.allowHooks,
105
105
  allowMcp: opts.allowMcp,
106
106
  followBranch: opts.branch,
107
+ allowPinnedHqAnywhereName: true,
107
108
  });
108
109
  return;
109
110
  }
@@ -21,8 +21,8 @@ export interface DaemonLockConflictOutcome {
21
21
  }
22
22
  /** Preserve the legacy lock failure unless Anywhere explicitly enables the no-op behavior. */
23
23
  export declare function daemonLockConflictOutcome(lock: DaemonLockResult, anywhereEnabled: boolean): DaemonLockConflictOutcome | null;
24
- /** Node's Windows named-pipe API uses a default DACL that is not user-scoped. */
25
- export declare function hqdStartupRefusal(platform: NodeJS.Platform, anywhereEnabled: boolean): string | null;
24
+ /** hqd is available on every platform while Anywhere is enabled; its listener fails closed if setup is unsafe. */
25
+ export declare function shouldStartHqd(anywhereEnabled: boolean): boolean;
26
26
  /** Route an updater request through the daemon's existing graceful handoff. */
27
27
  export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
28
28
  export interface RunDaemonOptions {
@@ -63,11 +63,9 @@ export function daemonLockConflictOutcome(lock, anywhereEnabled) {
63
63
  : "another hq daemon is starting up.\n",
64
64
  };
65
65
  }
66
- /** Node's Windows named-pipe API uses a default DACL that is not user-scoped. */
67
- export function hqdStartupRefusal(platform, anywhereEnabled) {
68
- if (platform !== "win32" || !anywhereEnabled)
69
- return null;
70
- return "hqd is disabled on Windows because the default named-pipe ACL grants read access to Everyone and Anonymous accounts.";
66
+ /** hqd is available on every platform while Anywhere is enabled; its listener fails closed if setup is unsafe. */
67
+ export function shouldStartHqd(anywhereEnabled) {
68
+ return anywhereEnabled;
71
69
  }
72
70
  /** Route an updater request through the daemon's existing graceful handoff. */
73
71
  export function handoffForPackageUpdateRequest(requested, restartDaemon) {
@@ -838,8 +836,7 @@ export async function runDaemon(opts) {
838
836
  // hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
839
837
  let hqd = null;
840
838
  let hqdShutdown = null;
841
- if (anywhereRuntimeEnabled()) {
842
- const refusal = hqdStartupRefusal(platform, true);
839
+ if (shouldStartHqd(anywhereRuntimeEnabled())) {
843
840
  try {
844
841
  const startConfig = readDaemonConfig(paths.configPath);
845
842
  hqd = await startHqdServer({
@@ -852,10 +849,7 @@ export async function runDaemon(opts) {
852
849
  });
853
850
  }
854
851
  catch (err) {
855
- if (refusal && err.message === refusal)
856
- log("error", refusal);
857
- else
858
- log("error", `hqd socket did not start; services run without it: ${err.message}`);
852
+ log("error", `hqd socket did not start; services run without it: ${err.message}`);
859
853
  }
860
854
  }
861
855
  // US-012: 60s presence heartbeat with version and session count.
@@ -182,7 +182,7 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
182
182
  ? "Windows named pipe (mode and owner are not reported)"
183
183
  : `mode ${socket.mode ?? "unknown"}; owner matches current user: ${socket.ownerMatchesUser === null ? "unknown" : socket.ownerMatchesUser ? "yes" : "no"}; user-only: ${socket.userOnly === null ? "unknown" : socket.userOnly ? "yes" : "no"}`;
184
184
  const unreachableMessage = socket.transport === "windows_named_pipe"
185
- ? `hqd is not reachable on ${socket.path}; Windows daemon startup is refused because named-pipe ACLs are unsupported`
185
+ ? `hqd is not reachable on ${socket.path}; secure Windows named-pipe setup may have refused an unsafe ACL`
186
186
  : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
187
187
  const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
188
188
  const warningNotes = [
@@ -192,7 +192,7 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
192
192
  const message = health.reachable
193
193
  ? `hqd reachable; version ${health.version ?? "unknown"}; uptime ${health.uptimeMs === null ? "unknown" : `${new Intl.NumberFormat("en-US").format(health.uptimeMs)} ms`}; runtime flag ${flagLabel}; last successful flag read ${age} ago; held through read error: ${flag.heldThroughReadError ? "yes" : "no"}; cwd binding ${binding}; socket ${socket.path}; ${socketSecurity}${warningNotes.length ? `; ${warningNotes.join("; ")}` : ""}`
194
194
  : unreachableMessage;
195
- const remediation = !health.reachable && socket.transport === "unix_socket" && serviceState
195
+ const remediation = !health.reachable && serviceState
196
196
  ? serviceState.installed ? REMEDIATION.daemonRestart : REMEDIATION.daemonInstall
197
197
  : healthUnsupported || socketNotUserOnly ? REMEDIATION.daemonRestart : undefined;
198
198
  return {
@@ -1,6 +1,6 @@
1
1
  import type { AuthState } from "./accounts.js";
2
2
  import type { HostCapacityAccount, HostCapacitySnapshot } from "./host-capacity.js";
3
- import type { LaneHost, LaneRecord, LanesConfig } from "./types.js";
3
+ import type { LaneHost, LanePlacementPolicy, LaneRecord, LanesConfig } from "./types.js";
4
4
  import { type QuotaState } from "./quota.js";
5
5
  export declare const BALANCE_LIVE_STATES: readonly ["running", "queued", "awaiting_input"];
6
6
  export declare const BALANCE_MOVABLE_STATES: readonly ["interrupted", "done", "awaiting_input"];
@@ -81,6 +81,10 @@ export declare function selectClaudeAccount(input: {
81
81
  recentUsageEnabled?: boolean;
82
82
  now?: Date;
83
83
  explicitAccount?: string;
84
+ requestedModelSource?: "flag" | "profile" | "lane" | "default";
85
+ requestedPlacement?: LanePlacementPolicy | null;
86
+ /** Actual host where the lane would run; targetHost may be a distinct balance host. */
87
+ actualTargetHost?: LaneHost;
84
88
  unavailableHosts?: readonly BalanceHostGap[];
85
89
  }): ClaudeAccountSelection;
86
90
  export declare function accountLoadsForReport(input: {
@@ -119,6 +119,9 @@ function targetIneligibilityReason(accountId, globalEnabled, allowedAccounts, ta
119
119
  return "login_status_unknown";
120
120
  return undefined;
121
121
  }
122
+ function configModelForAccount(config, accountId) {
123
+ return config.providers.claude.accounts[accountId]?.model;
124
+ }
122
125
  function buildLoads(input) {
123
126
  const configAccounts = input.config.providers.claude.accounts;
124
127
  const targetHost = input.target.capacity.host;
@@ -156,10 +159,16 @@ function buildLoads(input) {
156
159
  return { loads, skipped };
157
160
  }
158
161
  export function selectClaudeAccount(input) {
162
+ const actualTargetHost = input.actualTargetHost ?? input.targetHost;
163
+ const placementDetails = {
164
+ requested_placement: input.requestedPlacement ?? null,
165
+ target_host: actualTargetHost,
166
+ ...(sameHost(actualTargetHost, input.targetHost) ? {} : { balance_host: input.targetHost }),
167
+ };
159
168
  const target = input.hosts.find((host) => sameHost(host.capacity.host, input.targetHost));
160
169
  if (!target) {
161
170
  throw new LanesError("claude_account_balance_target_host_unavailable", `Cannot verify Claude accounts on target host ${input.targetHost.kind}:${input.targetHost.id}`, {
162
- target_host: input.targetHost,
171
+ ...placementDetails,
163
172
  unavailable_hosts: input.unavailableHosts ?? [],
164
173
  });
165
174
  }
@@ -170,8 +179,16 @@ export function selectClaudeAccount(input) {
170
179
  if (!selected || !selected.eligible) {
171
180
  throw new LanesError("claude_account_balance_account_unavailable", `Explicit Claude account ${JSON.stringify(input.explicitAccount)} is unavailable on ${input.targetHost.kind}:${input.targetHost.id}`, {
172
181
  account: input.explicitAccount,
173
- target_host: input.targetHost,
174
182
  reason: selected?.ineligible_reason ?? "unknown_account",
183
+ ...placementDetails,
184
+ compared: {
185
+ requested_model: input.model ?? configModelForAccount(input.config, input.explicitAccount) ?? input.config.providers.claude.model ?? null,
186
+ requested_model_source: input.requestedModelSource ?? "default",
187
+ account_model: configModelForAccount(input.config, input.explicitAccount) ?? null,
188
+ account_model_source: `personal/lanes/config.json#providers.claude.accounts.${input.explicitAccount}.model`,
189
+ host: input.targetHost,
190
+ reason: selected?.ineligible_reason ?? "unknown_account",
191
+ },
175
192
  account_reasons: skipped,
176
193
  unavailable_hosts: input.unavailableHosts ?? [],
177
194
  });
@@ -196,7 +213,7 @@ export function selectClaudeAccount(input) {
196
213
  : "";
197
214
  throw new LanesError("claude_account_balance_no_eligible_account", `No enabled, logged-in Claude account is available on ${input.targetHost.kind}:${input.targetHost.id}${unknownLoginDetail}`, {
198
215
  provider: "claude",
199
- target_host: input.targetHost,
216
+ ...placementDetails,
200
217
  account_reasons: loads.map((load) => ({
201
218
  account: load.account,
202
219
  reason: load.ineligible_reason ?? "unavailable",
@@ -334,11 +334,18 @@ function selectSpawnAccount(input) {
334
334
  const selection = selectClaudeAccount({
335
335
  hosts: [{ capacity, lanes, laneListAvailable: true }],
336
336
  targetHost: lane.host,
337
+ actualTargetHost: lane.host,
338
+ requestedPlacement: null,
337
339
  config,
338
340
  quotaState: loadQuotaState(hqRoot),
339
341
  ...((recordedModel ?? pinnedModel ?? profileModel) !== undefined
340
342
  ? { model: recordedModel ?? pinnedModel ?? profileModel }
341
343
  : {}),
344
+ requestedModelSource: recordedModel !== undefined || pinnedModel !== undefined
345
+ ? "lane"
346
+ : profileModel !== undefined
347
+ ? "profile"
348
+ : "default",
342
349
  });
343
350
  return {
344
351
  account: resolveAccount(lane.provider, selection.account, config, context),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.17",
3
+ "version": "5.345.19",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {