@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/assets/scaffold/core/scripts/clean-worktree-reconcile-handoff.sh +171 -0
  3. package/assets/scaffold/core/scripts/resume-thread-lock.sh +285 -0
  4. package/assets/scaffold/core/scripts/verify-story-deliverables.sh +148 -0
  5. package/assets/scaffold/core/scripts/worktree-gc.sh +362 -0
  6. package/dist/command-catalog.generated.d.ts +55 -0
  7. package/dist/command-catalog.generated.js +68 -0
  8. package/dist/commands/agent-enroll.js +10 -2
  9. package/dist/commands/agents.d.ts +1 -0
  10. package/dist/commands/agents.js +12 -4
  11. package/dist/commands/cloud-provision.js +99 -78
  12. package/dist/commands/core-native-handoff-flag.d.ts +4 -0
  13. package/dist/commands/core-native-handoff-flag.js +7 -0
  14. package/dist/commands/core-ontology.d.ts +4 -1
  15. package/dist/commands/core-ontology.js +26 -0
  16. package/dist/commands/core.js +45 -0
  17. package/dist/commands/lanes-resume.js +41 -2
  18. package/dist/commands/lanes-shared.d.ts +2 -0
  19. package/dist/commands/lanes.js +48 -29
  20. package/dist/commands/team-sync.js +71 -10
  21. package/dist/fast-lanes-list-route.js +2 -0
  22. package/dist/fast-lanes-list.js +1 -29
  23. package/dist/lanes-drain-hook.d.ts +12 -0
  24. package/dist/lanes-drain-hook.js +59 -55
  25. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +10 -1
  26. package/dist/lib/checkpoint-ontology/merge-copies.js +17 -2
  27. package/dist/lib/checkpoint-ontology/plan.d.ts +0 -88
  28. package/dist/lib/checkpoint-ontology/plan.js +80 -107
  29. package/dist/lib/core-utils/sentry-report.js +5 -32
  30. package/dist/lib/core-utils/worktree-resume.d.ts +18 -0
  31. package/dist/lib/core-utils/worktree-resume.js +1182 -0
  32. package/dist/lib/lanes/dropbox.d.ts +1 -2
  33. package/dist/lib/lanes/dropbox.js +10 -111
  34. package/dist/lib/lanes/message-replay.d.ts +8 -0
  35. package/dist/lib/lanes/message-replay.js +119 -0
  36. package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +21 -0
  37. package/dist/lib/lanes/outpost-snapshot-refresh.js +329 -0
  38. package/dist/lib/plan-limit-nag.js +11 -0
  39. package/dist/main.js +17 -4
  40. package/dist/utils/cli-telemetry.d.ts +21 -0
  41. package/dist/utils/cli-telemetry.js +101 -1
  42. package/dist/utils/cognito-refresh-sentry.d.ts +3 -0
  43. package/dist/utils/cognito-refresh-sentry.js +49 -0
  44. package/dist/utils/cognito-session.js +55 -0
  45. 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
- process.exit(1);
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
@@ -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
- if (interactive)
156
- await offerAgentCheckout(planLimit.checkoutUrl);
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, opts);
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.