@indigoai-us/hq-cli 5.286.0 → 5.288.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 +57 -0
- package/assets/scaffold/core/scripts/clean-worktree-reconcile-handoff.sh +171 -0
- package/assets/scaffold/core/scripts/resume-thread-lock.sh +285 -0
- package/assets/scaffold/core/scripts/verify-story-deliverables.sh +148 -0
- package/assets/scaffold/core/scripts/worktree-gc.sh +362 -0
- package/dist/command-catalog.generated.d.ts +55 -0
- package/dist/command-catalog.generated.js +68 -0
- package/dist/commands/agent-enroll.js +10 -2
- package/dist/commands/agents.d.ts +1 -0
- package/dist/commands/agents.js +12 -4
- package/dist/commands/cloud-provision.js +99 -78
- package/dist/commands/core-native-handoff-flag.d.ts +4 -0
- package/dist/commands/core-native-handoff-flag.js +7 -0
- package/dist/commands/core-ontology.d.ts +4 -1
- package/dist/commands/core-ontology.js +26 -0
- package/dist/commands/core.js +45 -0
- package/dist/commands/lanes-resume.js +41 -2
- package/dist/commands/lanes-shared.d.ts +2 -0
- package/dist/commands/lanes.js +48 -29
- package/dist/commands/team-sync.js +71 -10
- package/dist/fast-lanes-list-route.js +2 -0
- package/dist/fast-lanes-list.js +1 -29
- package/dist/lanes-drain-hook.d.ts +12 -0
- package/dist/lanes-drain-hook.js +59 -55
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +10 -1
- package/dist/lib/checkpoint-ontology/merge-copies.js +17 -2
- package/dist/lib/checkpoint-ontology/plan.d.ts +0 -88
- package/dist/lib/checkpoint-ontology/plan.js +80 -107
- package/dist/lib/core-utils/sentry-report.js +5 -32
- package/dist/lib/core-utils/worktree-resume.d.ts +18 -0
- package/dist/lib/core-utils/worktree-resume.js +1182 -0
- package/dist/lib/lanes/dropbox.d.ts +1 -2
- package/dist/lib/lanes/dropbox.js +10 -111
- package/dist/lib/lanes/message-replay.d.ts +8 -0
- package/dist/lib/lanes/message-replay.js +119 -0
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +21 -0
- package/dist/lib/lanes/outpost-snapshot-refresh.js +329 -0
- package/dist/lib/plan-limit-nag.js +11 -0
- package/dist/main.js +17 -4
- package/dist/utils/cli-telemetry.d.ts +21 -0
- package/dist/utils/cli-telemetry.js +101 -1
- package/dist/utils/cognito-refresh-sentry.d.ts +3 -0
- package/dist/utils/cognito-refresh-sentry.js +49 -0
- package/dist/utils/cognito-session.js +55 -0
- package/package.json +1 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# worktree-gc.sh — safe garbage collector for stale HQ git worktrees.
|
|
4
|
+
#
|
|
5
|
+
# HQ worktrees accumulate forever. Nothing GCs them, and a busy machine can
|
|
6
|
+
# reach hundreds of worktrees / hundreds of GB. This script enumerates the
|
|
7
|
+
# linked worktrees HQ creates under:
|
|
8
|
+
# <hqRoot>/workspace/worktrees/ (flat <name>/ and nested <repo>/<name>/)
|
|
9
|
+
# <hqRoot>/.claude/worktrees/ (agent-* local-only worktrees)
|
|
10
|
+
# and removes ONLY the ones that are provably safe to lose.
|
|
11
|
+
#
|
|
12
|
+
# DATA SAFETY IS PARAMOUNT. A worktree is removed only when EVERY guard holds:
|
|
13
|
+
# 1. Working tree is clean — `git status --porcelain` is empty.
|
|
14
|
+
# 2. Old enough — creation stamp (or dir mtime) is older
|
|
15
|
+
# than the retention window (default 7d).
|
|
16
|
+
# 3. Branch work is preserved — for a pushable (github) repo the tip is
|
|
17
|
+
# reachable from origin (or the branch is
|
|
18
|
+
# on origin at the same tip); for an
|
|
19
|
+
# HQ-root local-only worktree the branch
|
|
20
|
+
# is already merged into the HQ root's
|
|
21
|
+
# local `main`. Fetch is best-effort; a
|
|
22
|
+
# failed fetch makes the worktree UNSAFE.
|
|
23
|
+
# 4. Not in use — no active session references its path,
|
|
24
|
+
# and it was not touched in the last 2h.
|
|
25
|
+
#
|
|
26
|
+
# Removal mechanics are the gentle git verbs only — never `rm -rf`, never
|
|
27
|
+
# `--force`, never `branch -D`:
|
|
28
|
+
# git -C <main-repo> worktree remove <abs> (HQ_ALLOW_HQ_ROOT_GIT=1 for HQ root)
|
|
29
|
+
# git -C <main-repo> worktree prune
|
|
30
|
+
# git -C <main-repo> branch -d <branch>
|
|
31
|
+
#
|
|
32
|
+
# DRY-RUN IS THE DEFAULT. Nothing is removed unless `--apply` is passed. A single
|
|
33
|
+
# worktree error is logged and skipped — it never aborts the run.
|
|
34
|
+
#
|
|
35
|
+
# Usage:
|
|
36
|
+
# worktree-gc.sh [--days N] [--apply] [--dry-run] [--gated] [--json]
|
|
37
|
+
#
|
|
38
|
+
# Flags:
|
|
39
|
+
# --days N retention window in days (default 7; env HQ_WORKTREE_GC_DAYS)
|
|
40
|
+
# --apply actually remove safe worktrees (default: dry-run, no changes)
|
|
41
|
+
# --dry-run classify only, make no changes (the default; explicit for clarity)
|
|
42
|
+
# --gated no-op if a GC run already happened in the last 24h (for cadence
|
|
43
|
+
# callers like handoff-post.sh); writes a marker on each real run
|
|
44
|
+
# --json emit a machine-readable summary + per-worktree records on stdout
|
|
45
|
+
#
|
|
46
|
+
# Exit code is 0 whenever the scan completed (even with per-worktree skips).
|
|
47
|
+
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
|
|
50
|
+
HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
51
|
+
|
|
52
|
+
# ------------------------------- args ---------------------------------------
|
|
53
|
+
DAYS="${HQ_WORKTREE_GC_DAYS:-7}"
|
|
54
|
+
APPLY=0
|
|
55
|
+
JSON=0
|
|
56
|
+
GATED=0
|
|
57
|
+
|
|
58
|
+
while [[ $# -gt 0 ]]; do
|
|
59
|
+
case "$1" in
|
|
60
|
+
--days) DAYS="${2:-}"; shift 2 ;;
|
|
61
|
+
--days=*) DAYS="${1#*=}"; shift ;;
|
|
62
|
+
--apply) APPLY=1; shift ;;
|
|
63
|
+
--dry-run) APPLY=0; shift ;;
|
|
64
|
+
--gated) GATED=1; shift ;;
|
|
65
|
+
--json) JSON=1; shift ;;
|
|
66
|
+
-h|--help)
|
|
67
|
+
sed -n '2,45p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
68
|
+
exit 0 ;;
|
|
69
|
+
*) echo "worktree-gc: unknown arg: $1" >&2; exit 2 ;;
|
|
70
|
+
esac
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
case "$DAYS" in
|
|
74
|
+
''|*[!0-9]*) echo "worktree-gc: --days must be a non-negative integer (got: $DAYS)" >&2; exit 2 ;;
|
|
75
|
+
esac
|
|
76
|
+
|
|
77
|
+
NOW=$(date -u +%s)
|
|
78
|
+
CUTOFF_EPOCH=$((NOW - DAYS * 86400))
|
|
79
|
+
RECENT_EPOCH=$((NOW - 2 * 60 * 60)) # 2h in-use guard
|
|
80
|
+
|
|
81
|
+
GC_META_DIR="$HQ_ROOT/workspace/worktrees/.gc-meta"
|
|
82
|
+
GATE_MARKER="$GC_META_DIR/.last-gc-run"
|
|
83
|
+
|
|
84
|
+
# ------------------------------- gating -------------------------------------
|
|
85
|
+
# --gated callers (handoff-post) get a once-per-24h ceiling so the opportunistic
|
|
86
|
+
# cadence never storms. The marker is written at the START of a real run so two
|
|
87
|
+
# near-simultaneous handoffs cannot both do the work.
|
|
88
|
+
if [[ "$GATED" -eq 1 ]]; then
|
|
89
|
+
if [[ -f "$GATE_MARKER" ]]; then
|
|
90
|
+
marker_mtime=$(stat -c %Y "$GATE_MARKER" 2>/dev/null || stat -f %m "$GATE_MARKER" 2>/dev/null || echo 0)
|
|
91
|
+
if (( NOW - marker_mtime < 86400 )); then
|
|
92
|
+
[[ "$JSON" -eq 1 ]] && echo '{"skipped":"gated","reason":"ran within 24h"}' || echo "worktree-gc: skipped (gated — ran within 24h)"
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
mkdir -p "$GC_META_DIR" 2>/dev/null || true
|
|
97
|
+
touch "$GATE_MARKER" 2>/dev/null || true
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# --------------------------- helper functions -------------------------------
|
|
101
|
+
path_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
102
|
+
|
|
103
|
+
# Stable per-path key shared with worktree.sh's creation stamp. MUST match the
|
|
104
|
+
# hashing in worktree.sh so a stamp written at creation is found here.
|
|
105
|
+
path_key() { printf '%s' "$1" | cksum | cut -d' ' -f1; }
|
|
106
|
+
|
|
107
|
+
dir_size_kb() { du -sk "$1" 2>/dev/null | awk '{print $1; exit}'; }
|
|
108
|
+
|
|
109
|
+
# is_ancestor <repo> <ancestor-ish> <descendant-ref> — true iff ancestor.
|
|
110
|
+
# Used only in `if` conditions so `set -e` never trips on the non-zero "not an
|
|
111
|
+
# ancestor" (1) or the error (>1, treated as not-an-ancestor → unsafe) results.
|
|
112
|
+
is_ancestor() { git -C "$1" merge-base --is-ancestor "$2" "$3" >/dev/null 2>&1; }
|
|
113
|
+
|
|
114
|
+
# session_references <abs-worktree-path> — true if any active session / thread /
|
|
115
|
+
# state file names this worktree path. Best-effort and conservative: an
|
|
116
|
+
# unreadable tree simply yields no match (the 2h mtime guard still applies).
|
|
117
|
+
session_references() {
|
|
118
|
+
local wt="$1" hay
|
|
119
|
+
for hay in "$HQ_ROOT/workspace/sessions" "$HQ_ROOT/.claude/state" \
|
|
120
|
+
"$HQ_ROOT/workspace/threads/handoff.json" "$HQ_ROOT/workspace/locks"; do
|
|
121
|
+
[[ -e "$hay" ]] || continue
|
|
122
|
+
if grep -rqF "$wt" "$hay" 2>/dev/null; then
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Optional timeout wrapper for network fetches (macOS has no `timeout`).
|
|
130
|
+
_TIMEOUT_BIN=""
|
|
131
|
+
if command -v timeout >/dev/null 2>&1; then _TIMEOUT_BIN="timeout"
|
|
132
|
+
elif command -v gtimeout >/dev/null 2>&1; then _TIMEOUT_BIN="gtimeout"; fi
|
|
133
|
+
run_fetch() { # <repo>
|
|
134
|
+
if [[ -n "$_TIMEOUT_BIN" ]]; then
|
|
135
|
+
"$_TIMEOUT_BIN" 45 git -C "$1" fetch origin --quiet >/dev/null 2>&1
|
|
136
|
+
else
|
|
137
|
+
git -C "$1" fetch origin --quiet >/dev/null 2>&1
|
|
138
|
+
fi
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# ----------------------------- enumerate ------------------------------------
|
|
142
|
+
# Genuine LINKED worktrees have a `.git` FILE (a gitfile). Main checkouts have a
|
|
143
|
+
# `.git` DIRECTORY, and plain project subdirectories have no `.git` at all — so
|
|
144
|
+
# `-name .git -type f` selects exactly the linked worktrees and never a main
|
|
145
|
+
# repo. Depth covers flat (<name>/.git) and nested (<repo>/<name>/.git) layouts.
|
|
146
|
+
CANDIDATES=()
|
|
147
|
+
while IFS= read -r gitfile; do
|
|
148
|
+
[[ -n "$gitfile" ]] || continue
|
|
149
|
+
CANDIDATES+=("$(cd "$(dirname "$gitfile")" && pwd -P)")
|
|
150
|
+
done < <(
|
|
151
|
+
{
|
|
152
|
+
[[ -d "$HQ_ROOT/workspace/worktrees" ]] && find "$HQ_ROOT/workspace/worktrees" -maxdepth 4 -name .git -type f 2>/dev/null
|
|
153
|
+
[[ -d "$HQ_ROOT/.claude/worktrees" ]] && find "$HQ_ROOT/.claude/worktrees" -maxdepth 3 -name .git -type f 2>/dev/null
|
|
154
|
+
} | sort -u
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Never GC the worktree this script is executing from.
|
|
158
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
159
|
+
|
|
160
|
+
# --------------------------- results / counters -----------------------------
|
|
161
|
+
REC_NDJSON="$(mktemp 2>/dev/null || echo "/tmp/worktree-gc-rec.$$")"
|
|
162
|
+
: > "$REC_NDJSON"
|
|
163
|
+
cleanup_rec() { rm -f "$REC_NDJSON"; }
|
|
164
|
+
trap cleanup_rec EXIT
|
|
165
|
+
|
|
166
|
+
examined=0
|
|
167
|
+
removed=0
|
|
168
|
+
would=0
|
|
169
|
+
reclaimed_kb=0
|
|
170
|
+
declare -i skip_total=0
|
|
171
|
+
|
|
172
|
+
# wt_git <git-args...> — run git against the current worktree's main repo, adding
|
|
173
|
+
# the HQ_ALLOW_HQ_ROOT_GIT escape hatch only for HQ-root-class worktrees (whose
|
|
174
|
+
# mutations touch the HQ root object store and are otherwise hook-blocked).
|
|
175
|
+
# Reads $main_repo and $is_hq_root from the loop scope.
|
|
176
|
+
wt_git() {
|
|
177
|
+
if [[ "${is_hq_root:-0}" -eq 1 ]]; then
|
|
178
|
+
HQ_ALLOW_HQ_ROOT_GIT=1 git -C "$main_repo" "$@"
|
|
179
|
+
else
|
|
180
|
+
git -C "$main_repo" "$@"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
record() { # <path> <repo> <branch> <hq_root 0|1> <age_days> <size_kb> <action> <reason>
|
|
185
|
+
jq -nc \
|
|
186
|
+
--arg path "$1" --arg repo "$2" --arg branch "$3" \
|
|
187
|
+
--argjson hq_root "$4" --arg age_days "$5" --arg size_kb "$6" \
|
|
188
|
+
--arg action "$7" --arg reason "$8" \
|
|
189
|
+
'{path:$path,repo:$repo,branch:$branch,hq_root:($hq_root==1),
|
|
190
|
+
age_days:($age_days|tonumber?),size_kb:($size_kb|tonumber?),
|
|
191
|
+
action:$action,reason:$reason}' >> "$REC_NDJSON"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
log() { [[ "$JSON" -eq 1 ]] || echo "$*"; }
|
|
195
|
+
|
|
196
|
+
skip() { # <path> <repo> <branch> <hq_root> <age_days> <size_kb> <reason>
|
|
197
|
+
skip_total+=1
|
|
198
|
+
record "$1" "$2" "$3" "$4" "$5" "$6" "skipped" "$7"
|
|
199
|
+
log " skip [$7] $1"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ------------------------------- main loop ----------------------------------
|
|
203
|
+
for wt in "${CANDIDATES[@]:-}"; do
|
|
204
|
+
[[ -n "$wt" ]] || continue
|
|
205
|
+
examined=$((examined + 1))
|
|
206
|
+
|
|
207
|
+
# Skip the worktree we are running inside.
|
|
208
|
+
if [[ "$SELF_DIR" == "$wt"/* || "$SELF_DIR" == "$wt" ]]; then
|
|
209
|
+
skip "$wt" "" "" 0 "" "" "self"; continue
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# Resolve the owning repo. A gitfile that no longer resolves is a git error.
|
|
213
|
+
common_dir=$(git -C "$wt" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)
|
|
214
|
+
if [[ -z "$common_dir" ]]; then
|
|
215
|
+
skip "$wt" "" "" 0 "" "" "git-error"; continue
|
|
216
|
+
fi
|
|
217
|
+
main_repo=$(cd "$(dirname "$common_dir")" && pwd -P 2>/dev/null || echo "")
|
|
218
|
+
if [[ -z "$main_repo" || ! -d "$main_repo" ]]; then
|
|
219
|
+
skip "$wt" "" "" 0 "" "" "git-error"; continue
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
is_hq_root=0
|
|
223
|
+
[[ "$main_repo" -ef "$HQ_ROOT" ]] && is_hq_root=1
|
|
224
|
+
|
|
225
|
+
branch=$(git -C "$wt" symbolic-ref --short HEAD 2>/dev/null || echo "")
|
|
226
|
+
head_sha=$(git -C "$wt" rev-parse HEAD 2>/dev/null || echo "")
|
|
227
|
+
|
|
228
|
+
# ---- guard 1: clean working tree ----
|
|
229
|
+
porcelain=$(git -C "$wt" status --porcelain 2>/dev/null || echo "__ERR__")
|
|
230
|
+
if [[ "$porcelain" == "__ERR__" ]]; then
|
|
231
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "" "" "git-error"; continue
|
|
232
|
+
fi
|
|
233
|
+
if [[ -n "$porcelain" ]]; then
|
|
234
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "" "" "dirty"; continue
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# ---- guard 4a: recent activity (in-use heuristic) ----
|
|
238
|
+
wt_mtime=$(path_mtime "$wt")
|
|
239
|
+
if (( wt_mtime > RECENT_EPOCH )); then
|
|
240
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "" "" "recent-activity"; continue
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# ---- guard 4b: active-session reference ----
|
|
244
|
+
if session_references "$wt"; then
|
|
245
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "" "" "active-session"; continue
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# ---- guard 2: age (creation stamp, else dir mtime) ----
|
|
249
|
+
created_epoch="$wt_mtime"
|
|
250
|
+
stamp_file="$GC_META_DIR/$(path_key "$wt").json"
|
|
251
|
+
if [[ -f "$stamp_file" ]]; then
|
|
252
|
+
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null || echo "")
|
|
253
|
+
[[ -n "$stamped" ]] && created_epoch="$stamped"
|
|
254
|
+
fi
|
|
255
|
+
age_days=$(( (NOW - created_epoch) / 86400 ))
|
|
256
|
+
if (( created_epoch > CUTOFF_EPOCH )); then
|
|
257
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "" "too-recent"; continue
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# ---- guard 3: branch work is preserved somewhere ----
|
|
261
|
+
preserved=0
|
|
262
|
+
if [[ "$is_hq_root" -eq 1 ]]; then
|
|
263
|
+
# HQ root is local-only; HQ never pushes. Safe only if the tip is already
|
|
264
|
+
# merged into the HQ root's local `main`.
|
|
265
|
+
if [[ -n "$head_sha" ]] && is_ancestor "$HQ_ROOT" "$head_sha" "main"; then
|
|
266
|
+
preserved=1
|
|
267
|
+
fi
|
|
268
|
+
if [[ "$preserved" -ne 1 ]]; then
|
|
269
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "" "unmerged-local"; continue
|
|
270
|
+
fi
|
|
271
|
+
else
|
|
272
|
+
# Pushable repo: require a github origin.
|
|
273
|
+
origin_url=$(git -C "$main_repo" remote get-url origin 2>/dev/null || echo "")
|
|
274
|
+
if [[ "$origin_url" != *github* ]]; then
|
|
275
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "" "non-github-remote"; continue
|
|
276
|
+
fi
|
|
277
|
+
# Best-effort fetch; a failed fetch means we cannot prove preservation → UNSAFE.
|
|
278
|
+
if ! run_fetch "$main_repo"; then
|
|
279
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "" "fetch-failed"; continue
|
|
280
|
+
fi
|
|
281
|
+
# Reachable from origin's default branch (HEAD → main → master)?
|
|
282
|
+
for ref in origin/HEAD origin/main origin/master; do
|
|
283
|
+
if git -C "$main_repo" rev-parse --verify --quiet "$ref" >/dev/null 2>&1; then
|
|
284
|
+
if [[ -n "$head_sha" ]] && is_ancestor "$main_repo" "$head_sha" "$ref"; then
|
|
285
|
+
preserved=1; break
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
# Or the branch itself exists on origin at this exact tip.
|
|
290
|
+
if [[ "$preserved" -ne 1 && -n "$branch" && -n "$head_sha" ]]; then
|
|
291
|
+
remote_sha=$(git -C "$main_repo" rev-parse --verify --quiet "refs/remotes/origin/$branch" 2>/dev/null || echo "")
|
|
292
|
+
[[ -z "$remote_sha" ]] && remote_sha=$(git -C "$main_repo" ls-remote --heads origin "$branch" 2>/dev/null | awk '{print $1; exit}')
|
|
293
|
+
[[ -n "$remote_sha" && "$remote_sha" == "$head_sha" ]] && preserved=1
|
|
294
|
+
fi
|
|
295
|
+
if [[ "$preserved" -ne 1 ]]; then
|
|
296
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "" "unpushed"; continue
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# ---- SAFE TO REMOVE ----
|
|
301
|
+
size_kb=$(dir_size_kb "$wt"); size_kb="${size_kb:-0}"
|
|
302
|
+
|
|
303
|
+
if [[ "$APPLY" -ne 1 ]]; then
|
|
304
|
+
would=$((would + 1))
|
|
305
|
+
reclaimed_kb=$((reclaimed_kb + size_kb))
|
|
306
|
+
record "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "$size_kb" "would-remove" "safe"
|
|
307
|
+
log " DRY would remove ($((size_kb / 1024)) MB, ${age_days}d) $wt"
|
|
308
|
+
continue
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# Apply: gentle verbs only, fail-soft per worktree.
|
|
312
|
+
rm_rc=0
|
|
313
|
+
wt_git worktree remove "$wt" >/dev/null 2>&1 || rm_rc=$?
|
|
314
|
+
if [[ "$rm_rc" -ne 0 ]]; then
|
|
315
|
+
skip "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "$size_kb" "remove-failed"; continue
|
|
316
|
+
fi
|
|
317
|
+
wt_git worktree prune >/dev/null 2>&1 || true
|
|
318
|
+
if [[ -n "$branch" ]]; then
|
|
319
|
+
# Safe -d only: git refuses if the branch is not merged. Never -D.
|
|
320
|
+
wt_git branch -d "$branch" >/dev/null 2>&1 || true
|
|
321
|
+
fi
|
|
322
|
+
rm -f "$stamp_file" 2>/dev/null || true
|
|
323
|
+
|
|
324
|
+
removed=$((removed + 1))
|
|
325
|
+
reclaimed_kb=$((reclaimed_kb + size_kb))
|
|
326
|
+
record "$wt" "$main_repo" "$branch" "$is_hq_root" "$age_days" "$size_kb" "removed" "safe"
|
|
327
|
+
log " GC removed ($((size_kb / 1024)) MB, ${age_days}d) $wt"
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
# ------------------------------- summary ------------------------------------
|
|
331
|
+
reclaimed_gb=$(awk -v kb="$reclaimed_kb" 'BEGIN{printf "%.2f", kb/1024/1024}')
|
|
332
|
+
mode="dry-run"; [[ "$APPLY" -eq 1 ]] && mode="apply"
|
|
333
|
+
|
|
334
|
+
if [[ "$JSON" -eq 1 ]]; then
|
|
335
|
+
jq -n \
|
|
336
|
+
--arg mode "$mode" \
|
|
337
|
+
--argjson examined "$examined" \
|
|
338
|
+
--argjson removed "$removed" \
|
|
339
|
+
--argjson reclaimed_kb "$reclaimed_kb" \
|
|
340
|
+
--arg reclaimed_gb "$reclaimed_gb" \
|
|
341
|
+
--argjson days "$DAYS" \
|
|
342
|
+
--slurpfile records "$REC_NDJSON" \
|
|
343
|
+
'{
|
|
344
|
+
mode: $mode,
|
|
345
|
+
retention_days: $days,
|
|
346
|
+
examined: $examined,
|
|
347
|
+
removed: $removed,
|
|
348
|
+
reclaimed_gb: ($reclaimed_gb|tonumber),
|
|
349
|
+
reclaimed_kb: $reclaimed_kb,
|
|
350
|
+
skipped_by_reason: ([$records[] | select(.action=="skipped") | .reason]
|
|
351
|
+
| group_by(.) | map({(.[0]): length}) | add // {}),
|
|
352
|
+
records: $records
|
|
353
|
+
}'
|
|
354
|
+
else
|
|
355
|
+
if [[ "$APPLY" -eq 1 ]]; then
|
|
356
|
+
echo "worktree-gc [apply]: examined $examined · removed $removed · reclaimed ${reclaimed_gb} GB · skipped ${skip_total} (retention ${DAYS}d)"
|
|
357
|
+
else
|
|
358
|
+
echo "worktree-gc [dry-run]: examined $examined · would remove $would · would reclaim ${reclaimed_gb} GB · skipped ${skip_total} (retention ${DAYS}d)"
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
exit 0
|
|
@@ -5205,6 +5205,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5205
5205
|
}, {
|
|
5206
5206
|
readonly flags: "--limit <n>";
|
|
5207
5207
|
readonly description: "handle at most this many copies";
|
|
5208
|
+
}, {
|
|
5209
|
+
readonly flags: "--only <file>";
|
|
5210
|
+
readonly description: "handle only the copy -> original pairs listed in this JSON file";
|
|
5208
5211
|
}, {
|
|
5209
5212
|
readonly flags: "--json";
|
|
5210
5213
|
readonly description: "print JSON";
|
|
@@ -5460,6 +5463,58 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5460
5463
|
readonly options: readonly [];
|
|
5461
5464
|
readonly subcommands: readonly [];
|
|
5462
5465
|
}];
|
|
5466
|
+
}, {
|
|
5467
|
+
readonly name: "worktree-gc";
|
|
5468
|
+
readonly description: "Safely collect old, preserved HQ worktrees";
|
|
5469
|
+
readonly aliases: readonly [];
|
|
5470
|
+
readonly hidden: false;
|
|
5471
|
+
readonly usage: "[args...]";
|
|
5472
|
+
readonly arguments: readonly [{
|
|
5473
|
+
readonly name: "args";
|
|
5474
|
+
readonly required: false;
|
|
5475
|
+
readonly variadic: true;
|
|
5476
|
+
}];
|
|
5477
|
+
readonly options: readonly [];
|
|
5478
|
+
readonly subcommands: readonly [];
|
|
5479
|
+
}, {
|
|
5480
|
+
readonly name: "clean-worktree-reconcile-handoff";
|
|
5481
|
+
readonly description: "Remove only byte-identical incoming handoff mirrors";
|
|
5482
|
+
readonly aliases: readonly [];
|
|
5483
|
+
readonly hidden: false;
|
|
5484
|
+
readonly usage: "[args...]";
|
|
5485
|
+
readonly arguments: readonly [{
|
|
5486
|
+
readonly name: "args";
|
|
5487
|
+
readonly required: false;
|
|
5488
|
+
readonly variadic: true;
|
|
5489
|
+
}];
|
|
5490
|
+
readonly options: readonly [];
|
|
5491
|
+
readonly subcommands: readonly [];
|
|
5492
|
+
}, {
|
|
5493
|
+
readonly name: "resume-thread-lock";
|
|
5494
|
+
readonly description: "Inspect or acquire a durable resumework lock";
|
|
5495
|
+
readonly aliases: readonly [];
|
|
5496
|
+
readonly hidden: false;
|
|
5497
|
+
readonly usage: "[args...]";
|
|
5498
|
+
readonly arguments: readonly [{
|
|
5499
|
+
readonly name: "args";
|
|
5500
|
+
readonly required: false;
|
|
5501
|
+
readonly variadic: true;
|
|
5502
|
+
}];
|
|
5503
|
+
readonly options: readonly [];
|
|
5504
|
+
readonly subcommands: readonly [];
|
|
5505
|
+
}, {
|
|
5506
|
+
readonly name: "verify-story-deliverables";
|
|
5507
|
+
readonly description: "Check declared and returned story deliverables";
|
|
5508
|
+
readonly aliases: readonly [];
|
|
5509
|
+
readonly hidden: false;
|
|
5510
|
+
readonly usage: "[args...]";
|
|
5511
|
+
readonly arguments: readonly [{
|
|
5512
|
+
readonly name: "args";
|
|
5513
|
+
readonly required: false;
|
|
5514
|
+
readonly variadic: true;
|
|
5515
|
+
}];
|
|
5516
|
+
readonly options: readonly [];
|
|
5517
|
+
readonly subcommands: readonly [];
|
|
5463
5518
|
}, {
|
|
5464
5519
|
readonly name: "generate-workers-registry";
|
|
5465
5520
|
readonly description: "Regenerate the derived workers registry";
|
|
@@ -6730,6 +6730,10 @@ export const COMMAND_CATALOG = [
|
|
|
6730
6730
|
"flags": "--limit <n>",
|
|
6731
6731
|
"description": "handle at most this many copies"
|
|
6732
6732
|
},
|
|
6733
|
+
{
|
|
6734
|
+
"flags": "--only <file>",
|
|
6735
|
+
"description": "handle only the copy -> original pairs listed in this JSON file"
|
|
6736
|
+
},
|
|
6733
6737
|
{
|
|
6734
6738
|
"flags": "--json",
|
|
6735
6739
|
"description": "print JSON"
|
|
@@ -7047,6 +7051,70 @@ export const COMMAND_CATALOG = [
|
|
|
7047
7051
|
}
|
|
7048
7052
|
]
|
|
7049
7053
|
},
|
|
7054
|
+
{
|
|
7055
|
+
"name": "worktree-gc",
|
|
7056
|
+
"description": "Safely collect old, preserved HQ worktrees",
|
|
7057
|
+
"aliases": [],
|
|
7058
|
+
"hidden": false,
|
|
7059
|
+
"usage": "[args...]",
|
|
7060
|
+
"arguments": [
|
|
7061
|
+
{
|
|
7062
|
+
"name": "args",
|
|
7063
|
+
"required": false,
|
|
7064
|
+
"variadic": true
|
|
7065
|
+
}
|
|
7066
|
+
],
|
|
7067
|
+
"options": [],
|
|
7068
|
+
"subcommands": []
|
|
7069
|
+
},
|
|
7070
|
+
{
|
|
7071
|
+
"name": "clean-worktree-reconcile-handoff",
|
|
7072
|
+
"description": "Remove only byte-identical incoming handoff mirrors",
|
|
7073
|
+
"aliases": [],
|
|
7074
|
+
"hidden": false,
|
|
7075
|
+
"usage": "[args...]",
|
|
7076
|
+
"arguments": [
|
|
7077
|
+
{
|
|
7078
|
+
"name": "args",
|
|
7079
|
+
"required": false,
|
|
7080
|
+
"variadic": true
|
|
7081
|
+
}
|
|
7082
|
+
],
|
|
7083
|
+
"options": [],
|
|
7084
|
+
"subcommands": []
|
|
7085
|
+
},
|
|
7086
|
+
{
|
|
7087
|
+
"name": "resume-thread-lock",
|
|
7088
|
+
"description": "Inspect or acquire a durable resumework lock",
|
|
7089
|
+
"aliases": [],
|
|
7090
|
+
"hidden": false,
|
|
7091
|
+
"usage": "[args...]",
|
|
7092
|
+
"arguments": [
|
|
7093
|
+
{
|
|
7094
|
+
"name": "args",
|
|
7095
|
+
"required": false,
|
|
7096
|
+
"variadic": true
|
|
7097
|
+
}
|
|
7098
|
+
],
|
|
7099
|
+
"options": [],
|
|
7100
|
+
"subcommands": []
|
|
7101
|
+
},
|
|
7102
|
+
{
|
|
7103
|
+
"name": "verify-story-deliverables",
|
|
7104
|
+
"description": "Check declared and returned story deliverables",
|
|
7105
|
+
"aliases": [],
|
|
7106
|
+
"hidden": false,
|
|
7107
|
+
"usage": "[args...]",
|
|
7108
|
+
"arguments": [
|
|
7109
|
+
{
|
|
7110
|
+
"name": "args",
|
|
7111
|
+
"required": false,
|
|
7112
|
+
"variadic": true
|
|
7113
|
+
}
|
|
7114
|
+
],
|
|
7115
|
+
"options": [],
|
|
7116
|
+
"subcommands": []
|
|
7117
|
+
},
|
|
7050
7118
|
{
|
|
7051
7119
|
"name": "generate-workers-registry",
|
|
7052
7120
|
"description": "Regenerate the derived workers registry",
|
|
@@ -20,6 +20,7 @@ import * as os from "node:os";
|
|
|
20
20
|
import { CLI_VERSION } from "../cli-version.js";
|
|
21
21
|
import { DEFAULT_VAULT_API_URL, personTokenCacheFile, } from "../utils/cognito-session.js";
|
|
22
22
|
import { HQ_CLIENT_NAME } from "../utils/vault-api.js";
|
|
23
|
+
import { queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, } from "../utils/cli-telemetry.js";
|
|
23
24
|
import { networkTransportErrorCode } from "../utils/network-transport-error.js";
|
|
24
25
|
import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
|
|
25
26
|
import { generateHostKeyPair, machineCredsFileExists, readExternalMachineCreds, writeHostKeyPair, writeMachineCreds, } from "../lib/agent-kit/creds.js";
|
|
@@ -373,9 +374,14 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
373
374
|
else if (err instanceof EnrollError && err.upgradeUrl) {
|
|
374
375
|
const interactive = canOfferTeamUpgrade();
|
|
375
376
|
console.error(chalk.red(formatEnrollFailureMessage(err, { urlOnly: !interactive })));
|
|
377
|
+
const exposure = err.code === "billing_required"
|
|
378
|
+
? queueCliLimitNoticeExposure("agents", "refusal")
|
|
379
|
+
: null;
|
|
376
380
|
if (interactive) {
|
|
377
381
|
try {
|
|
378
|
-
await offerUpgradeLink(err.upgradeUrl, { interactive: true });
|
|
382
|
+
const opened = await offerUpgradeLink(err.upgradeUrl, { interactive: true });
|
|
383
|
+
if (opened)
|
|
384
|
+
queueCliLimitNoticeEngagement(exposure);
|
|
379
385
|
}
|
|
380
386
|
catch (openErr) {
|
|
381
387
|
const message = openErr instanceof Error ? openErr.message : String(openErr);
|
|
@@ -386,7 +392,9 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
386
392
|
else {
|
|
387
393
|
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
388
394
|
}
|
|
389
|
-
|
|
395
|
+
// Let the CLI's bounded finalization flush the best-effort prompt
|
|
396
|
+
// telemetry before the process exits with the same failure status.
|
|
397
|
+
process.exitCode = 1;
|
|
390
398
|
}
|
|
391
399
|
});
|
|
392
400
|
}
|
|
@@ -62,6 +62,7 @@ export declare function agentCreatePlanLimitFromPayload(status: number, body: un
|
|
|
62
62
|
/** Print a server-priced plan block without minting or rewriting its URL. */
|
|
63
63
|
export declare function surfaceAgentCreatePlanLimit(planLimit: AgentCreatePlanLimit, opts?: {
|
|
64
64
|
json?: boolean;
|
|
65
|
+
companyUid?: string;
|
|
65
66
|
}): Promise<void>;
|
|
66
67
|
/**
|
|
67
68
|
* Resolve a closed-set option value, or exit(1) with a message naming the
|
package/dist/commands/agents.js
CHANGED
|
@@ -34,6 +34,7 @@ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
|
34
34
|
import { maxLength } from "../utils/table-width.js";
|
|
35
35
|
import { DEFAULT_VAULT_API_URL } from "../utils/cognito-session.js";
|
|
36
36
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
37
|
+
import { queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, } from "../utils/cli-telemetry.js";
|
|
37
38
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
38
39
|
import { consoleOrigin } from "./integrations-connect.js";
|
|
39
40
|
import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, waitForTerminalSession, } from "@indigoai-us/hq-cloud/outposts/node";
|
|
@@ -128,13 +129,14 @@ function formatServerPrice(amountMinor, currency) {
|
|
|
128
129
|
async function offerAgentCheckout(url) {
|
|
129
130
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
130
131
|
try {
|
|
131
|
-
await offerUpgradeLink(url, {
|
|
132
|
+
return await offerUpgradeLink(url, {
|
|
132
133
|
interactive: true,
|
|
133
134
|
ask: (question) => new Promise((resolve) => rl.question(question, resolve)),
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
catch {
|
|
137
138
|
console.error("Could not open the upgrade page automatically. Use the URL above.");
|
|
139
|
+
return false;
|
|
138
140
|
}
|
|
139
141
|
finally {
|
|
140
142
|
rl.close();
|
|
@@ -149,11 +151,14 @@ export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
|
|
|
149
151
|
if (!planLimit.checkoutUrl) {
|
|
150
152
|
console.error(reason);
|
|
151
153
|
console.error("Ask a company owner to upgrade before creating an agent.");
|
|
154
|
+
queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
|
|
152
155
|
return;
|
|
153
156
|
}
|
|
154
157
|
console.error(formatUpgradeRefusal(reason, planLimit.checkoutUrl, { urlOnly: !interactive }));
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
const exposure = queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
|
|
159
|
+
if (interactive && await offerAgentCheckout(planLimit.checkoutUrl)) {
|
|
160
|
+
queueCliLimitNoticeEngagement(exposure);
|
|
161
|
+
}
|
|
157
162
|
}
|
|
158
163
|
/**
|
|
159
164
|
* Resolve a closed-set option value, or exit(1) with a message naming the
|
|
@@ -1548,7 +1553,10 @@ export function registerAgentsCommand(program) {
|
|
|
1548
1553
|
if (!err.agentCreatePlanLimit) {
|
|
1549
1554
|
throw new Error("HQ Pro returned an unrecognized agent payment-required response.");
|
|
1550
1555
|
}
|
|
1551
|
-
await surfaceAgentCreatePlanLimit(err.agentCreatePlanLimit,
|
|
1556
|
+
await surfaceAgentCreatePlanLimit(err.agentCreatePlanLimit, {
|
|
1557
|
+
...opts,
|
|
1558
|
+
companyUid,
|
|
1559
|
+
});
|
|
1552
1560
|
// Keep the payment-required status through normal CLI teardown.
|
|
1553
1561
|
// Unlike process.exit(), this is testable and cannot be caught by
|
|
1554
1562
|
// this action's outer error boundary as a generic failure.
|