@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 +8 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +12 -5
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +12 -5
- package/assets/scaffold/core/scripts/hq-job-probe.sh +12 -5
- package/assets/scaffold/core/scripts/jobs-validate.sh +12 -5
- package/assets/scaffold/core/scripts/session-journal.sh +55 -6
- package/dist/commands/core-session-journal-native.js +2 -2
- package/dist/commands/lanes-accounts.js +1 -0
- package/dist/commands/lanes.js +20 -0
- package/dist/commands/pack-install.d.ts +4 -0
- package/dist/commands/pack-install.js +12 -4
- package/dist/commands/packs.js +4 -1
- package/dist/commands/pkg-install.js +3 -2
- package/dist/lib/daemon/run.d.ts +2 -2
- package/dist/lib/daemon/run.js +5 -11
- package/dist/lib/doctor/checks/anywhere.js +2 -2
- package/dist/lib/lanes/claude-balance.d.ts +5 -1
- package/dist/lib/lanes/claude-balance.js +20 -3
- package/dist/lib/lanes/spawn.js +7 -0
- package/package.json +1 -1
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
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
});
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
-
//
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
|
|
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
|
package/dist/commands/packs.js
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
25
|
-
export declare function
|
|
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 {
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -63,11 +63,9 @@ export function daemonLockConflictOutcome(lock, anywhereEnabled) {
|
|
|
63
63
|
: "another hq daemon is starting up.\n",
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
67
|
-
export function
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
...placementDetails,
|
|
200
217
|
account_reasons: loads.map((load) => ({
|
|
201
218
|
account: load.account,
|
|
202
219
|
reason: load.ineligible_reason ?? "unavailable",
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -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),
|