@indigoai-us/hq-cli 5.301.0 → 5.303.0
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 +26 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
- package/assets/scaffold/core/scripts/token-usage-report.sh +72 -20
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/bot.js +1 -0
- package/dist/commands/core-checkpoint.js +1 -0
- package/dist/commands/core-handoff-fast.js +1 -0
- package/dist/commands/core-handoff.js +2 -0
- package/dist/commands/deploy-comments.js +5 -2
- package/dist/commands/integrations-connect.d.ts +3 -0
- package/dist/commands/integrations-connect.js +21 -2
- package/dist/commands/lanes-message.js +33 -3
- package/dist/commands/lanes-questions.d.ts +4 -2
- package/dist/commands/lanes-questions.js +123 -2
- package/dist/commands/lanes-resume.js +25 -4
- package/dist/commands/lanes-shared.d.ts +13 -1
- package/dist/commands/lanes-watch.js +5 -1
- package/dist/commands/lanes.js +91 -9
- package/dist/commands/monitor.js +17 -2
- package/dist/commands/report.js +5 -2
- package/dist/commands/rescue.d.ts +4 -0
- package/dist/commands/rescue.js +27 -17
- package/dist/lib/agent-kit/fallback.d.ts +1 -0
- package/dist/lib/agent-kit/fallback.js +1 -0
- package/dist/lib/bot/cli-update.d.ts +0 -8
- package/dist/lib/bot/cli-update.js +5 -5
- package/dist/lib/bot/daemon.d.ts +5 -1
- package/dist/lib/bot/daemon.js +4 -4
- package/dist/lib/bot/local-jobs.d.ts +1 -21
- package/dist/lib/bot/local-jobs.js +12 -12
- package/dist/lib/core-utils/soft-timeout.js +1 -1
- package/dist/lib/daemon/config.d.ts +0 -1
- package/dist/lib/daemon/config.js +1 -1
- package/dist/lib/deploy-apps.d.ts +13 -0
- package/dist/lib/deploy-apps.js +24 -0
- package/dist/lib/index-render/projects.js +82 -5
- package/dist/lib/lanes/delivery.d.ts +7 -3
- package/dist/lib/lanes/delivery.js +18 -8
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +13 -3
- package/dist/lib/lanes/keep-alive.js +29 -5
- package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
- package/dist/lib/lanes/outpost-dispatch.js +12 -0
- package/dist/lib/lanes/reconcile.d.ts +1 -0
- package/dist/lib/lanes/reconcile.js +358 -38
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +20 -10
- package/dist/lib/lanes/store.js +24 -0
- package/dist/lib/lanes/story-ledger.d.ts +2 -0
- package/dist/lib/lanes/story-ledger.js +8 -0
- package/dist/lib/lanes/types.d.ts +26 -4
- package/dist/lib/lanes/types.js +33 -1
- package/dist/lib/lanes/watch.d.ts +1 -0
- package/dist/lib/lanes/watch.js +19 -6
- package/dist/lib/lanes/watcher.js +139 -9
- package/dist/lib/mesh/live/daemon/run.js +36 -2
- package/dist/lib/report-snapshot.d.ts +0 -2
- package/dist/lib/report-snapshot.js +2 -2
- package/dist/lib/search-index/background.d.ts +1 -0
- package/dist/lib/search-index/background.js +1 -0
- package/dist/utils/run-bundled-script.js +103 -1
- package/dist/utils/update-command-supervisor.cjs +1 -0
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.303.0] — 2026-10-01
|
|
6
|
+
- Fixed bundled shell scripts failing on Windows when Git Bash is installed but `bash` is not on `PATH`.
|
|
7
|
+
- The bundled token usage script now reads Claude sessions from `CLAUDE_CONFIG_DIR/projects` and the standard Claude project directories, including nested session folders (subagent transcripts are skipped), when no project directory is given.
|
|
8
|
+
|
|
9
|
+
## [5.302.0] — 2026-10-01
|
|
10
|
+
|
|
11
|
+
- Generate company project indexes while preserving hand-written content unless `--accept` is used.
|
|
12
|
+
|
|
13
|
+
- Follow all hq-deploy app-list pages when resolving apps for comments and report publishing.
|
|
14
|
+
- Let a senior message or resume an `awaiting_input` lane when its pending question is gone or cannot be reached from the senior's host. Pending questions remain answer-first, including across registered Outpost routes.
|
|
15
|
+
- A senior can resume or send a fallback message to an `awaiting_input` lane when its pending questions are addressed to another audience. Once the resume is accepted, those questions are cancelled on their owner; unreachable owners are reported while the lane continues.
|
|
16
|
+
- When a blocker escalation targets a session without a registered monitor inbox, record the reason and deliver it through the lane's existing inbox instead of retrying the failed escalation every watcher pass.
|
|
17
|
+
- `hq integrations disconnect` can now remove company integrations that were created outside the app catalog.
|
|
18
|
+
- Lead envelopes can set `next_check_at` to defer automatic keep-alive resumes
|
|
19
|
+
until a scheduled check, with pending work continuing to resume immediately.
|
|
20
|
+
- Reconcile records deduplicated keep-alive skip reasons in each lane's watcher event log.
|
|
21
|
+
- Add a company-scoped reconcile gate that closes local interrupted worker lanes after 24 hours when all carried project stories are done or dropped and no pending work remains.
|
|
22
|
+
- Skip refreshing failed-dispatch lane mirrors that never reached their Outpost owner.
|
|
23
|
+
- Keep lane keep-alive, retry-context, and monitor history readers moving past torn JSONL records.
|
|
24
|
+
- Refresh primary mirror heartbeats from fresh running Outpost reports before dead-host reconciliation.
|
|
25
|
+
- Report answerable awaiting-input questions as the `hq lanes show` delivery path, and retry blocker-freshness fallback messages after a failed append.
|
|
26
|
+
- Let `hq integrations connect` send an API key in a custom HTTP header.
|
|
27
|
+
- Bound `hq rescue` GitHub release and tag lookups to 30 seconds with a clear timeout error.
|
|
28
|
+
- hq-cli now logs live-board refresh failures and recovery while continuing to retry.
|
|
29
|
+
- On Windows, background jobs started by `/handoff` and search indexing no longer open console windows.
|
|
30
|
+
|
|
5
31
|
## [5.301.0] — 2026-10-01
|
|
6
32
|
- `hq meetings transcript` and `hq meetings notes` stop a stalled document download after two minutes and say so, instead of waiting forever.
|
|
7
33
|
|
|
@@ -11,12 +11,11 @@ set -euo pipefail
|
|
|
11
11
|
HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
12
12
|
cd "$HQ_ROOT"
|
|
13
13
|
|
|
14
|
+
COMPANIES_DIR="companies"
|
|
14
15
|
PROJECTS_DIR="personal/projects"
|
|
15
16
|
OUT="${PROJECTS_DIR}/INDEX.md"
|
|
16
17
|
DATE=$(date -u +%Y-%m-%d)
|
|
17
18
|
|
|
18
|
-
[[ -d "$PROJECTS_DIR" ]] || { echo "rebuild-projects-index: ${PROJECTS_DIR}/ missing, skipping" >&2; exit 0; }
|
|
19
|
-
|
|
20
19
|
sanitize_cell() {
|
|
21
20
|
printf '%s' "$1" | tr '\n' ' ' | sed -e 's/|/\\|/g' -e 's/ */ /g' -e 's/^ *//' -e 's/ *$//'
|
|
22
21
|
}
|
|
@@ -29,17 +28,23 @@ trunc() {
|
|
|
29
28
|
project_status() {
|
|
30
29
|
local project="$1"
|
|
31
30
|
local prd="$2"
|
|
31
|
+
local fallback="${3:-—}"
|
|
32
|
+
local expected_prd="${4:-}"
|
|
32
33
|
local state="workspace/orchestrator/${project}/state.json"
|
|
33
34
|
if [[ -f "$state" ]]; then
|
|
34
|
-
local
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
local state_prd
|
|
36
|
+
state_prd=$(jq -r '.prd_path // ""' "$state" 2>/dev/null || echo "")
|
|
37
|
+
if [[ -z "$expected_prd" || "$state_prd" == "$expected_prd" ]]; then
|
|
38
|
+
local raw
|
|
39
|
+
raw=$(jq -r '.status // "active"' "$state" 2>/dev/null || echo "active")
|
|
40
|
+
case "$raw" in
|
|
41
|
+
completed|complete) echo "complete" ;;
|
|
42
|
+
in_progress|active|started) echo "active" ;;
|
|
43
|
+
archived) echo "archived" ;;
|
|
44
|
+
*) echo "$raw" ;;
|
|
45
|
+
esac
|
|
46
|
+
return
|
|
47
|
+
fi
|
|
43
48
|
fi
|
|
44
49
|
if [[ -f "$prd" ]]; then
|
|
45
50
|
local total passes
|
|
@@ -50,9 +55,91 @@ project_status() {
|
|
|
50
55
|
return
|
|
51
56
|
fi
|
|
52
57
|
fi
|
|
53
|
-
echo "
|
|
58
|
+
echo "$fallback"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sha256_file() {
|
|
62
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
63
|
+
sha256sum "$1" | awk '{print $1}'
|
|
64
|
+
else
|
|
65
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
66
|
+
fi
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
stamp_generated() {
|
|
70
|
+
local input="$1" output="$2" normalized hash
|
|
71
|
+
normalized=$(mktemp)
|
|
72
|
+
sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$input" > "$normalized"
|
|
73
|
+
hash=$(sha256_file "$normalized")
|
|
74
|
+
rm -f "$normalized"
|
|
75
|
+
awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$input" > "$output"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
company_title() {
|
|
79
|
+
printf '%s\n' "$1" | awk -F'-' '{ for (i=1; i<=NF; i++) { $i = toupper(substr($i,1,1)) substr($i,2) } print }' OFS=' '
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
write_company_projects_index() {
|
|
83
|
+
local company="$1" company_root project_dir output tmp name prd description status
|
|
84
|
+
company_root="${COMPANIES_DIR}/${company}"
|
|
85
|
+
project_dir="${company_root}/projects"
|
|
86
|
+
output="${project_dir}/INDEX.md"
|
|
87
|
+
tmp=$(mktemp)
|
|
88
|
+
{
|
|
89
|
+
printf '# %s Projects\n\n' "$(company_title "$company")"
|
|
90
|
+
printf '> Auto-generated. Updated: %s\n\n' "$DATE"
|
|
91
|
+
echo '| Project | Status | Description |'
|
|
92
|
+
echo '|---------|--------|-------------|'
|
|
93
|
+
while IFS= read -r project; do
|
|
94
|
+
name=$(basename "$project")
|
|
95
|
+
[[ "$name" == _* ]] && continue
|
|
96
|
+
[[ "$name" == .* ]] && continue
|
|
97
|
+
prd="${project}/prd.json"
|
|
98
|
+
description=""
|
|
99
|
+
if [[ -f "$prd" ]]; then description=$(jq -r '.description // ""' "$prd" 2>/dev/null || echo ""); fi
|
|
100
|
+
status=$(project_status "$name" "$prd" "active" "$prd")
|
|
101
|
+
description=$(sanitize_cell "$description")
|
|
102
|
+
description=$(trunc "$description" 110)
|
|
103
|
+
[[ -z "$description" ]] && description="—"
|
|
104
|
+
printf '| `%s` | %s | %s |\n' "$name" "$status" "$description"
|
|
105
|
+
done < <(find "$project_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
|
|
106
|
+
} > "$tmp"
|
|
107
|
+
local existing=0 stored actual normalized
|
|
108
|
+
[[ -e "$output" ]] && existing=1
|
|
109
|
+
if [[ "$existing" == 1 ]]; then
|
|
110
|
+
if grep -q '<!-- hq:keep -->' "$output" || ! grep -q '^> Auto-generated. Updated: ' "$output"; then
|
|
111
|
+
echo "rebuild-projects-index: refused to overwrite ${output} (hand-written or kept)" >&2
|
|
112
|
+
rm -f "$tmp"
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
stored=$(sed -n 's/^<!-- hq-generated-sha256: \([a-f0-9]*\) -->$/\1/p' "$output" | head -n 1)
|
|
116
|
+
if [[ -n "$stored" ]]; then
|
|
117
|
+
normalized=$(mktemp)
|
|
118
|
+
sed -e '/^<!-- hq-generated-sha256: [a-f0-9]* -->$/d' -e 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$output" > "$normalized"
|
|
119
|
+
actual=$(sha256_file "$normalized")
|
|
120
|
+
rm -f "$normalized"
|
|
121
|
+
if [[ "$stored" != "$actual" ]]; then
|
|
122
|
+
echo "rebuild-projects-index: refused to overwrite ${output} (generated content was edited)" >&2
|
|
123
|
+
rm -f "$tmp"
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
stamp_generated "$tmp" "$output"
|
|
129
|
+
rm -f "$tmp"
|
|
130
|
+
echo "rebuild-projects-index: wrote ${output}" >&2
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
while IFS= read -r dir; do
|
|
134
|
+
co=$(basename "$dir")
|
|
135
|
+
[[ "$co" == _* ]] && continue
|
|
136
|
+
[[ "$co" == .* ]] && continue
|
|
137
|
+
project_dir="${COMPANIES_DIR}/${co}/projects"
|
|
138
|
+
[[ -d "$project_dir" ]] && write_company_projects_index "$co"
|
|
139
|
+
done < <(find "$COMPANIES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
|
|
140
|
+
|
|
141
|
+
[[ -d "$PROJECTS_DIR" ]] || { echo "rebuild-projects-index: ${PROJECTS_DIR}/ missing, skipping" >&2; exit 0; }
|
|
142
|
+
|
|
56
143
|
{
|
|
57
144
|
echo "# Projects"
|
|
58
145
|
echo ""
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
set -euo pipefail
|
|
14
14
|
|
|
15
|
-
PROJECT_DIR="
|
|
15
|
+
PROJECT_DIR=""
|
|
16
|
+
PROJECT_DIR_SET=0
|
|
16
17
|
LAST_DAYS=7
|
|
17
18
|
SINCE=""
|
|
18
19
|
JSON_MODE=0
|
|
@@ -31,6 +32,7 @@ while [ $# -gt 0 ]; do
|
|
|
31
32
|
exit 1
|
|
32
33
|
fi
|
|
33
34
|
PROJECT_DIR="$2"
|
|
35
|
+
PROJECT_DIR_SET=1
|
|
34
36
|
shift 2
|
|
35
37
|
;;
|
|
36
38
|
--compare-windows) shift ;; # flag, presence triggers compare mode via --before/--after
|
|
@@ -47,14 +49,47 @@ done
|
|
|
47
49
|
|
|
48
50
|
command -v node >/dev/null 2>&1 || { echo "token-usage-report: node is required" >&2; exit 1; }
|
|
49
51
|
|
|
52
|
+
# Resolve roots in the same precedence order as the native command. Explicit
|
|
53
|
+
# project directories and environment overrides select one exact activity root;
|
|
54
|
+
# otherwise include standard, desktop-saved, and discovered Claude roots.
|
|
55
|
+
PROJECT_DIRS=("")
|
|
56
|
+
add_project_dir() {
|
|
57
|
+
local candidate="$1" existing
|
|
58
|
+
[ -d "$candidate" ] || return 0
|
|
59
|
+
for existing in "${PROJECT_DIRS[@]}"; do [ "$existing" = "$candidate" ] && return 0; done
|
|
60
|
+
PROJECT_DIRS+=("$candidate")
|
|
61
|
+
}
|
|
62
|
+
trim_env_path() {
|
|
63
|
+
local value="$1"
|
|
64
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
65
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
66
|
+
printf '%s' "$value"
|
|
67
|
+
}
|
|
68
|
+
if [ "$PROJECT_DIR_SET" -eq 1 ]; then
|
|
69
|
+
if [ ! -d "$PROJECT_DIR" ]; then echo "Project dir not found: $PROJECT_DIR" >&2; exit 1; fi
|
|
70
|
+
add_project_dir "$PROJECT_DIR"
|
|
71
|
+
elif [ -n "$(trim_env_path "${CLAUDE_PROJECTS_DIR:-}")" ]; then
|
|
72
|
+
add_project_dir "$(trim_env_path "${CLAUDE_PROJECTS_DIR:-}")"
|
|
73
|
+
elif [ -n "$(trim_env_path "${CLAUDE_CONFIG_DIR:-}")" ]; then
|
|
74
|
+
add_project_dir "$(trim_env_path "${CLAUDE_CONFIG_DIR:-}")/projects"
|
|
75
|
+
else
|
|
76
|
+
add_project_dir "$HOME/.claude/projects"
|
|
77
|
+
SAVED_PROJECT_DIR="$(node -e 'try { const value = require(process.argv[1]).claudeProjectsDir; if (typeof value === "string" && value.trim()) process.stdout.write(value.trim()); } catch {}' "$HOME/.hq/menubar.json")"
|
|
78
|
+
[ -z "$SAVED_PROJECT_DIR" ] || add_project_dir "$SAVED_PROJECT_DIR"
|
|
79
|
+
for candidate in "$HOME"/.claude-*; do
|
|
80
|
+
[ -d "$candidate/projects" ] && add_project_dir "$candidate/projects"
|
|
81
|
+
done
|
|
82
|
+
fi
|
|
83
|
+
if [ "${#PROJECT_DIRS[@]}" -le 1 ]; then echo "Claude activity folders not found" >&2; exit 1; fi
|
|
84
|
+
|
|
50
85
|
# --- Compare-windows mode (US-011) ---
|
|
51
86
|
# Format: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD
|
|
52
87
|
if [ -n "$COMPARE_BEFORE" ] && [ -n "$COMPARE_AFTER" ]; then
|
|
53
|
-
node - "$
|
|
88
|
+
node - "$COMPARE_BEFORE" "$COMPARE_AFTER" "$MIN_HOURS" "$JSON_MODE" "${PROJECT_DIRS[@]:1}" <<'JS'
|
|
54
89
|
const fs = require("fs");
|
|
55
90
|
const path = require("path");
|
|
56
91
|
|
|
57
|
-
const [
|
|
92
|
+
const [before, after, minHoursS, jsonModeS, ...projectDirs] = process.argv.slice(2);
|
|
58
93
|
const minHours = parseFloat(minHoursS);
|
|
59
94
|
const jsonMode = parseInt(jsonModeS, 10) === 1;
|
|
60
95
|
|
|
@@ -63,13 +98,24 @@ const [bStart, bEnd] = parseRange(before);
|
|
|
63
98
|
const [aStart, aEnd] = parseRange(after);
|
|
64
99
|
|
|
65
100
|
const fmt = (n) => n.toLocaleString("en-US");
|
|
66
|
-
const listJsonl = () => {
|
|
67
|
-
|
|
101
|
+
const listJsonl = (projectDir) => {
|
|
102
|
+
const found = [];
|
|
103
|
+
const visit = (current, relative) => {
|
|
104
|
+
let entries;
|
|
105
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch (e) { return; }
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const nextRelative = path.join(relative, entry.name);
|
|
108
|
+
if (entry.isDirectory() && entry.name !== "subagents") visit(path.join(current, entry.name), nextRelative);
|
|
109
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) found.push(nextRelative);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
visit(projectDir, "");
|
|
113
|
+
return found.sort();
|
|
68
114
|
};
|
|
69
115
|
|
|
70
116
|
function collect(windowStart, windowEnd) {
|
|
71
117
|
const rows = [];
|
|
72
|
-
for (const name of listJsonl()) {
|
|
118
|
+
for (const proj of projectDirs) for (const name of listJsonl(proj)) {
|
|
73
119
|
const f = path.join(proj, name);
|
|
74
120
|
let cr = 0, firstTs = null, lastTs = null;
|
|
75
121
|
const sid = name.replace(/\.jsonl$/, "");
|
|
@@ -130,16 +176,11 @@ JS
|
|
|
130
176
|
exit 0
|
|
131
177
|
fi
|
|
132
178
|
|
|
133
|
-
|
|
134
|
-
echo "Project dir not found: $PROJECT_DIR" >&2
|
|
135
|
-
exit 1
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
node - "$PROJECT_DIR" "$LAST_DAYS" "$SINCE" "$JSON_MODE" <<'JS'
|
|
179
|
+
node - "$LAST_DAYS" "$SINCE" "$JSON_MODE" "${PROJECT_DIRS[@]:1}" <<'JS'
|
|
139
180
|
const fs = require("fs");
|
|
140
181
|
const path = require("path");
|
|
141
182
|
|
|
142
|
-
const [
|
|
183
|
+
const [lastDaysS, sinceS, jsonModeS, ...projectDirs] = process.argv.slice(2);
|
|
143
184
|
const lastDays = parseInt(lastDaysS, 10);
|
|
144
185
|
const jsonMode = parseInt(jsonModeS, 10) === 1;
|
|
145
186
|
|
|
@@ -156,11 +197,20 @@ const fmt = (n) => n.toLocaleString("en-US");
|
|
|
156
197
|
const dayTotals = {};
|
|
157
198
|
const sessionTotals = {};
|
|
158
199
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
200
|
+
// Parent sessions only; listJsonl skips each session's subagents/ tree.
|
|
201
|
+
for (const projectDir of projectDirs) {
|
|
202
|
+
const listJsonl = (current, relative = "") => {
|
|
203
|
+
let entries;
|
|
204
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch (e) { return []; }
|
|
205
|
+
return entries.flatMap((entry) => {
|
|
206
|
+
const nextRelative = path.join(relative, entry.name);
|
|
207
|
+
if (entry.isDirectory() && entry.name !== "subagents") return listJsonl(path.join(current, entry.name), nextRelative);
|
|
208
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) return [nextRelative];
|
|
209
|
+
return [];
|
|
210
|
+
}).sort();
|
|
211
|
+
};
|
|
212
|
+
const names = listJsonl(projectDir);
|
|
213
|
+
for (const name of names) {
|
|
164
214
|
const f = path.join(projectDir, name);
|
|
165
215
|
let mtime;
|
|
166
216
|
try { mtime = fs.statSync(f).mtime; } catch (e) { continue; }
|
|
@@ -170,6 +220,7 @@ for (const name of names) {
|
|
|
170
220
|
let firstTs = null, lastTs = null, firstUser = "";
|
|
171
221
|
let subCount = 0;
|
|
172
222
|
const sid = name.replace(/\.jsonl$/, "");
|
|
223
|
+
const sessionId = path.basename(path.dirname(projectDir)) + "/" + sid;
|
|
173
224
|
let content;
|
|
174
225
|
try { content = fs.readFileSync(f, "utf8"); } catch (e) { continue; }
|
|
175
226
|
for (const line of content.split(/\r?\n/)) {
|
|
@@ -196,15 +247,16 @@ for (const name of names) {
|
|
|
196
247
|
if (!day) day = mt;
|
|
197
248
|
if (day < cutoff) continue;
|
|
198
249
|
if (!dayTotals[day]) dayTotals[day] = { sessions: new Set(), inp: 0, out: 0, cc: 0, cr: 0 };
|
|
199
|
-
dayTotals[day].sessions.add(
|
|
250
|
+
dayTotals[day].sessions.add(sessionId);
|
|
200
251
|
dayTotals[day].inp += inp;
|
|
201
252
|
dayTotals[day].out += out;
|
|
202
253
|
dayTotals[day].cc += cc;
|
|
203
254
|
dayTotals[day].cr += cr;
|
|
204
|
-
sessionTotals[
|
|
255
|
+
sessionTotals[sessionId] = {
|
|
205
256
|
day: day, inp: inp, out: out, cc: cc, cr: cr,
|
|
206
257
|
eff: Math.trunc(eff), subagents: subCount, first_user: firstUser.split("\n").join(" "),
|
|
207
258
|
};
|
|
259
|
+
}
|
|
208
260
|
}
|
|
209
261
|
|
|
210
262
|
const sortedDays = Object.keys(dayTotals).sort();
|
|
@@ -3122,6 +3122,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3122
3122
|
}, {
|
|
3123
3123
|
readonly flags: "--token-stdin";
|
|
3124
3124
|
readonly description: "Read the API key from stdin";
|
|
3125
|
+
}, {
|
|
3126
|
+
readonly flags: "--auth-header <name>";
|
|
3127
|
+
readonly description: "Send the API key in this HTTP header (implies --auth key)";
|
|
3125
3128
|
}, {
|
|
3126
3129
|
readonly flags: "--auth <mode>";
|
|
3127
3130
|
readonly description: "Force the auth mode: none, key, or oauth (default: detect)";
|
|
@@ -8608,7 +8611,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8608
8611
|
readonly subcommands: readonly [];
|
|
8609
8612
|
}, {
|
|
8610
8613
|
readonly name: "resume";
|
|
8611
|
-
readonly description: "Resume an interrupted, done or blocked lane
|
|
8614
|
+
readonly description: "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option";
|
|
8612
8615
|
readonly aliases: readonly [];
|
|
8613
8616
|
readonly hidden: false;
|
|
8614
8617
|
readonly usage: "[options] <lane-id>";
|
|
@@ -4013,6 +4013,10 @@ export const COMMAND_CATALOG = [
|
|
|
4013
4013
|
"flags": "--token-stdin",
|
|
4014
4014
|
"description": "Read the API key from stdin"
|
|
4015
4015
|
},
|
|
4016
|
+
{
|
|
4017
|
+
"flags": "--auth-header <name>",
|
|
4018
|
+
"description": "Send the API key in this HTTP header (implies --auth key)"
|
|
4019
|
+
},
|
|
4016
4020
|
{
|
|
4017
4021
|
"flags": "--auth <mode>",
|
|
4018
4022
|
"description": "Force the auth mode: none, key, or oauth (default: detect)"
|
|
@@ -11100,7 +11104,7 @@ export const COMMAND_CATALOG = [
|
|
|
11100
11104
|
},
|
|
11101
11105
|
{
|
|
11102
11106
|
"name": "resume",
|
|
11103
|
-
"description": "Resume an interrupted, done or blocked lane
|
|
11107
|
+
"description": "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option",
|
|
11104
11108
|
"aliases": [],
|
|
11105
11109
|
"hidden": false,
|
|
11106
11110
|
"usage": "[options] <lane-id>",
|
package/dist/commands/bot.js
CHANGED
|
@@ -228,6 +228,7 @@ export function spawnDetachedRun(name, dir) {
|
|
|
228
228
|
const fd = fs.openSync(botLogPath(dir), "a");
|
|
229
229
|
const child = spawn(process.execPath, [entry, "bot", "run", name], {
|
|
230
230
|
detached: true,
|
|
231
|
+
windowsHide: true,
|
|
231
232
|
stdio: ["ignore", fd, fd],
|
|
232
233
|
env: { ...process.env, HQ_NO_UPDATE_CHECK: "1" },
|
|
233
234
|
});
|
|
@@ -1049,6 +1049,7 @@ function startSibling(liveRoot, input, threadPath, backend) {
|
|
|
1049
1049
|
try {
|
|
1050
1050
|
child = spawn(backend, args, {
|
|
1051
1051
|
detached: true,
|
|
1052
|
+
windowsHide: true,
|
|
1052
1053
|
cwd: liveRoot,
|
|
1053
1054
|
stdio: ["ignore", logFd, logFd],
|
|
1054
1055
|
env: siblingEnv(process.env, backend),
|
|
@@ -802,6 +802,7 @@ function spawnBashDetached(script, args, cwd, root, logFile) {
|
|
|
802
802
|
cwd,
|
|
803
803
|
env: { ...process.env, HQ_ROOT: root },
|
|
804
804
|
detached: true,
|
|
805
|
+
windowsHide: true,
|
|
805
806
|
stdio: ["ignore", fd, fd],
|
|
806
807
|
});
|
|
807
808
|
child.once("error", (error) => appendLog(logFile, `${error.message}\n`));
|
|
@@ -841,6 +842,7 @@ async function launchHandoffPost(args, root) {
|
|
|
841
842
|
cwd: root,
|
|
842
843
|
env: { ...process.env, HQ_NATIVE_HANDOFF_POST_CHILD: "1" },
|
|
843
844
|
detached: true,
|
|
845
|
+
windowsHide: true,
|
|
844
846
|
stdio: "ignore",
|
|
845
847
|
});
|
|
846
848
|
child.once("error", (error) => resolve({ code: 127, stdout: "", stderr: `${error.message}\n` }));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deployApiRequest } from "../lib/deploy-api.js";
|
|
2
|
+
import { listDeployApps } from "../lib/deploy-apps.js";
|
|
2
3
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken } from "../utils/cognito-session.js";
|
|
3
4
|
import { readActiveCompanySlug } from "./skill.js";
|
|
4
5
|
class DeployCommentsError extends Error {
|
|
@@ -50,8 +51,10 @@ async function deployJson(token, route, orgHeaders) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
async function resolveAppId(token, appNameOrId, orgHeaders) {
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const apps = await listDeployApps((cursor) => {
|
|
55
|
+
const route = cursor ? `/api/apps?cursor=${encodeURIComponent(cursor)}` : "/api/apps";
|
|
56
|
+
return deployJson(token, route, orgHeaders);
|
|
57
|
+
});
|
|
55
58
|
const byId = apps.find((app) => app && typeof app.id === "string" && app.id === appNameOrId);
|
|
56
59
|
const matches = byId
|
|
57
60
|
? [byId]
|
|
@@ -30,6 +30,7 @@ interface ConnectOptions {
|
|
|
30
30
|
displayName?: string;
|
|
31
31
|
token?: string;
|
|
32
32
|
tokenStdin?: boolean;
|
|
33
|
+
authHeader?: string;
|
|
33
34
|
auth?: string;
|
|
34
35
|
browser?: boolean;
|
|
35
36
|
timeout?: string;
|
|
@@ -48,6 +49,8 @@ interface ConnectOptions {
|
|
|
48
49
|
* than fall through to a default.
|
|
49
50
|
*/
|
|
50
51
|
export declare function assertAuthMode(value: string | undefined): void;
|
|
52
|
+
/** RFC 7230 token characters accepted for an HTTP field name. */
|
|
53
|
+
export declare function assertAuthHeader(value: string | undefined): void;
|
|
51
54
|
/** Single-quote a value for a copy-pasteable shell command. */
|
|
52
55
|
export declare function shellQuote(value: string): string;
|
|
53
56
|
/** Everything the connect verbs need to name one app to hq-pro. */
|
|
@@ -185,6 +185,12 @@ export function assertAuthMode(value) {
|
|
|
185
185
|
throw new IntegrationsCliError("--auth must be none, key, or oauth.", { expected: true });
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
/** RFC 7230 token characters accepted for an HTTP field name. */
|
|
189
|
+
export function assertAuthHeader(value) {
|
|
190
|
+
if (value !== undefined && !/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(value)) {
|
|
191
|
+
throw new IntegrationsCliError("--auth-header must be a non-empty HTTP header name using RFC 7230 token characters.", { expected: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
188
194
|
/** Single-quote a value for a copy-pasteable shell command. */
|
|
189
195
|
export function shellQuote(value) {
|
|
190
196
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -904,7 +910,9 @@ function reportInstall(result, opts, expectedRevivedConnectionId) {
|
|
|
904
910
|
*/
|
|
905
911
|
async function connectApp(token, companyUid, app, opts, expectedRevivedConnectionId) {
|
|
906
912
|
const target = await resolveTarget(token, companyUid, app, opts, expectedRevivedConnectionId !== undefined);
|
|
907
|
-
const authMode = opts.
|
|
913
|
+
const authMode = opts.authHeader
|
|
914
|
+
? "key"
|
|
915
|
+
: opts.auth ?? target.authClass;
|
|
908
916
|
assertUserClientApplicable(opts, target, authMode);
|
|
909
917
|
assertScopesApplicable(opts, target, authMode);
|
|
910
918
|
if (authMode === "oauth") {
|
|
@@ -922,7 +930,13 @@ async function connectApp(token, companyUid, app, opts, expectedRevivedConnectio
|
|
|
922
930
|
const result = await installIntegration(token, companyUid, {
|
|
923
931
|
...target.ref,
|
|
924
932
|
...(bearerToken
|
|
925
|
-
? {
|
|
933
|
+
? {
|
|
934
|
+
authMode: "bearer",
|
|
935
|
+
bearerToken,
|
|
936
|
+
...(opts.authHeader
|
|
937
|
+
? { authScheme: { placement: "header", header: opts.authHeader } }
|
|
938
|
+
: {}),
|
|
939
|
+
}
|
|
926
940
|
: authMode === "none"
|
|
927
941
|
? { authMode: "none" }
|
|
928
942
|
: {}),
|
|
@@ -1173,6 +1187,7 @@ export function registerConnectCommands(integrations) {
|
|
|
1173
1187
|
.option("--display-name <name>", "Display name for a custom --mcp-url")
|
|
1174
1188
|
.option("--token <key>", "API key (prefer --token-stdin: --token lands in shell history)")
|
|
1175
1189
|
.option("--token-stdin", "Read the API key from stdin")
|
|
1190
|
+
.option("--auth-header <name>", "Send the API key in this HTTP header (implies --auth key)")
|
|
1176
1191
|
.option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
|
|
1177
1192
|
.option("--client-id <id>", "Client id of an OAuth app you registered with the provider yourself")
|
|
1178
1193
|
.option("--client-secret-stdin", "Read that app's client secret from stdin")
|
|
@@ -1183,6 +1198,10 @@ export function registerConnectCommands(integrations) {
|
|
|
1183
1198
|
.option("--json", "Machine-readable output")
|
|
1184
1199
|
.action(async (app, opts) => {
|
|
1185
1200
|
assertAuthMode(opts.auth);
|
|
1201
|
+
assertAuthHeader(opts.authHeader);
|
|
1202
|
+
if (opts.authHeader && opts.auth !== undefined && opts.auth !== "key") {
|
|
1203
|
+
throw new IntegrationsCliError("--auth-header can only be used with --auth key.", { expected: true });
|
|
1204
|
+
}
|
|
1186
1205
|
const token = await ensureCognitoIdToken();
|
|
1187
1206
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
1188
1207
|
if (await refuseIfAlreadyConnected(token, companyUid, app, opts))
|
|
@@ -247,6 +247,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
247
247
|
}
|
|
248
248
|
let lane = resolution.lane;
|
|
249
249
|
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
250
|
+
const fallbackAt = lane.state === "awaiting_input" ? ctx.now().toISOString() : undefined;
|
|
250
251
|
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
251
252
|
const supersededMessageCallerId = callerLaneId === undefined || resolution.remoteOnly
|
|
252
253
|
? undefined
|
|
@@ -262,9 +263,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
262
263
|
...opts,
|
|
263
264
|
...(supersededMessageCallerId === undefined ? {} : { supersededMessageCallerId }),
|
|
264
265
|
...(supersededMessageSessionId === undefined ? {} : { supersededMessageSessionId }),
|
|
265
|
-
},
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
}, {
|
|
267
|
+
...(resolution.remoteOnly
|
|
268
|
+
? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
|
|
269
|
+
: {}),
|
|
270
|
+
...(fallbackAt === undefined ? {} : {
|
|
271
|
+
afterFallbackResume: () => deps.cancelFallbackQuestionsForLane(ctx, lane, "message", fallbackAt),
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
268
274
|
return;
|
|
269
275
|
}
|
|
270
276
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
@@ -350,6 +356,19 @@ export function createLaneMessageHandlers(deps) {
|
|
|
350
356
|
if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
|
|
351
357
|
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
352
358
|
}
|
|
359
|
+
const pendingAnswerableQuestion = lane.state === "awaiting_input"
|
|
360
|
+
? await deps.hasPendingQuestionForLane(ctx, lane)
|
|
361
|
+
: false;
|
|
362
|
+
if (pendingAnswerableQuestion) {
|
|
363
|
+
const message = deliveryRefusalReason(lane);
|
|
364
|
+
throw new LanesError("lane_not_accepting_messages", message, {
|
|
365
|
+
lane_id: laneId,
|
|
366
|
+
state: lane.state,
|
|
367
|
+
provider: lane.provider,
|
|
368
|
+
message,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const awaitingInputFallback = lane.state === "awaiting_input" && !pendingAnswerableQuestion;
|
|
353
372
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
354
373
|
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
355
374
|
const mechanism = laneDeliveryMechanism(lane);
|
|
@@ -536,9 +555,13 @@ export function createLaneMessageHandlers(deps) {
|
|
|
536
555
|
...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
537
556
|
});
|
|
538
557
|
}
|
|
558
|
+
let questionCancellation = null;
|
|
539
559
|
if (move?.queued) {
|
|
540
560
|
// Parked at the target's cap: reconcile resumes the session there.
|
|
541
561
|
markAccepted();
|
|
562
|
+
questionCancellation = awaitingInputFallback
|
|
563
|
+
? await deps.cancelFallbackQuestionsForLane(ctx, lane, "message", at)
|
|
564
|
+
: null;
|
|
542
565
|
}
|
|
543
566
|
else {
|
|
544
567
|
const dispatched = await dispatchLaneDelivery(lane, {
|
|
@@ -637,6 +660,9 @@ export function createLaneMessageHandlers(deps) {
|
|
|
637
660
|
if (dispatched.mechanism === "resume")
|
|
638
661
|
resumed = dispatched.value;
|
|
639
662
|
markAccepted();
|
|
663
|
+
questionCancellation = awaitingInputFallback
|
|
664
|
+
? await deps.cancelFallbackQuestionsForLane(ctx, lane, "message", at)
|
|
665
|
+
: null;
|
|
640
666
|
}
|
|
641
667
|
// Every accepted message moves the compaction reference forward.
|
|
642
668
|
const next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
@@ -679,6 +705,10 @@ export function createLaneMessageHandlers(deps) {
|
|
|
679
705
|
inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
|
|
680
706
|
drain_hook: drainHook,
|
|
681
707
|
warning,
|
|
708
|
+
...(questionCancellation === null ? {} : {
|
|
709
|
+
cancelled_questions: questionCancellation.cancelled_question_ids,
|
|
710
|
+
question_cancellation_failures: questionCancellation.failures,
|
|
711
|
+
}),
|
|
682
712
|
resumed_session_id: resumed?.last_resume?.session_id ?? null,
|
|
683
713
|
compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
684
714
|
? resumed.last_compaction
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { type QuestionBody, type QuestionRecord } from "../lib/lanes/index.js";
|
|
2
|
-
import type { Ctx, LaneQuestionsHandlerDeps } from "./lanes-shared.js";
|
|
1
|
+
import { type QuestionBody, type LaneRecord, type QuestionRecord } from "../lib/lanes/index.js";
|
|
2
|
+
import type { Ctx, FallbackQuestionCancellationResult, LaneQuestionsHandlerDeps } from "./lanes-shared.js";
|
|
3
3
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
4
4
|
export declare function createLaneQuestionsHandlers(deps: LaneQuestionsHandlerDeps): {
|
|
5
|
+
hasPendingQuestionForLane: (ctx: Ctx, lane: LaneRecord) => Promise<boolean>;
|
|
6
|
+
cancelFallbackQuestionsForLane: (ctx: Ctx, lane: LaneRecord, trigger: "message" | "resume", at: string) => Promise<FallbackQuestionCancellationResult>;
|
|
5
7
|
runQuestionsList: (ctx: Ctx, opts: {
|
|
6
8
|
allHosts?: boolean;
|
|
7
9
|
for?: string;
|