@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 +7 -0
- package/assets/scaffold/core/scripts/generate-workers-registry.sh +130 -6
- package/assets/scaffold/core/scripts/hq-session.sh +1 -1
- package/assets/scaffold/core/scripts/lib/hq-cli-floor.sh +342 -0
- package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +122 -5
- package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +21 -1
- package/assets/scaffold/core/scripts/scan-packages.sh +130 -5
- package/dist/commands/core.js +1 -0
- package/dist/commands/lanes-message.js +6 -3
- package/dist/commands/lanes-resume.js +4 -2
- package/dist/commands/lanes.js +20 -23
- package/dist/lib/core-utils/sentry-report.d.ts +2 -2
- package/dist/lib/core-utils/sentry-report.js +47 -8
- package/dist/lib/lanes/claude-balance.d.ts +0 -2
- package/dist/lib/lanes/claude-balance.js +1 -6
- package/package.json +1 -1
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 —
|
|
2
|
+
# FORWARDER — the implementation of this script now lives in the hq CLI.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
# as the
|
|
6
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
5
|
-
# `hq core qmd-reindex-after-sync
|
|
6
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
2
|
+
# FORWARDER — the implementation of this script now lives in the hq CLI.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/commands/core.js
CHANGED
|
@@ -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
|
|
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 ||
|
|
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
|
|
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 }
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
|
6632
|
-
//
|
|
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
|
|
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
|
|
6794
|
-
profileModel
|
|
6795
|
-
accountModel:
|
|
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:
|
|
6807
|
-
profileModel
|
|
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
|
-
...(
|
|
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", "
|
|
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 =
|
|
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
|
-
:
|
|
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
|
|
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 === "
|
|
612
|
-
?
|
|
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:
|
|
736
|
+
cwd_kind: cwdKindTag(metadata),
|
|
699
737
|
os_name: operatingSystemName(metadata),
|
|
700
|
-
load_bucket: 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) ?? (
|
|
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]);
|