@indigoai-us/hq-cli 5.345.16 → 5.345.18

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,10 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.18] — 2026-10-06
6
+ - Git Bash forwarders now keep slash commands intact and convert file path arguments that hq needs on Windows.
7
+ - 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.
8
+
5
9
  ## [5.345.16] — 2026-10-06
10
+ - Claude account model settings are defaults, so explicit lane models remain eligible; explicit account selections on create, resume, and message are recorded as CLI choices.
11
+ - When `hq-anywhere-runtime` is enabled, `hq install --global --runtime claude --via plugin` installs the `hq` plugin from the `indigoai-us/hq-packages` Claude marketplace, and `hq install @indigoai-us/hq-anywhere` installs the Codex pack from that repository. The flag-off path refuses the HQ Anywhere alias without fetching a package source.
12
+
13
+ ## [5.345.15] — 2026-10-06
6
14
  - Route live hook Sentry reports through the fast CLI path.
7
15
 
8
- - Claude account model settings are defaults, so explicit lane models remain eligible; explicit account selections on create, resume, and message are recorded as CLI choices.
9
16
  - When `hq-anywhere-runtime` is enabled, `hq install --global --runtime claude --via plugin` installs the `hq` plugin from the `indigoai-us/hq-packages` Claude marketplace, and `hq install @indigoai-us/hq-anywhere` installs the Codex pack from that repository. The flag-off path refuses the HQ Anywhere alias without fetching a package source.
10
17
 
11
18
  ## [5.345.14] — 2026-10-06
@@ -1,13 +1,33 @@
1
1
  #!/usr/bin/env bash
2
- # FORWARDER — native implementation: `hq core generate-workers-registry`.
2
+ # FORWARDER — the implementation of this script now lives in the hq CLI.
3
3
  #
4
- # This preserves the legacy script ABI. It discovers the live HQ root exactly
5
- # as the original script did, forwards every argument untouched, and execs the
6
- # CLI so stdout, stderr, exit status, and signal disposition remain the child's.
4
+ # It ships at assets/scaffold/core/scripts/generate-workers-registry.sh inside @indigoai-us/hq-cli and
5
+ # runs as the hidden command `hq core generate-workers-registry`. This file stays behind so every
6
+ # existing caller — skills, other scripts, CI, and muscle memory — keeps working
7
+ # against the path it already knows.
8
+ #
9
+ # Why the implementation moved: it is a derived-index generator invoked explicitly and by reindex, so
10
+ # process startup is immaterial and the CLI already owns the index primitives.
11
+ #
12
+ # Equivalence is not assumed. The CLI's native implementation is pinned against a
13
+ # byte-frozen copy of this script's pre-migration source by a differential suite
14
+ # (hq-cli e2e/registry-scan-differential.test.ts), which compares exit status,
15
+ # stdout, stderr and a sha256 manifest of every file touched, across repeated runs.
16
+ #
17
+ # The ABI is preserved exactly: arguments are forwarded unchanged, stdin is never
18
+ # read by this file, stdout and stderr are inherited untouched, and the child
19
+ # replaces this process so its exit code and signal disposition become the
20
+ # caller's.
7
21
 
8
22
  set -euo pipefail
9
23
 
10
- HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
24
+ FORWARDER_PATH="${BASH_SOURCE[0]}"
25
+ FORWARDER_DIR="${FORWARDER_PATH%/*}"
26
+ [ "$FORWARDER_DIR" != "$FORWARDER_PATH" ] || FORWARDER_DIR=.
27
+ SCRIPT_DIR="$(cd "$FORWARDER_DIR" && pwd)"
28
+
29
+ # This forwarder sits in the tree it targets, so its own location IS the root.
30
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
11
31
 
12
32
  if ! command -v hq >/dev/null 2>&1; then
13
33
  echo "generate-workers-registry.sh: requires the hq CLI — this script's implementation now ships with it." >&2
@@ -15,4 +35,108 @@ if ! command -v hq >/dev/null 2>&1; then
15
35
  exit 127
16
36
  fi
17
37
 
18
- exec hq core --hq-root "$HQ_ROOT" generate-workers-registry "$@"
38
+ if [ -f "$SCRIPT_DIR/lib/hq-cli-floor.sh" ]; then
39
+ # shellcheck source=lib/hq-cli-floor.sh
40
+ . "$SCRIPT_DIR/lib/hq-cli-floor.sh"
41
+ hq_cli_floor_check "generate-workers-registry.sh" "5.78.0"
42
+ fi
43
+
44
+ # MSYS path conversion is disabled for the hq process so slash commands arrive unchanged.
45
+ # Convert only this generated root path to Windows form for the native CLI.
46
+ if command -v cygpath >/dev/null 2>&1; then
47
+ HQ_ROOT="$(cygpath -m "$HQ_ROOT")"
48
+ fi
49
+
50
+ forwarded_args=("$@")
51
+ forwarded_arg_count=$#
52
+ PATH_OPERANDS=none
53
+ convert_forwarded_path() {
54
+ local arg_index="$1"
55
+ local value="${forwarded_args[$arg_index]}"
56
+ case "$value" in
57
+ /*) forwarded_args[$arg_index]="$(cygpath -m "$value")" ;;
58
+ esac
59
+ }
60
+
61
+ if command -v cygpath >/dev/null 2>&1; then
62
+ IFS=';' read -r -a path_rules <<< "$PATH_OPERANDS"
63
+ for path_rule in "${path_rules[@]}"; do
64
+ case "$path_rule" in
65
+ none) ;;
66
+ position:*)
67
+ index="${path_rule#position:}"
68
+ if (( index < forwarded_arg_count )); then convert_forwarded_path "$index"; fi
69
+ ;;
70
+ first-nonoption)
71
+ for ((index = 0; index < forwarded_arg_count; index++)); do
72
+ case "${forwarded_args[index]}" in --*) ;; *) convert_forwarded_path "$index"; break ;; esac
73
+ done
74
+ ;;
75
+ all-nonoptions)
76
+ for ((index = 0; index < forwarded_arg_count; index++)); do
77
+ case "${forwarded_args[index]}" in --*) ;; *) convert_forwarded_path "$index" ;; esac
78
+ done
79
+ ;;
80
+ options:*)
81
+ IFS=, read -r -a path_options <<< "${path_rule#options:}"
82
+ for ((index = 0; index < forwarded_arg_count; index++)); do
83
+ for path_option in "${path_options[@]}"; do
84
+ case "${forwarded_args[index]}" in
85
+ "$path_option")
86
+ index=$((index + 1))
87
+ if (( index < forwarded_arg_count )); then convert_forwarded_path "$index"; fi
88
+ break
89
+ ;;
90
+ "$path_option"=*)
91
+ value="${forwarded_args[index]#*=}"
92
+ case "$value" in /*) forwarded_args[index]="${path_option}=$(cygpath -m "$value")" ;; esac
93
+ break
94
+ ;;
95
+ esac
96
+ done
97
+ done
98
+ ;;
99
+ comma:*)
100
+ path_option="${path_rule#comma:}"
101
+ for ((index = 0; index < forwarded_arg_count; index++)); do
102
+ case "${forwarded_args[index]}" in
103
+ "$path_option")
104
+ index=$((index + 1))
105
+ if (( index < forwarded_arg_count )); then
106
+ IFS=, read -r -a path_values <<< "${forwarded_args[index]}"
107
+ converted_values=()
108
+ for path_value in "${path_values[@]}"; do
109
+ case "$path_value" in /*) converted_values+=("$(cygpath -m "$path_value")") ;; *) converted_values+=("$path_value") ;; esac
110
+ done
111
+ value="$(IFS=,; printf '%s' "${converted_values[*]}")"
112
+ forwarded_args[index]="$value"
113
+ fi
114
+ break
115
+ ;;
116
+ "$path_option"=*)
117
+ value="${forwarded_args[index]#*=}"
118
+ case "$value" in
119
+ /*,*|/*)
120
+ IFS=, read -r -a path_values <<< "$value"
121
+ converted_values=()
122
+ for path_value in "${path_values[@]}"; do
123
+ case "$path_value" in /*) converted_values+=("$(cygpath -m "$path_value")") ;; *) converted_values+=("$path_value") ;; esac
124
+ done
125
+ value="$(IFS=,; printf '%s' "${converted_values[*]}")"
126
+ forwarded_args[index]="${path_option}=$value"
127
+ ;;
128
+ esac
129
+ break
130
+ ;;
131
+ esac
132
+ done
133
+ ;;
134
+ *) echo "unknown path_operands rule: $path_rule" >&2; exit 70 ;;
135
+ esac
136
+ done
137
+ fi
138
+ if [ "$forwarded_arg_count" -gt 0 ]; then
139
+ MSYS2_ARG_CONV_EXCL='*' exec hq core --hq-root "$HQ_ROOT" generate-workers-registry "${forwarded_args[@]}"
140
+ else
141
+ MSYS2_ARG_CONV_EXCL='*' exec hq core --hq-root "$HQ_ROOT" generate-workers-registry
142
+ fi
@@ -60,7 +60,7 @@ if [ "${HQ_HQ_SESSION_NO_CLI:-}" != "1" ]; then
60
60
  case "$__hs_caps" in
61
61
  *hq-session*)
62
62
  case "$__hs_caps" in
63
- *exact-company-directory-match-v1*) exec hq core hq-session "$@" ;;
63
+ *exact-company-directory-match-v1*) MSYS2_ARG_CONV_EXCL='*' exec hq core hq-session "$@" ;;
64
64
  esac
65
65
  ;;
66
66
  esac
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env bash
2
+ # Shared hq-cli version-floor helpers for setup and the standalone hook doctor.
3
+ # Keep this compatible with macOS Bash 3.2 and Linux Bash 5.
4
+
5
+ HQ_CLI_FLOOR_UPGRADE_COMMAND='npm install -g @indigoai-us/hq-cli@latest'
6
+
7
+ # Print the top-level requiresHqCli value from core/core.yaml without yq.
8
+ # Return 1 when the key/file is absent and 2 when the key is duplicated.
9
+ hq_cli_floor_required() {
10
+ local hq_root="$1"
11
+ local core_yaml="${hq_root%/}/core/core.yaml"
12
+ [ -r "$core_yaml" ] || return 1
13
+
14
+ awk '
15
+ BEGIN { found = 0; invalid = 0 }
16
+ /^requiresHqCli[[:space:]]*:/ {
17
+ if (found) { invalid = 1; next }
18
+ found = 1
19
+ value = $0
20
+ sub(/^requiresHqCli[[:space:]]*:[[:space:]]*/, "", value)
21
+ sub(/[[:space:]]+#.*$/, "", value)
22
+ sub(/^[[:space:]]*/, "", value)
23
+ sub(/[[:space:]]*$/, "", value)
24
+ quote = sprintf("%c", 39)
25
+ first = substr(value, 1, 1)
26
+ last = substr(value, length(value), 1)
27
+ if ((first == "\"" && last == "\"") || (first == quote && last == quote)) {
28
+ value = substr(value, 2, length(value) - 2)
29
+ }
30
+ }
31
+ END {
32
+ if (invalid) exit 2
33
+ if (!found) exit 1
34
+ print value
35
+ }
36
+ ' "$core_yaml"
37
+ }
38
+
39
+ # Resolve an executable through symlinks using portable readlink calls.
40
+ _hq_cli_floor_resolve_executable() {
41
+ local executable="$1" link_dir link_target hops=0
42
+ case "$executable" in
43
+ /*) ;;
44
+ *) executable="$PWD/$executable" ;;
45
+ esac
46
+
47
+ while [ -L "$executable" ]; do
48
+ hops=$((hops + 1))
49
+ [ "$hops" -le 40 ] || return 1
50
+ link_dir="${executable%/*}"
51
+ [ -n "$link_dir" ] || link_dir="/"
52
+ link_dir="$(cd -P "$link_dir" 2>/dev/null && pwd)" || return 1
53
+ link_target="$(readlink "$executable" 2>/dev/null)" || return 1
54
+ case "$link_target" in
55
+ /*) executable="$link_target" ;;
56
+ *) executable="$link_dir/$link_target" ;;
57
+ esac
58
+ done
59
+
60
+ [ -f "$executable" ] || return 1
61
+ link_dir="${executable%/*}"
62
+ [ -n "$link_dir" ] || link_dir="/"
63
+ link_dir="$(cd -P "$link_dir" 2>/dev/null && pwd)" || return 1
64
+ printf '%s/%s\n' "${link_dir%/}" "${executable##*/}"
65
+ }
66
+
67
+ _hq_cli_floor_package_version() {
68
+ local package_json="$1"
69
+ awk -F '"' '
70
+ {
71
+ for (i = 2; i + 2 <= NF; i += 4) {
72
+ key = $i
73
+ value = $(i + 2)
74
+ if (key == "name" && package_name == "") package_name = value
75
+ else if (key == "version" && package_version == "") package_version = value
76
+ }
77
+ }
78
+ END {
79
+ if (package_name == "@indigoai-us/hq-cli" && package_version != "") {
80
+ print package_version
81
+ }
82
+ }
83
+ ' "$package_json"
84
+ }
85
+
86
+ # Read the package version using shell builtins on the warm path. The npm
87
+ # package's top-level version is the first version key in its package.json.
88
+ _hq_cli_floor_package_version_builtin() {
89
+ local package_json="$1" line
90
+ while IFS= read -r line || [ -n "$line" ]; do
91
+ if [[ "$line" =~ \"version\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
92
+ printf '%s\n' "${BASH_REMATCH[1]}"
93
+ return 0
94
+ fi
95
+ done < "$package_json"
96
+ return 1
97
+ }
98
+
99
+ # Return the installed CLI version without starting another Node process on the
100
+ # hook path. A package's version field is the same version reported by its
101
+ # generated hq entry point. Unrecognized or malformed installations retain the
102
+ # historical --version probe as a fallback.
103
+ _hq_cli_floor_installed_version() {
104
+ local hq_command cached_schema cached_command cached_target cached_package_json cached_version current_package_version
105
+ local cache_dir cache_file resolved dir package_json package_version parent raw_version parsed_version
106
+
107
+ hq_command="$(command -v hq 2>/dev/null)" || return 127
108
+ [ -n "$hq_command" ] || return 127
109
+
110
+ # The cache hit path is intentionally shell-only. Compare the selected hq
111
+ # entry point and its current target with the entry recorded on the cold
112
+ # lookup; -nt also catches an in-place replacement of that target.
113
+ cache_dir="${XDG_CACHE_HOME:-${HOME:-}/.cache}/hq-cli-floor"
114
+ cache_file="$cache_dir/version"
115
+ if [ -r "$cache_file" ]; then
116
+ {
117
+ IFS= read -r cached_schema || cached_schema=""
118
+ IFS= read -r cached_command || cached_command=""
119
+ IFS= read -r cached_target || cached_target=""
120
+ IFS= read -r cached_package_json || cached_package_json=""
121
+ IFS= read -r cached_version || cached_version=""
122
+ } < "$cache_file"
123
+ if [ "$cached_schema" = "hq-cli-floor.v2" ] &&
124
+ [ "$cached_command" = "$hq_command" ] && [ -n "$cached_target" ] &&
125
+ [ -n "$cached_package_json" ] && [ -n "$cached_version" ] &&
126
+ _hq_cli_floor_parse_semver "$cached_version" >/dev/null 2>&1 &&
127
+ [ -e "$cached_target" ] && [ -f "$cached_package_json" ] &&
128
+ [ "$hq_command" -ef "$cached_target" ] &&
129
+ ! [[ "$cached_target" -nt "$cache_file" ]] &&
130
+ ! [[ "$cached_package_json" -nt "$cache_file" ]] &&
131
+ current_package_version="$( _hq_cli_floor_package_version_builtin "$cached_package_json" )" &&
132
+ [ "$current_package_version" = "$cached_version" ]; then
133
+ printf '%s\n' "$cached_version"
134
+ return 0
135
+ fi
136
+ fi
137
+
138
+ resolved="$( _hq_cli_floor_resolve_executable "$hq_command" 2>/dev/null )" || resolved=""
139
+ if [ -n "$resolved" ]; then
140
+ dir="${resolved%/*}"
141
+ while [ -n "$dir" ] && [ "$dir" != / ]; do
142
+ package_json="$dir/package.json"
143
+ if [ -f "$package_json" ]; then
144
+ package_version="$( _hq_cli_floor_package_version "$package_json" 2>/dev/null )" || package_version=""
145
+ if [ -n "$package_version" ]; then
146
+ if mkdir -p "$cache_dir" 2>/dev/null; then
147
+ local cache_tmp="$cache_file.$$"
148
+ if {
149
+ printf 'hq-cli-floor.v2\n'
150
+ printf '%s\n' "$hq_command"
151
+ printf '%s\n' "$resolved"
152
+ printf '%s\n' "$package_json"
153
+ printf '%s\n' "$package_version"
154
+ } > "$cache_tmp" 2>/dev/null; then
155
+ mv -f "$cache_tmp" "$cache_file" 2>/dev/null || rm -f "$cache_tmp" 2>/dev/null || true
156
+ fi
157
+ fi
158
+ printf '%s\n' "$package_version"
159
+ return 0
160
+ fi
161
+ fi
162
+ parent="${dir%/*}"
163
+ [ -n "$parent" ] || parent="/"
164
+ [ "$parent" != "$dir" ] || break
165
+ dir="$parent"
166
+ done
167
+ raw_version="$("$resolved" --version 2>/dev/null)" || return 1
168
+ else
169
+ raw_version="$(hq --version 2>/dev/null)" || return 1
170
+ fi
171
+ parsed_version="$(printf '%s\n' "$raw_version" | awk '
172
+ match($0, /[v]?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?/) {
173
+ version = substr($0, RSTART, RLENGTH)
174
+ sub(/^v/, "", version)
175
+ print version
176
+ exit
177
+ }
178
+ ')"
179
+ [ -n "$parsed_version" ] || return 1
180
+ printf '%s\n' "$parsed_version"
181
+ }
182
+
183
+ # Parse strict SemVer fields into private globals for the comparator.
184
+ _hq_cli_floor_parse_semver() {
185
+ local version="$1" core prerelease="" build="" identifier
186
+ local -a identifiers=()
187
+
188
+ case "$version" in
189
+ *+*)
190
+ build="${version#*+}"
191
+ [[ "$build" != *+* ]] || return 1
192
+ version="${version%%+*}"
193
+ [[ "$build" =~ ^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$ ]] || return 1
194
+ ;;
195
+ esac
196
+ case "$version" in
197
+ *-*)
198
+ prerelease="${version#*-}"
199
+ version="${version%%-*}"
200
+ [[ "$prerelease" =~ ^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$ ]] || return 1
201
+ IFS=. read -r -a identifiers <<< "$prerelease"
202
+ for identifier in "${identifiers[@]}"; do
203
+ if [[ "$identifier" =~ ^[0-9]+$ ]] && [ "${#identifier}" -gt 1 ] && [[ "$identifier" == 0* ]]; then
204
+ return 1
205
+ fi
206
+ done
207
+ ;;
208
+ esac
209
+
210
+ core="$version"
211
+ [[ "$core" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || return 1
212
+ HQ_CLI_FLOOR_MAJOR="${BASH_REMATCH[1]}"
213
+ HQ_CLI_FLOOR_MINOR="${BASH_REMATCH[2]}"
214
+ HQ_CLI_FLOOR_PATCH="${BASH_REMATCH[3]}"
215
+ HQ_CLI_FLOOR_PRERELEASE="$prerelease"
216
+ }
217
+
218
+ _hq_cli_floor_numeric_compare() {
219
+ local left="$1" right="$2"
220
+ if [ "${#left}" -lt "${#right}" ]; then
221
+ HQ_CLI_FLOOR_CMP=-1
222
+ elif [ "${#left}" -gt "${#right}" ]; then
223
+ HQ_CLI_FLOOR_CMP=1
224
+ elif [ "$left" = "$right" ]; then
225
+ HQ_CLI_FLOOR_CMP=0
226
+ elif [[ "$left" > "$right" ]]; then
227
+ HQ_CLI_FLOOR_CMP=1
228
+ else
229
+ HQ_CLI_FLOOR_CMP=-1
230
+ fi
231
+ }
232
+
233
+ _hq_cli_floor_prerelease_compare() {
234
+ local left="$1" right="$2" i left_id right_id left_num right_num
235
+ local -a left_parts=() right_parts=()
236
+ IFS=. read -r -a left_parts <<< "$left"
237
+ IFS=. read -r -a right_parts <<< "$right"
238
+
239
+ for ((i = 0; i < ${#left_parts[@]} && i < ${#right_parts[@]}; i++)); do
240
+ left_id="${left_parts[$i]}"
241
+ right_id="${right_parts[$i]}"
242
+ left_num=0
243
+ right_num=0
244
+ [[ "$left_id" =~ ^[0-9]+$ ]] && left_num=1
245
+ [[ "$right_id" =~ ^[0-9]+$ ]] && right_num=1
246
+ if [ "$left_num" -eq 1 ] && [ "$right_num" -eq 1 ]; then
247
+ _hq_cli_floor_numeric_compare "$left_id" "$right_id"
248
+ elif [ "$left_num" -eq 1 ]; then
249
+ HQ_CLI_FLOOR_CMP=-1
250
+ elif [ "$right_num" -eq 1 ]; then
251
+ HQ_CLI_FLOOR_CMP=1
252
+ elif [ "$left_id" = "$right_id" ]; then
253
+ HQ_CLI_FLOOR_CMP=0
254
+ elif [[ "$left_id" > "$right_id" ]]; then
255
+ HQ_CLI_FLOOR_CMP=1
256
+ else
257
+ HQ_CLI_FLOOR_CMP=-1
258
+ fi
259
+ [ "$HQ_CLI_FLOOR_CMP" -eq 0 ] || return 0
260
+ done
261
+
262
+ if [ "${#left_parts[@]}" -lt "${#right_parts[@]}" ]; then
263
+ HQ_CLI_FLOOR_CMP=-1
264
+ elif [ "${#left_parts[@]}" -gt "${#right_parts[@]}" ]; then
265
+ HQ_CLI_FLOOR_CMP=1
266
+ else
267
+ HQ_CLI_FLOOR_CMP=0
268
+ fi
269
+ }
270
+
271
+ # Pure-Bash SemVer comparison. Returns 0 when have >= want, 1 when older, 2 if invalid.
272
+ hq_cli_version_ge() {
273
+ local LC_ALL=C field left right have_prerelease want_prerelease
274
+ _hq_cli_floor_parse_semver "$1" || return 2
275
+ local have_major="$HQ_CLI_FLOOR_MAJOR" have_minor="$HQ_CLI_FLOOR_MINOR"
276
+ local have_patch="$HQ_CLI_FLOOR_PATCH"
277
+ have_prerelease="$HQ_CLI_FLOOR_PRERELEASE"
278
+
279
+ _hq_cli_floor_parse_semver "$2" || return 2
280
+ want_prerelease="$HQ_CLI_FLOOR_PRERELEASE"
281
+
282
+ for field in major minor patch; do
283
+ case "$field" in
284
+ major) left="$have_major"; right="$HQ_CLI_FLOOR_MAJOR" ;;
285
+ minor) left="$have_minor"; right="$HQ_CLI_FLOOR_MINOR" ;;
286
+ patch) left="$have_patch"; right="$HQ_CLI_FLOOR_PATCH" ;;
287
+ esac
288
+ _hq_cli_floor_numeric_compare "$left" "$right"
289
+ if [ "$HQ_CLI_FLOOR_CMP" -gt 0 ]; then
290
+ return 0
291
+ elif [ "$HQ_CLI_FLOOR_CMP" -lt 0 ]; then
292
+ return 1
293
+ fi
294
+ done
295
+
296
+ if [ -z "$have_prerelease" ]; then
297
+ return 0
298
+ elif [ -z "$want_prerelease" ]; then
299
+ return 1
300
+ fi
301
+
302
+ _hq_cli_floor_prerelease_compare "$have_prerelease" "$want_prerelease"
303
+ [ "$HQ_CLI_FLOOR_CMP" -ge 0 ]
304
+ }
305
+
306
+ # Return 0 when hq satisfies the floor; otherwise print the required message and return 127.
307
+ hq_cli_floor_check() {
308
+ local script_name="$1" required="$2" minimum have compare_status
309
+ if ! command -v hq >/dev/null 2>&1; then
310
+ printf '%s: requires the hq CLI — this script\047s implementation now ships with it.\n' "$script_name" >&2
311
+ printf 'Install it with: npm install -g @indigoai-us/hq-cli\n' >&2
312
+ return 127
313
+ fi
314
+
315
+ case "$required" in
316
+ '>='*) minimum="${required#>=}" ;;
317
+ *) minimum="$required" ;;
318
+ esac
319
+ if ! _hq_cli_floor_parse_semver "$minimum"; then
320
+ printf '%s: requiresHqCli has unsupported value %s; expected >=X.Y.Z.\n' "$script_name" "$required" >&2
321
+ return 64
322
+ fi
323
+
324
+ have="$( _hq_cli_floor_installed_version )" || have=""
325
+ if [ -z "$have" ]; then
326
+ printf '%s: could not read hq --version; required hq-cli >= %s.\n' "$script_name" "$minimum" >&2
327
+ return 126
328
+ fi
329
+ if hq_cli_version_ge "$have" "$minimum"; then
330
+ return 0
331
+ else
332
+ compare_status=$?
333
+ fi
334
+ if [ "$compare_status" -gt 1 ]; then
335
+ printf '%s: hq --version returned invalid SemVer %s; required hq-cli >= %s.\n' "$script_name" "$have" "$minimum" >&2
336
+ return 126
337
+ fi
338
+
339
+ printf '%s: this script needs hq-cli >= %s (found %s); upgrade with: %s\n' \
340
+ "$script_name" "$minimum" "$have" "$HQ_CLI_FLOOR_UPGRADE_COMMAND" >&2
341
+ return 127
342
+ }
@@ -1,16 +1,133 @@
1
1
  #!/usr/bin/env bash
2
- # FORWARDER — the implementation of this script lives in the hq CLI.
2
+ # FORWARDER — the implementation of this script now lives in the hq CLI.
3
3
  #
4
- # This shipped compatibility path must delegate rather than invoke qmd itself:
5
- # `hq core qmd-reindex-after-sync` participates in hq-cli's shared embed lock.
6
- # Arguments, stdio, exit status, and signal disposition are preserved.
4
+ # It ships at assets/scaffold/core/scripts/qmd-reindex-after-sync.sh inside @indigoai-us/hq-cli and
5
+ # runs as the hidden command `hq core qmd-reindex-after-sync`. This file stays behind so every
6
+ # existing caller — skills, other scripts, CI, and muscle memory — keeps working
7
+ # against the path it already knows.
8
+ #
9
+ # Why the implementation moved: it is a cold, explicitly-invoked operation where
10
+ # process startup is immaterial, so hosting it in the CLI costs nothing and
11
+ # keeps the scaffold to configuration rather than code.
12
+ #
13
+ # The ABI is preserved exactly: arguments are forwarded unchanged, stdin is never
14
+ # read by this file, stdout and stderr are inherited untouched, and the child
15
+ # replaces this process so its exit code and signal disposition become the
16
+ # caller's.
7
17
 
8
18
  set -euo pipefail
9
19
 
20
+ FORWARDER_PATH="${BASH_SOURCE[0]}"
21
+ FORWARDER_DIR="${FORWARDER_PATH%/*}"
22
+ [ "$FORWARDER_DIR" != "$FORWARDER_PATH" ] || FORWARDER_DIR=.
23
+ SCRIPT_DIR="$(cd "$FORWARDER_DIR" && pwd)"
24
+
25
+ # No root is injected: this script derived its root from the CALLER's cwd
26
+ # before it moved (git top level, a cwd walk, or a positional argument), and the
27
+ # CLI preserves that. Anything the caller already exported still applies.
28
+
10
29
  if ! command -v hq >/dev/null 2>&1; then
11
30
  echo "qmd-reindex-after-sync.sh: requires the hq CLI — this script's implementation now ships with it." >&2
12
31
  echo "Install it with: npm install -g @indigoai-us/hq-cli" >&2
13
32
  exit 127
14
33
  fi
15
34
 
16
- exec hq core qmd-reindex-after-sync "$@"
35
+ if [ -f "$SCRIPT_DIR/lib/hq-cli-floor.sh" ]; then
36
+ # shellcheck source=lib/hq-cli-floor.sh
37
+ . "$SCRIPT_DIR/lib/hq-cli-floor.sh"
38
+ hq_cli_floor_check "qmd-reindex-after-sync.sh" "5.78.0"
39
+ fi
40
+
41
+ forwarded_args=("$@")
42
+ forwarded_arg_count=$#
43
+ PATH_OPERANDS=first-nonoption
44
+ convert_forwarded_path() {
45
+ local arg_index="$1"
46
+ local value="${forwarded_args[$arg_index]}"
47
+ case "$value" in
48
+ /*) forwarded_args[$arg_index]="$(cygpath -m "$value")" ;;
49
+ esac
50
+ }
51
+
52
+ if command -v cygpath >/dev/null 2>&1; then
53
+ IFS=';' read -r -a path_rules <<< "$PATH_OPERANDS"
54
+ for path_rule in "${path_rules[@]}"; do
55
+ case "$path_rule" in
56
+ none) ;;
57
+ position:*)
58
+ index="${path_rule#position:}"
59
+ if (( index < forwarded_arg_count )); then convert_forwarded_path "$index"; fi
60
+ ;;
61
+ first-nonoption)
62
+ for ((index = 0; index < forwarded_arg_count; index++)); do
63
+ case "${forwarded_args[index]}" in --*) ;; *) convert_forwarded_path "$index"; break ;; esac
64
+ done
65
+ ;;
66
+ all-nonoptions)
67
+ for ((index = 0; index < forwarded_arg_count; index++)); do
68
+ case "${forwarded_args[index]}" in --*) ;; *) convert_forwarded_path "$index" ;; esac
69
+ done
70
+ ;;
71
+ options:*)
72
+ IFS=, read -r -a path_options <<< "${path_rule#options:}"
73
+ for ((index = 0; index < forwarded_arg_count; index++)); do
74
+ for path_option in "${path_options[@]}"; do
75
+ case "${forwarded_args[index]}" in
76
+ "$path_option")
77
+ index=$((index + 1))
78
+ if (( index < forwarded_arg_count )); then convert_forwarded_path "$index"; fi
79
+ break
80
+ ;;
81
+ "$path_option"=*)
82
+ value="${forwarded_args[index]#*=}"
83
+ case "$value" in /*) forwarded_args[index]="${path_option}=$(cygpath -m "$value")" ;; esac
84
+ break
85
+ ;;
86
+ esac
87
+ done
88
+ done
89
+ ;;
90
+ comma:*)
91
+ path_option="${path_rule#comma:}"
92
+ for ((index = 0; index < forwarded_arg_count; index++)); do
93
+ case "${forwarded_args[index]}" in
94
+ "$path_option")
95
+ index=$((index + 1))
96
+ if (( index < forwarded_arg_count )); then
97
+ IFS=, read -r -a path_values <<< "${forwarded_args[index]}"
98
+ converted_values=()
99
+ for path_value in "${path_values[@]}"; do
100
+ case "$path_value" in /*) converted_values+=("$(cygpath -m "$path_value")") ;; *) converted_values+=("$path_value") ;; esac
101
+ done
102
+ value="$(IFS=,; printf '%s' "${converted_values[*]}")"
103
+ forwarded_args[index]="$value"
104
+ fi
105
+ break
106
+ ;;
107
+ "$path_option"=*)
108
+ value="${forwarded_args[index]#*=}"
109
+ case "$value" in
110
+ /*,*|/*)
111
+ IFS=, read -r -a path_values <<< "$value"
112
+ converted_values=()
113
+ for path_value in "${path_values[@]}"; do
114
+ case "$path_value" in /*) converted_values+=("$(cygpath -m "$path_value")") ;; *) converted_values+=("$path_value") ;; esac
115
+ done
116
+ value="$(IFS=,; printf '%s' "${converted_values[*]}")"
117
+ forwarded_args[index]="${path_option}=$value"
118
+ ;;
119
+ esac
120
+ break
121
+ ;;
122
+ esac
123
+ done
124
+ ;;
125
+ *) echo "unknown path_operands rule: $path_rule" >&2; exit 70 ;;
126
+ esac
127
+ done
128
+ fi
129
+ if [ "$forwarded_arg_count" -gt 0 ]; then
130
+ MSYS2_ARG_CONV_EXCL='*' exec hq core qmd-reindex-after-sync "${forwarded_args[@]}"
131
+ else
132
+ MSYS2_ARG_CONV_EXCL='*' exec hq core qmd-reindex-after-sync
133
+ fi
@@ -37,4 +37,24 @@ if ! command -v hq >/dev/null 2>&1; then
37
37
  exit 127
38
38
  fi
39
39
 
40
- exec hq index background --hq-root "$HQ_ROOT" "$@"
40
+ if command -v cygpath >/dev/null 2>&1; then
41
+ HQ_ROOT="$(cygpath -m "$HQ_ROOT")"
42
+ fi
43
+ if command -v cygpath >/dev/null 2>&1; then
44
+ HQ_ROOT="$(cygpath -m "$HQ_ROOT")"
45
+ fi
46
+ forwarded_args=("$@")
47
+ forwarded_arg_count=$#
48
+ if command -v cygpath >/dev/null 2>&1; then
49
+ for ((index = 0; index < forwarded_arg_count; index++)); do
50
+ case "${forwarded_args[index]}" in
51
+ --log) index=$((index + 1)); if ((index < forwarded_arg_count)); then forwarded_args[index]="$(cygpath -m "${forwarded_args[index]}")"; fi ;;
52
+ --log=*) forwarded_args[index]="--log=$(cygpath -m "${forwarded_args[index]#--log=}")" ;;
53
+ esac
54
+ done
55
+ fi
56
+ if [ "$forwarded_arg_count" -gt 0 ]; then
57
+ MSYS2_ARG_CONV_EXCL='*' exec hq index background --hq-root "$HQ_ROOT" "${forwarded_args[@]}"
58
+ else
59
+ MSYS2_ARG_CONV_EXCL='*' exec hq index background --hq-root "$HQ_ROOT"
60
+ fi
@@ -1,12 +1,33 @@
1
1
  #!/usr/bin/env bash
2
- # FORWARDER — native implementation: `hq core scan-packages`.
2
+ # FORWARDER — the implementation of this script now lives in the hq CLI.
3
3
  #
4
- # The original script treated $HQ_ROOT (or the caller's cwd) as authoritative.
5
- # Keep that root binding and exec the hidden command so the shell ABI survives.
4
+ # It ships at assets/scaffold/core/scripts/scan-packages.sh inside @indigoai-us/hq-cli and
5
+ # runs as the hidden command `hq core scan-packages`. This file stays behind so every
6
+ # existing caller — skills, other scripts, CI, and muscle memory — keeps working
7
+ # against the path it already knows.
8
+ #
9
+ # Why the implementation moved: the pack contribution mapping it encoded is single-sourced in the CLI
10
+ # (src/utils/pack-contributions.ts); keeping a second copy here invited drift.
11
+ #
12
+ # Equivalence is not assumed. The CLI's native implementation is pinned against a
13
+ # byte-frozen copy of this script's pre-migration source by a differential suite
14
+ # (hq-cli e2e/registry-scan-differential.test.ts), which compares exit status,
15
+ # stdout, stderr and a sha256 manifest of every file touched, across repeated runs.
16
+ #
17
+ # The ABI is preserved exactly: arguments are forwarded unchanged, stdin is never
18
+ # read by this file, stdout and stderr are inherited untouched, and the child
19
+ # replaces this process so its exit code and signal disposition become the
20
+ # caller's.
6
21
 
7
22
  set -euo pipefail
8
23
 
9
- HQ_ROOT="${HQ_ROOT:-$PWD}"
24
+ FORWARDER_PATH="${BASH_SOURCE[0]}"
25
+ FORWARDER_DIR="${FORWARDER_PATH%/*}"
26
+ [ "$FORWARDER_DIR" != "$FORWARDER_PATH" ] || FORWARDER_DIR=.
27
+ SCRIPT_DIR="$(cd "$FORWARDER_DIR" && pwd)"
28
+
29
+ # This forwarder sits in the tree it targets, so its own location IS the root.
30
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
10
31
 
11
32
  if ! command -v hq >/dev/null 2>&1; then
12
33
  echo "scan-packages.sh: requires the hq CLI — this script's implementation now ships with it." >&2
@@ -14,4 +35,108 @@ if ! command -v hq >/dev/null 2>&1; then
14
35
  exit 127
15
36
  fi
16
37
 
17
- exec hq core --hq-root "$HQ_ROOT" scan-packages "$@"
38
+ if [ -f "$SCRIPT_DIR/lib/hq-cli-floor.sh" ]; then
39
+ # shellcheck source=lib/hq-cli-floor.sh
40
+ . "$SCRIPT_DIR/lib/hq-cli-floor.sh"
41
+ hq_cli_floor_check "scan-packages.sh" "5.78.0"
42
+ fi
43
+
44
+ # MSYS path conversion is disabled for the hq process so slash commands arrive unchanged.
45
+ # Convert only this generated root path to Windows form for the native CLI.
46
+ if command -v cygpath >/dev/null 2>&1; then
47
+ HQ_ROOT="$(cygpath -m "$HQ_ROOT")"
48
+ fi
49
+
50
+ forwarded_args=("$@")
51
+ forwarded_arg_count=$#
52
+ PATH_OPERANDS=none
53
+ convert_forwarded_path() {
54
+ local arg_index="$1"
55
+ local value="${forwarded_args[$arg_index]}"
56
+ case "$value" in
57
+ /*) forwarded_args[$arg_index]="$(cygpath -m "$value")" ;;
58
+ esac
59
+ }
60
+
61
+ if command -v cygpath >/dev/null 2>&1; then
62
+ IFS=';' read -r -a path_rules <<< "$PATH_OPERANDS"
63
+ for path_rule in "${path_rules[@]}"; do
64
+ case "$path_rule" in
65
+ none) ;;
66
+ position:*)
67
+ index="${path_rule#position:}"
68
+ if (( index < forwarded_arg_count )); then convert_forwarded_path "$index"; fi
69
+ ;;
70
+ first-nonoption)
71
+ for ((index = 0; index < forwarded_arg_count; index++)); do
72
+ case "${forwarded_args[index]}" in --*) ;; *) convert_forwarded_path "$index"; break ;; esac
73
+ done
74
+ ;;
75
+ all-nonoptions)
76
+ for ((index = 0; index < forwarded_arg_count; index++)); do
77
+ case "${forwarded_args[index]}" in --*) ;; *) convert_forwarded_path "$index" ;; esac
78
+ done
79
+ ;;
80
+ options:*)
81
+ IFS=, read -r -a path_options <<< "${path_rule#options:}"
82
+ for ((index = 0; index < forwarded_arg_count; index++)); do
83
+ for path_option in "${path_options[@]}"; do
84
+ case "${forwarded_args[index]}" in
85
+ "$path_option")
86
+ index=$((index + 1))
87
+ if (( index < forwarded_arg_count )); then convert_forwarded_path "$index"; fi
88
+ break
89
+ ;;
90
+ "$path_option"=*)
91
+ value="${forwarded_args[index]#*=}"
92
+ case "$value" in /*) forwarded_args[index]="${path_option}=$(cygpath -m "$value")" ;; esac
93
+ break
94
+ ;;
95
+ esac
96
+ done
97
+ done
98
+ ;;
99
+ comma:*)
100
+ path_option="${path_rule#comma:}"
101
+ for ((index = 0; index < forwarded_arg_count; index++)); do
102
+ case "${forwarded_args[index]}" in
103
+ "$path_option")
104
+ index=$((index + 1))
105
+ if (( index < forwarded_arg_count )); then
106
+ IFS=, read -r -a path_values <<< "${forwarded_args[index]}"
107
+ converted_values=()
108
+ for path_value in "${path_values[@]}"; do
109
+ case "$path_value" in /*) converted_values+=("$(cygpath -m "$path_value")") ;; *) converted_values+=("$path_value") ;; esac
110
+ done
111
+ value="$(IFS=,; printf '%s' "${converted_values[*]}")"
112
+ forwarded_args[index]="$value"
113
+ fi
114
+ break
115
+ ;;
116
+ "$path_option"=*)
117
+ value="${forwarded_args[index]#*=}"
118
+ case "$value" in
119
+ /*,*|/*)
120
+ IFS=, read -r -a path_values <<< "$value"
121
+ converted_values=()
122
+ for path_value in "${path_values[@]}"; do
123
+ case "$path_value" in /*) converted_values+=("$(cygpath -m "$path_value")") ;; *) converted_values+=("$path_value") ;; esac
124
+ done
125
+ value="$(IFS=,; printf '%s' "${converted_values[*]}")"
126
+ forwarded_args[index]="${path_option}=$value"
127
+ ;;
128
+ esac
129
+ break
130
+ ;;
131
+ esac
132
+ done
133
+ ;;
134
+ *) echo "unknown path_operands rule: $path_rule" >&2; exit 70 ;;
135
+ esac
136
+ done
137
+ fi
138
+ if [ "$forwarded_arg_count" -gt 0 ]; then
139
+ MSYS2_ARG_CONV_EXCL='*' exec hq core --hq-root "$HQ_ROOT" scan-packages "${forwarded_args[@]}"
140
+ else
141
+ MSYS2_ARG_CONV_EXCL='*' exec hq core --hq-root "$HQ_ROOT" scan-packages
142
+ fi
@@ -252,6 +252,7 @@ export const SCAFFOLD_ONLY_ASSETS = [
252
252
  { asset: "core/scripts/hook-lib.sh", root: "live", summary: "Shared shell primitives sourced by the checkpoint gate" },
253
253
  { asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
254
254
  { asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
255
+ { asset: "core/scripts/lib/hq-cli-floor.sh", root: "live", summary: "Shared CLI version-floor helper sourced by generated forwarders" },
255
256
  { asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
256
257
  { asset: "core/scripts/lib/work-mesh-live-hook.sh", root: "live", summary: "Work Mesh Live hook helpers sourced by rebind" },
257
258
  { asset: "core/scripts/lib/work-mesh-live-rebind.sh", root: "live", summary: "Work Mesh Live session rebind library sourced by hooks and the native rollback command" },
@@ -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;
@@ -26,7 +26,7 @@ export declare const MAX_SENTRY_REPORT_METADATA_VALUE_BYTES = 256;
26
26
  * is owner-approved telemetry for the exact hook script and may include a
27
27
  * company directory; it is never a substitute for hook input or a transcript.
28
28
  */
29
- export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "slow_phase", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence", "hook_timeout_debug_context"];
29
+ export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "slow_phase", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "root_storage_class", "shell", "nproc", "hook_sequence", "hook_timeout_debug_context"];
30
30
  export declare const MAX_SENTRY_REPORT_METADATA_KEYS: number;
31
31
  export type HookSequenceEntry = {
32
32
  script: string;
@@ -47,7 +47,7 @@ type HookTimeoutDebugContext = {
47
47
  wait_point: string;
48
48
  waiting_child_basename: string;
49
49
  waiting_child_elapsed_ms: number;
50
- load_average?: number | "unavailable";
50
+ load_average?: number | "unavailable" | "unsupported_platform";
51
51
  spawn_ms?: number | "unavailable";
52
52
  process_count?: number | "unavailable";
53
53
  };
@@ -177,6 +177,7 @@ export const ALLOWED_SENTRY_REPORT_METADATA_KEYS = [
177
177
  "final_elapsed_ms",
178
178
  "bash_env_set",
179
179
  "cwd_kind",
180
+ "root_storage_class",
180
181
  "shell",
181
182
  "nproc",
182
183
  "hook_sequence",
@@ -290,6 +291,11 @@ function boundedDebugProbe(value) {
290
291
  return value;
291
292
  return boundedDebugMilliseconds(value) ?? undefined;
292
293
  }
294
+ function boundedDebugLoadAverage(value) {
295
+ if (value === "unsupported_platform")
296
+ return value;
297
+ return boundedDebugProbe(value);
298
+ }
293
299
  function boundedDebugPhaseTimings(value) {
294
300
  if (!Array.isArray(value) || value.length > MAX_HOOK_TIMEOUT_DEBUG_PHASES)
295
301
  return null;
@@ -324,7 +330,7 @@ function boundedHookTimeoutDebugContext(value) {
324
330
  phaseTimings === null) {
325
331
  return null;
326
332
  }
327
- const loadAverage = boundedDebugProbe(value.load_average);
333
+ const loadAverage = boundedDebugLoadAverage(value.load_average);
328
334
  const spawnMs = boundedDebugProbe(value.spawn_ms);
329
335
  const processCount = boundedDebugProbe(value.process_count);
330
336
  const hasLoadAverage = typeof loadAverage === "number";
@@ -439,7 +445,11 @@ function parseSentryReportEvent(stdin) {
439
445
  ? boundedHookSequence(rawMetadata[key])
440
446
  : key === "hook_timeout_debug_context"
441
447
  ? boundedHookTimeoutDebugContext(rawMetadata[key])
442
- : rawMetadata[key];
448
+ : key === "cwd_kind"
449
+ ? closedMetadataValue(rawMetadata[key], SAFE_CWD_KINDS, "unknown")
450
+ : key === "root_storage_class"
451
+ ? closedMetadataValue(rawMetadata[key], SAFE_ROOT_STORAGE_CLASSES, "unavailable")
452
+ : rawMetadata[key];
443
453
  }
444
454
  if (typeof metadata.slow_child === "string") {
445
455
  metadata.slow_child = safeSlowChild(metadata);
@@ -484,6 +494,21 @@ const SAFE_HOOK_EVENT = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;
484
494
  const SAFE_HOOK_SCRIPT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
485
495
  const SAFE_EXIT_CODE = /^(?:-?[0-9]+|running)$/;
486
496
  const SAFE_NPROC = /^[0-9]+$/;
497
+ const SAFE_CWD_KINDS = new Set([
498
+ "hq-root",
499
+ "repo",
500
+ "worktree",
501
+ "hq_subdir",
502
+ "root_unset",
503
+ "root_not_directory",
504
+ "cwd_unset",
505
+ "cwd_outside_root",
506
+ ]);
507
+ const SAFE_ROOT_STORAGE_CLASSES = new Set([
508
+ "system_drive",
509
+ "non_system_fixed_drive",
510
+ "network_or_mapped_drive",
511
+ ]);
487
512
  function safeTagValue(value, pattern = SAFE_TAG_TEXT) {
488
513
  return pattern.test(value) ? value : "unknown";
489
514
  }
@@ -546,14 +571,27 @@ function parsedLoadAverage(metadata) {
546
571
  return value;
547
572
  if (typeof value !== "string")
548
573
  return undefined;
549
- if (value === "unavailable")
550
- return "unavailable";
574
+ if (value === "unavailable" || value === "unsupported_platform")
575
+ return value;
551
576
  const match = /^\s*\{?\s*([0-9]+(?:\.[0-9]+)?)/.exec(value);
552
577
  if (!match)
553
578
  return undefined;
554
579
  const parsed = Number(match[1]);
555
580
  return Number.isFinite(parsed) ? parsed : undefined;
556
581
  }
582
+ function cwdKindTag(metadata) {
583
+ const value = metadata.cwd_kind;
584
+ return typeof value === "string" && SAFE_CWD_KINDS.has(value) ? value : "unknown";
585
+ }
586
+ function rootStorageClassTag(metadata) {
587
+ const value = metadata.root_storage_class;
588
+ return typeof value === "string" && SAFE_ROOT_STORAGE_CLASSES.has(value)
589
+ ? value
590
+ : "unavailable";
591
+ }
592
+ function closedMetadataValue(value, allowed, fallback) {
593
+ return typeof value === "string" && allowed.has(value) ? value : fallback;
594
+ }
557
595
  function operatingSystemName(metadata) {
558
596
  const platform = stringMetadata(metadata, "platform", "").toLowerCase();
559
597
  if (/windows|win32|mingw|msys|cygwin/.test(platform))
@@ -608,8 +646,8 @@ function hookTimeoutDebugContextTags(context) {
608
646
  timeout_debug_wait_point: context.wait_point,
609
647
  timeout_debug_waiting_child: context.waiting_child_basename,
610
648
  timeout_debug_waiting_child_elapsed_bucket: elapsedBucket(context.waiting_child_elapsed_ms),
611
- timeout_debug_load_bucket: context.load_average === "unavailable"
612
- ? "unavailable"
649
+ timeout_debug_load_bucket: typeof context.load_average === "string"
650
+ ? context.load_average
613
651
  : loadBucket(context.load_average),
614
652
  timeout_debug_spawn_ms_bucket: debugSpawnMsBucket(context.spawn_ms),
615
653
  timeout_debug_process_count_bucket: processCountBucket(context.process_count),
@@ -695,9 +733,10 @@ function hookTimeoutTags(event) {
695
733
  hq_core_version: safeMetadataTag(metadata, "hq_version"),
696
734
  shell: safeTagValue(shell),
697
735
  bash_env: bashEnvTag(metadata),
698
- cwd_kind: safeMetadataTag(metadata, "cwd_kind"),
736
+ cwd_kind: cwdKindTag(metadata),
699
737
  os_name: operatingSystemName(metadata),
700
- load_bucket: loadAverage === "unavailable" ? "unavailable" : loadBucket(loadAverage),
738
+ load_bucket: typeof loadAverage === "string" ? loadAverage : loadBucket(loadAverage),
739
+ root_storage_class: rootStorageClassTag(metadata),
701
740
  spawn_ms_bucket: spawnMsBucket(metadata),
702
741
  slow_child: safeSlowChild(metadata),
703
742
  slow_phase: safeSlowPhase(metadata.slow_phase),
@@ -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.16",
3
+ "version": "5.345.18",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {