@indigoai-us/hq-cli 5.345.15 → 5.345.17

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.17] — 2026-10-06
6
+ - Git Bash forwarders now keep slash commands intact and convert file path arguments that hq needs on Windows.
7
+
8
+ ## [5.345.16] — 2026-10-06
9
+ - 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.
10
+ - 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.
11
+
5
12
  ## [5.345.15] — 2026-10-06
6
13
  - Route live hook Sentry reports through the fast CLI path.
7
14
 
@@ -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" },
@@ -518,7 +518,7 @@ export function createLaneMessageHandlers(deps) {
518
518
  automaticAccountSelection = undefined;
519
519
  }
520
520
  }
521
- const profileRefreshAccount = refreshProfileSettings && resumeProfile !== undefined &&
521
+ const profileRefreshAccount = opts.account === undefined && refreshProfileSettings && resumeProfile !== undefined &&
522
522
  lane.account_source !== undefined && lane.account_source !== "cli" &&
523
523
  resumeProfile.profile.meta.provider === lane.provider
524
524
  ? resumeProfile.profile.meta.account ?? (resumeProfile.profile.meta.accounts !== undefined &&
@@ -665,9 +665,10 @@ export function createLaneMessageHandlers(deps) {
665
665
  // lane (the dropbox mechanism) is refused inside prepareAccountMove.
666
666
  move = await prepareAccountMove(ctx, lane, {
667
667
  account: opts.account,
668
+ accountSource: "cli",
668
669
  cliModel: opts.model?.trim() || undefined,
669
670
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
670
- ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "cli" } : {}),
671
+ ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
671
672
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
672
673
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
673
674
  pending: {
@@ -694,6 +695,7 @@ export function createLaneMessageHandlers(deps) {
694
695
  account: selection.account,
695
696
  cliModel: undefined,
696
697
  ...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
698
+ ...(automatic.preservedModel !== undefined ? { preservedModel: automatic.preservedModel } : {}),
697
699
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
698
700
  ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
699
701
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
@@ -746,8 +748,9 @@ export function createLaneMessageHandlers(deps) {
746
748
  if (resumeProfile !== undefined && !refreshProfileSettings && (opts.account === undefined || keepCurrentAccount)) {
747
749
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
748
750
  }
749
- if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
751
+ if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || keepCurrentAccount)) {
750
752
  lane = recordResumeSetup(ctx, lane, {
753
+ ...(keepCurrentAccount ? { accountSource: "cli" } : {}),
751
754
  ...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
752
755
  ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
753
756
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
@@ -279,7 +279,7 @@ export function createLaneResumeHandlers(deps) {
279
279
  ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
280
280
  })
281
281
  : undefined;
282
- const profileRefreshAccount = refreshProfileSettings && resumeProfile !== undefined &&
282
+ const profileRefreshAccount = opts.account === undefined && refreshProfileSettings && resumeProfile !== undefined &&
283
283
  lane.account_source !== undefined && lane.account_source !== "cli" &&
284
284
  resumeProfile.profile.meta.provider === lane.provider
285
285
  ? resumeProfile.profile.meta.account ?? (resumeProfile.profile.meta.accounts !== undefined &&
@@ -346,9 +346,10 @@ export function createLaneResumeHandlers(deps) {
346
346
  if (opts.account !== undefined && !keepCurrentAccount) {
347
347
  move = await prepareAccountMove(ctx, lane, {
348
348
  account: opts.account,
349
+ accountSource: "cli",
349
350
  cliModel: opts.model?.trim() || undefined,
350
351
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
351
- ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "cli" } : {}),
352
+ ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
352
353
  ...(profileRepin !== undefined ? { profileRepin } : {}),
353
354
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
354
355
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
@@ -432,6 +433,7 @@ export function createLaneResumeHandlers(deps) {
432
433
  ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
433
434
  ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
434
435
  ...(keepAlive !== undefined ? { keepAlive } : {}),
436
+ ...(move === null && keepCurrentAccount ? { accountSource: "cli" } : {}),
435
437
  ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
436
438
  ...(requestedMeshCompany !== undefined
437
439
  ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
@@ -6488,6 +6488,7 @@ function recordResumeSetup(ctx, lane, input) {
6488
6488
  input.effort === undefined &&
6489
6489
  input.keepAlive === undefined &&
6490
6490
  input.cliModel === undefined &&
6491
+ input.accountSource === undefined &&
6491
6492
  input.meshCompany === undefined &&
6492
6493
  input.clearWorkerProfileChangedQueue !== true &&
6493
6494
  input.refreshProfileSettings !== true &&
@@ -6516,7 +6517,11 @@ function recordResumeSetup(ctx, lane, input) {
6516
6517
  // recorded, so keep-alive may continue it again after this turn.
6517
6518
  if (next.keep_alive_blocked_reason === "terminal_state")
6518
6519
  next.keep_alive_blocked_reason = undefined;
6520
+ if (input.accountSource !== undefined)
6521
+ next.account_source = input.accountSource;
6519
6522
  applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
6523
+ if (input.accountSource !== undefined)
6524
+ next.account_source = input.accountSource;
6520
6525
  if (input.clearWorkerProfileChangedQueue === true) {
6521
6526
  next.queued_reason = undefined;
6522
6527
  next.queued_details = undefined;
@@ -6628,19 +6633,14 @@ async function prepareAccountMove(ctx, lane, input) {
6628
6633
  }
6629
6634
  // The model the moved run would use, resolved the way spawn resolves it:
6630
6635
  // the operator's --model, else the lane's own, else the target account's
6631
- // default, else the provider's. The quota and validation checks use this
6632
- // effective model so a per-model hold on the target's default is seen
6633
- // here, before anything is copied. A target that pins a different model
6634
- // refuses the lane's model unless the operator names one for the move.
6636
+ // default, else the provider's. The target account model is a default, not
6637
+ // a restriction; quota and provider validation use this effective model.
6635
6638
  const model = resolveLaneModel({
6636
6639
  cliModel: input.cliModel ?? pinnedModel,
6637
6640
  profileModel,
6638
6641
  accountModel: targetCfg.model,
6639
6642
  providerModel: providerCfg.model,
6640
6643
  });
6641
- if (input.cliModel === undefined && lane.model !== undefined && targetCfg.model !== undefined && model !== targetCfg.model) {
6642
- throw new LanesError("model_unsupported", `Account ${target} pins model ${targetCfg.model}; lane ${laneId} resolves to ${model ?? lane.model}. Pass --model ${targetCfg.model} to move it onto that model`, { provider: lane.provider, account: target, model: model ?? lane.model, account_model: targetCfg.model, lane_id: laneId });
6643
- }
6644
6644
  const block = quotaBlockFor(loadQuotaState(hqRoot), lane.provider, target, model, now);
6645
6645
  if (block.blocked) {
6646
6646
  throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
@@ -6785,26 +6785,26 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6785
6785
  : lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
6786
6786
  ? lane.model
6787
6787
  : undefined;
6788
- const recordedModelAccount = lane.account !== undefined &&
6789
- (lane.model_source === "cli" || lane.model_source === "preserved")
6790
- ? lane.account
6791
- : undefined;
6788
+ const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
6792
6789
  const quotaModel = resolveLaneModel({
6793
- cliModel: pinnedModel ?? (lane.model_source === "account" ? lane.model : undefined),
6794
- profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
6795
- accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
6790
+ cliModel: pinnedModel,
6791
+ profileModel,
6792
+ accountModel: currentConfig?.model,
6796
6793
  providerModel: config.providers.claude.model,
6797
6794
  });
6798
6795
  const recordedAccountQuotaHeld = lane.account !== undefined &&
6799
6796
  quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, quotaModel, ctx.now()).blocked;
6800
6797
  const accountNeedsRepair = recordedAccountUnavailable || recordedAccountQuotaHeld;
6801
6798
  const repairUnavailableAccount = options.repairUnavailableAccount === true && accountNeedsRepair;
6802
- const preservedModel = repairUnavailableAccount && lane.model_source === "account"
6799
+ const preservedModel = repairUnavailableAccount && recordedAccountUnavailable && lane.model_source === "account"
6803
6800
  ? lane.model ?? currentConfig?.model
6804
6801
  : undefined;
6802
+ // Repair keeps an account-derived model pinned through the move; plain
6803
+ // automatic rebalancing resolves each candidate's account default itself.
6804
+ const selectionModel = preservedModel ?? pinnedModel ?? profileModel;
6805
6805
  const model = resolveLaneModel({
6806
- cliModel: preservedModel ?? pinnedModel,
6807
- profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
6806
+ cliModel: pinnedModel,
6807
+ profileModel,
6808
6808
  accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
6809
6809
  providerModel: config.providers.claude.model,
6810
6810
  });
@@ -6824,9 +6824,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6824
6824
  allowedAccounts?.includes(lane.account) &&
6825
6825
  currentConfig?.enabled === true &&
6826
6826
  currentTargetAccount?.enabled === true &&
6827
- currentTargetAccount.logged_in === true &&
6828
- (model === undefined || currentConfig.model === undefined || currentConfig.model === model ||
6829
- lane.account === recordedModelAccount))
6827
+ currentTargetAccount.logged_in === true)
6830
6828
  return undefined;
6831
6829
  const selectionInput = {
6832
6830
  hosts: balanceHosts.snapshots,
@@ -6835,8 +6833,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6835
6833
  quotaState: loadQuotaState(hqRoot),
6836
6834
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, lane.company ?? undefined),
6837
6835
  now: ctx.now(),
6838
- ...(model !== undefined ? { model } : {}),
6839
- ...(recordedModelAccount !== undefined ? { recordedModelAccount } : {}),
6836
+ ...(selectionModel !== undefined ? { model: selectionModel } : {}),
6840
6837
  unavailableHosts: balanceHosts.unavailableHosts,
6841
6838
  };
6842
6839
  let selected;
@@ -6853,7 +6850,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6853
6850
  Array.isArray(err.details.account_reasons)
6854
6851
  ? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
6855
6852
  : [];
6856
- const unavailablePinReasons = new Set(["disabled", "home_missing", "not_logged_in", "model_mismatch", "quota_held"]);
6853
+ const unavailablePinReasons = new Set(["disabled", "home_missing", "not_logged_in", "quota_held"]);
6857
6854
  const everyProfileAccountUnavailable = profilePin !== undefined && profilePin.length > 0 &&
6858
6855
  profilePin.every((account) => accountReasons.some((reason) => reason.account === account && typeof reason.reason === "string" && unavailablePinReasons.has(reason.reason)));
6859
6856
  if (!everyProfileAccountUnavailable || lane.account === undefined ||
@@ -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),
@@ -76,8 +76,6 @@ export declare function selectClaudeAccount(input: {
76
76
  config: LanesConfig;
77
77
  allowedAccounts?: readonly string[];
78
78
  model?: string;
79
- /** Keep the recorded account eligible for its already-running CLI model. */
80
- recordedModelAccount?: string;
81
79
  quotaState?: QuotaState;
82
80
  /** Recent completed-run tiebreak; callers pass the default-off hq-flags result. */
83
81
  recentUsageEnabled?: boolean;
@@ -131,12 +131,7 @@ function buildLoads(input) {
131
131
  const quotaBlock = input.quotaState === undefined
132
132
  ? undefined
133
133
  : quotaBlockFor(input.quotaState, "claude", account, quotaModel, input.now);
134
- const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (account !== input.recordedModelAccount &&
135
- input.model !== undefined && configuredModel !== undefined && configuredModel !== input.model
136
- ? "model_mismatch"
137
- : quotaBlock?.blocked === true
138
- ? "quota_held"
139
- : undefined);
134
+ const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (quotaBlock?.blocked === true ? "quota_held" : undefined);
140
135
  if (ineligible !== undefined)
141
136
  skipped.push({ account, host: targetHost, reason: ineligible });
142
137
  const knownCounts = hosts.flatMap((host) => host.live_lanes === null ? [] : [host.live_lanes]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.15",
3
+ "version": "5.345.17",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {