@mmerterden/multi-agent-pipeline 11.5.0 → 12.1.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 +75 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +2 -0
- package/docs/adr/README.md +1 -0
- package/docs/features.md +2 -2
- package/install/claude.mjs +51 -1
- package/install/index.mjs +34 -1
- package/package.json +2 -2
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
- package/pipeline/lib/multi-repo-pipeline.sh +6 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
- package/pipeline/scripts/_cost.mjs +35 -0
- package/pipeline/scripts/agent-guard.py +8 -3
- package/pipeline/scripts/cost-budget-check.mjs +2 -1
- package/pipeline/scripts/cost-lib.sh +43 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +4 -2
- package/pipeline/scripts/gc-tmp.sh +67 -8
- package/pipeline/scripts/gc-worktrees.sh +140 -0
- package/pipeline/scripts/localize-commands.mjs +130 -0
- package/pipeline/scripts/phase-tracker.sh +10 -19
- package/pipeline/scripts/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- package/pipeline/scripts/render-agent-log-cost.sh +7 -13
- package/pipeline/scripts/render-cost-summary.sh +4 -7
- package/pipeline/scripts/run-aggregator.mjs +2 -3
- package/pipeline/scripts/smoke-description-tr.sh +82 -0
- package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
- package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
- package/pipeline/scripts/smoke-prune-logs.sh +57 -7
- package/pipeline/scripts/smoke-purge.sh +138 -0
- package/pipeline/scripts/smoke-source-parity.sh +85 -0
- package/pipeline/scripts/smoke-update-check.sh +13 -0
- package/pipeline/scripts/uninstall.mjs +41 -6
- package/pipeline/scripts/update-check.sh +9 -0
- package/pipeline/skills/.skill-manifest.json +7 -7
- package/pipeline/skills/.skills-index.json +9 -9
- package/pipeline/skills/shared/README.md +19 -10
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
- package/pipeline/skills/skills-index.md +1 -1
- package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
- package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# purge.sh - deletion backend for /multi-agent:purge (full reset).
|
|
3
|
+
#
|
|
4
|
+
# Wipes every multi-agent worktree under <repo>/.worktrees/ (registered or
|
|
5
|
+
# orphaned), deletes the local branches those worktrees carried, and resets
|
|
6
|
+
# the project's task counter at
|
|
7
|
+
# $HOME/.claude/logs/multi-agent/<project>/.counter
|
|
8
|
+
#
|
|
9
|
+
# The confirmation UX (double confirmation, remote-branch question) lives in
|
|
10
|
+
# the /multi-agent:purge skill; this script is the mechanical part. Task log
|
|
11
|
+
# dirs are /multi-agent:prune-logs territory (prune-logs.sh --project=<name>).
|
|
12
|
+
#
|
|
13
|
+
# PRESERVED ALWAYS:
|
|
14
|
+
# - the repo's main worktree, its checked-out branch, and main/master/develop
|
|
15
|
+
# - everything outside <repo>/.worktrees/ (symlinked entries are never
|
|
16
|
+
# followed; every rm target must resolve strictly inside .worktrees/)
|
|
17
|
+
# - the audit trail + metrics corpus at the log root (only the project's
|
|
18
|
+
# .counter file is removed there)
|
|
19
|
+
#
|
|
20
|
+
# SAFE BY DEFAULT: dry-run. Lists what WOULD be removed; deletes nothing until
|
|
21
|
+
# you pass --yes. Remote branches are deleted only with --delete-remote AND
|
|
22
|
+
# --yes; a missing/unreachable remote is a warning, not a failure.
|
|
23
|
+
#
|
|
24
|
+
# Usage:
|
|
25
|
+
# purge.sh # dry-run in the current repo
|
|
26
|
+
# purge.sh --yes # wipe worktrees + branches + counter
|
|
27
|
+
# purge.sh --repo <path> # operate on another repo
|
|
28
|
+
# purge.sh --delete-remote --yes # also delete the remote branches
|
|
29
|
+
#
|
|
30
|
+
# Env:
|
|
31
|
+
# PURGE_LOG_ROOT override the log root (default $HOME/.claude/logs/multi-agent)
|
|
32
|
+
#
|
|
33
|
+
# Exit: 0 on success (including "nothing to do"), 2 on usage error or refused
|
|
34
|
+
# repo.
|
|
35
|
+
|
|
36
|
+
set -uo pipefail
|
|
37
|
+
|
|
38
|
+
REPO="$PWD"
|
|
39
|
+
DELETE=0
|
|
40
|
+
DELETE_REMOTE=0
|
|
41
|
+
LOG_ROOT="${PURGE_LOG_ROOT:-$HOME/.claude/logs/multi-agent}"
|
|
42
|
+
|
|
43
|
+
usage() {
|
|
44
|
+
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
while [ $# -gt 0 ]; do
|
|
48
|
+
case "$1" in
|
|
49
|
+
--yes | --force) DELETE=1; shift ;;
|
|
50
|
+
--delete-remote) DELETE_REMOTE=1; shift ;;
|
|
51
|
+
--repo)
|
|
52
|
+
REPO="${2:-}"
|
|
53
|
+
[ -z "$REPO" ] && { echo "purge: --repo needs a path" >&2; exit 2; }
|
|
54
|
+
shift 2
|
|
55
|
+
;;
|
|
56
|
+
-h | --help) usage; exit 0 ;;
|
|
57
|
+
*) echo "purge: unknown argument: $1" >&2; exit 2 ;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
62
|
+
echo "purge: $REPO is not a git work tree - nothing to do"
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Anchor everything on the MAIN worktree (first porcelain entry), so running
|
|
67
|
+
# purge from inside a linked worktree still targets <repo>/.worktrees/.
|
|
68
|
+
MAIN_WT="$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
|
|
69
|
+
MAIN_WT="$(cd "$MAIN_WT" 2>/dev/null && pwd -P)"
|
|
70
|
+
HOME_REAL="$(cd "$HOME" 2>/dev/null && pwd -P || printf '%s' "$HOME")"
|
|
71
|
+
if [ -z "$MAIN_WT" ] || [ "$MAIN_WT" = "/" ] || [ "$MAIN_WT" = "$HOME_REAL" ]; then
|
|
72
|
+
echo "purge: refusing to operate on repo root '$MAIN_WT'" >&2
|
|
73
|
+
exit 2
|
|
74
|
+
fi
|
|
75
|
+
WT_ROOT="$MAIN_WT/.worktrees"
|
|
76
|
+
CURRENT_BRANCH="$(git -C "$MAIN_WT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
77
|
+
|
|
78
|
+
# Branches that must survive a purge no matter what a worktree carried.
|
|
79
|
+
is_protected_branch() {
|
|
80
|
+
case "$1" in
|
|
81
|
+
"" | main | master | develop | "$CURRENT_BRANCH") return 0 ;;
|
|
82
|
+
esac
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resolve_dir() {
|
|
87
|
+
cd "$1" 2>/dev/null && pwd -P
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Collect registered worktrees that live strictly inside .worktrees/, with
|
|
91
|
+
# the branch each carries (empty when detached). bash 3.2: parallel arrays.
|
|
92
|
+
wt_paths=()
|
|
93
|
+
wt_branches=()
|
|
94
|
+
cur_path=""
|
|
95
|
+
cur_branch=""
|
|
96
|
+
flush_block() {
|
|
97
|
+
[ -z "$cur_path" ] && { cur_branch=""; return 0; }
|
|
98
|
+
local resolved
|
|
99
|
+
resolved="$(resolve_dir "$cur_path")"
|
|
100
|
+
case "$resolved" in
|
|
101
|
+
"$WT_ROOT"/*)
|
|
102
|
+
wt_paths+=("$resolved")
|
|
103
|
+
wt_branches+=("$cur_branch")
|
|
104
|
+
;;
|
|
105
|
+
esac
|
|
106
|
+
cur_path=""
|
|
107
|
+
cur_branch=""
|
|
108
|
+
}
|
|
109
|
+
while IFS= read -r line; do
|
|
110
|
+
case "$line" in
|
|
111
|
+
worktree\ *) flush_block; cur_path="${line#worktree }" ;;
|
|
112
|
+
branch\ refs/heads/*) cur_branch="${line#branch refs/heads/}" ;;
|
|
113
|
+
esac
|
|
114
|
+
done <<EOF
|
|
115
|
+
$(git -C "$MAIN_WT" worktree list --porcelain 2>/dev/null)
|
|
116
|
+
EOF
|
|
117
|
+
flush_block
|
|
118
|
+
|
|
119
|
+
# Orphan dirs under .worktrees/ that git no longer registers.
|
|
120
|
+
orphans=()
|
|
121
|
+
if [ -d "$WT_ROOT" ]; then
|
|
122
|
+
for d in "$WT_ROOT"/*/; do
|
|
123
|
+
[ -d "$d" ] || continue
|
|
124
|
+
[ -L "${d%/}" ] && continue # never follow symlinked entries out of the tree
|
|
125
|
+
dir="$(resolve_dir "$d")"
|
|
126
|
+
[ -z "$dir" ] && continue
|
|
127
|
+
case "$dir" in "$WT_ROOT"/*) ;; *) continue ;; esac
|
|
128
|
+
reg=0
|
|
129
|
+
for p in ${wt_paths+"${wt_paths[@]}"}; do
|
|
130
|
+
[ "$p" = "$dir" ] && { reg=1; break; }
|
|
131
|
+
done
|
|
132
|
+
[ "$reg" -eq 1 ] && continue
|
|
133
|
+
orphans+=("$dir")
|
|
134
|
+
done
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Counter file for this project.
|
|
138
|
+
PROJECT="$(basename "$MAIN_WT")"
|
|
139
|
+
COUNTER=""
|
|
140
|
+
if [ -n "$PROJECT" ] && [ "$PROJECT" != "." ] && [ "$PROJECT" != ".." ] &&
|
|
141
|
+
[ -f "$LOG_ROOT/$PROJECT/.counter" ]; then
|
|
142
|
+
COUNTER="$LOG_ROOT/$PROJECT/.counter"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
total=$((${#wt_paths[@]} + ${#orphans[@]}))
|
|
146
|
+
if [ "$total" -eq 0 ] && [ -z "$COUNTER" ]; then
|
|
147
|
+
echo "purge: no multi-agent worktrees, branches, or counter in $MAIN_WT - nothing to do"
|
|
148
|
+
exit 0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [ "$DELETE" -eq 0 ]; then
|
|
152
|
+
echo "DRY-RUN - purge would remove from $MAIN_WT:"
|
|
153
|
+
i=0
|
|
154
|
+
while [ "$i" -lt "${#wt_paths[@]}" ]; do
|
|
155
|
+
br="${wt_branches[$i]}"
|
|
156
|
+
echo " worktree ${wt_paths[$i]#"$MAIN_WT"/} (branch: ${br:-detached})"
|
|
157
|
+
if ! is_protected_branch "$br"; then
|
|
158
|
+
echo " local branch $br"
|
|
159
|
+
[ "$DELETE_REMOTE" -eq 1 ] && echo " remote branch origin/$br"
|
|
160
|
+
fi
|
|
161
|
+
i=$((i + 1))
|
|
162
|
+
done
|
|
163
|
+
for d in ${orphans+"${orphans[@]}"}; do
|
|
164
|
+
echo " orphan worktree dir ${d#"$MAIN_WT"/}"
|
|
165
|
+
done
|
|
166
|
+
[ -n "$COUNTER" ] && echo " task counter $COUNTER"
|
|
167
|
+
echo "Main worktree, current branch, and main/master/develop are never touched."
|
|
168
|
+
echo "Re-run with --yes to delete."
|
|
169
|
+
exit 0
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
removed_wt=0
|
|
173
|
+
removed_br=0
|
|
174
|
+
i=0
|
|
175
|
+
while [ "$i" -lt "${#wt_paths[@]}" ]; do
|
|
176
|
+
p="${wt_paths[$i]}"
|
|
177
|
+
br="${wt_branches[$i]}"
|
|
178
|
+
i=$((i + 1))
|
|
179
|
+
wt_gone=0
|
|
180
|
+
if git -C "$MAIN_WT" worktree remove --force "$p" >/dev/null 2>&1; then
|
|
181
|
+
wt_gone=1
|
|
182
|
+
else
|
|
183
|
+
# Fall back to a guarded rm + prune when git refuses (locked, damaged).
|
|
184
|
+
case "$p" in
|
|
185
|
+
"$WT_ROOT"/*)
|
|
186
|
+
rm -rf "$p" 2>/dev/null && wt_gone=1
|
|
187
|
+
git -C "$MAIN_WT" worktree prune >/dev/null 2>&1
|
|
188
|
+
;;
|
|
189
|
+
*) echo "purge: skipping worktree outside $WT_ROOT: $p" >&2 ;;
|
|
190
|
+
esac
|
|
191
|
+
fi
|
|
192
|
+
if [ "$wt_gone" -eq 1 ]; then
|
|
193
|
+
removed_wt=$((removed_wt + 1))
|
|
194
|
+
echo "→ removed worktree ${p#"$MAIN_WT"/}"
|
|
195
|
+
else
|
|
196
|
+
echo "purge: warning: could not remove worktree $p" >&2
|
|
197
|
+
continue
|
|
198
|
+
fi
|
|
199
|
+
if is_protected_branch "$br"; then
|
|
200
|
+
[ -n "$br" ] && echo "→ kept protected branch $br"
|
|
201
|
+
continue
|
|
202
|
+
fi
|
|
203
|
+
if git -C "$MAIN_WT" branch -D "$br" >/dev/null 2>&1; then
|
|
204
|
+
removed_br=$((removed_br + 1))
|
|
205
|
+
echo "→ deleted local branch $br"
|
|
206
|
+
fi
|
|
207
|
+
if [ "$DELETE_REMOTE" -eq 1 ]; then
|
|
208
|
+
if git -C "$MAIN_WT" push origin --delete "$br" >/dev/null 2>&1; then
|
|
209
|
+
echo "→ deleted remote branch origin/$br"
|
|
210
|
+
else
|
|
211
|
+
echo "purge: warning: could not delete remote branch origin/$br (no remote?)" >&2
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
for d in ${orphans+"${orphans[@]}"}; do
|
|
217
|
+
# Containment guard: resolve again right before rm.
|
|
218
|
+
dir="$(resolve_dir "$d")"
|
|
219
|
+
case "$dir" in
|
|
220
|
+
"$WT_ROOT"/*)
|
|
221
|
+
rm -rf "$dir" 2>/dev/null && { removed_wt=$((removed_wt + 1)); echo "→ removed orphan worktree dir ${dir#"$MAIN_WT"/}"; }
|
|
222
|
+
;;
|
|
223
|
+
*) echo "purge: skipping orphan outside $WT_ROOT: $d" >&2 ;;
|
|
224
|
+
esac
|
|
225
|
+
done
|
|
226
|
+
|
|
227
|
+
if [ -n "$COUNTER" ]; then
|
|
228
|
+
rm -f "$COUNTER" 2>/dev/null && echo "→ reset task counter $COUNTER"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# Drop the now-empty .worktrees/ shell (only when truly empty).
|
|
232
|
+
[ -d "$WT_ROOT" ] && rmdir "$WT_ROOT" 2>/dev/null
|
|
233
|
+
|
|
234
|
+
echo "══ purge: removed $removed_wt worktree(s) and $removed_br local branch(es) in $MAIN_WT ══"
|
|
235
|
+
exit 0
|
|
@@ -32,6 +32,8 @@ done
|
|
|
32
32
|
|
|
33
33
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
34
34
|
COST_TABLE="$ROOT/pipeline/scripts/cost-table.json"
|
|
35
|
+
COST_LIB="$ROOT/pipeline/scripts/cost-lib.sh"
|
|
36
|
+
if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
|
|
35
37
|
|
|
36
38
|
command -v jq >/dev/null 2>&1 || { echo "render-agent-log-cost: jq required" >&2; exit 2; }
|
|
37
39
|
|
|
@@ -94,27 +96,19 @@ if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
|
|
|
94
96
|
' "$SPANS_FILE")
|
|
95
97
|
fi
|
|
96
98
|
|
|
97
|
-
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" '
|
|
99
|
+
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" "$COST_JQ_DEFS"'
|
|
98
100
|
$rows | map(
|
|
99
101
|
. as $r |
|
|
100
102
|
($prices[0].prices[$r.model] // null) as $p |
|
|
101
103
|
# tokens_in is freshly-billed input (cache-exclusive, matching the host
|
|
102
104
|
# input_tokens field); tokens_cached is the separate prompt-cache-read
|
|
103
|
-
# count, billed at the discounted cacheReadPerMtok rate
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# renderer and aggregate-metrics.mjs agree on what "total input" means.
|
|
107
|
-
$r.tokens_in as $fresh |
|
|
105
|
+
# count, billed at the discounted cacheReadPerMtok rate. The two counts are
|
|
106
|
+
# disjoint - never subtract one from the other - so this renderer and
|
|
107
|
+
# aggregate-metrics.mjs agree on what "total input" means.
|
|
108
108
|
($r.tokens_cached // 0) as $cached |
|
|
109
109
|
. + {
|
|
110
110
|
tokens_cached: $cached,
|
|
111
|
-
usd: (
|
|
112
|
-
if $p == null then null
|
|
113
|
-
else ( ($fresh / 1000000) * $p.inPerMtok
|
|
114
|
-
+ ($cached / 1000000) * ($p.cacheReadPerMtok // $p.inPerMtok)
|
|
115
|
-
+ ($r.tokens_out / 1000000) * $p.outPerMtok )
|
|
116
|
-
end
|
|
117
|
-
)
|
|
111
|
+
usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; $cached)
|
|
118
112
|
}
|
|
119
113
|
)
|
|
120
114
|
')
|
|
@@ -26,6 +26,8 @@ done
|
|
|
26
26
|
|
|
27
27
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
28
28
|
COST_TABLE="$ROOT/pipeline/scripts/cost-table.json"
|
|
29
|
+
COST_LIB="$ROOT/pipeline/scripts/cost-lib.sh"
|
|
30
|
+
if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
|
|
29
31
|
|
|
30
32
|
command -v jq >/dev/null 2>&1 || { echo "render-cost-summary: jq required" >&2; exit 2; }
|
|
31
33
|
|
|
@@ -78,17 +80,12 @@ if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
|
|
|
78
80
|
fi
|
|
79
81
|
|
|
80
82
|
# Compute USD per row using cost-table
|
|
81
|
-
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" '
|
|
83
|
+
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" "$COST_JQ_DEFS"'
|
|
82
84
|
$rows | map(
|
|
83
85
|
. as $r |
|
|
84
86
|
($prices[0].prices[$r.model] // null) as $p |
|
|
85
87
|
. + {
|
|
86
|
-
usd: (
|
|
87
|
-
if $p == null then null
|
|
88
|
-
else ( ($r.tokens_in / 1000000) * $p.inPerMtok
|
|
89
|
-
+ ($r.tokens_out / 1000000) * $p.outPerMtok )
|
|
90
|
-
end
|
|
91
|
-
)
|
|
88
|
+
usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; 0)
|
|
92
89
|
}
|
|
93
90
|
)
|
|
94
91
|
')
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
38
38
|
import { join, dirname } from "node:path";
|
|
39
39
|
import { fileURLToPath } from "node:url";
|
|
40
|
+
import { costUsd } from "./_cost.mjs";
|
|
40
41
|
|
|
41
42
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
43
|
|
|
@@ -135,9 +136,7 @@ function computeCost(modelKey, tokensIn, tokensOut, costTable) {
|
|
|
135
136
|
if (!price || (typeof price.inPerMtok !== "number" && typeof price.outPerMtok !== "number")) {
|
|
136
137
|
return { cost_usd: 0, cost_unknown_model: true };
|
|
137
138
|
}
|
|
138
|
-
const usd =
|
|
139
|
-
((tokensIn || 0) / 1_000_000) * (price.inPerMtok || 0) +
|
|
140
|
-
((tokensOut || 0) / 1_000_000) * (price.outPerMtok || 0);
|
|
139
|
+
const usd = costUsd(price, tokensIn, tokensOut);
|
|
141
140
|
return { cost_usd: Number(usd.toFixed(6)) };
|
|
142
141
|
}
|
|
143
142
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-description-tr.sh - localized picker-description contract.
|
|
3
|
+
#
|
|
4
|
+
# Verifies: every shipped command SKILL.md carries a description-tr line
|
|
5
|
+
# (coverage), repo description: lines stay English with no description-en
|
|
6
|
+
# sidecars committed (hygiene), and localize-commands.mjs apply/restore is
|
|
7
|
+
# an idempotent, byte-identical round trip (mechanics).
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
13
|
+
CMD_DIR="$REPO_ROOT/pipeline/commands/multi-agent"
|
|
14
|
+
LOCALIZER="$REPO_ROOT/pipeline/scripts/localize-commands.mjs"
|
|
15
|
+
|
|
16
|
+
PASS=0
|
|
17
|
+
FAIL=0
|
|
18
|
+
record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
|
|
19
|
+
record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
|
|
20
|
+
|
|
21
|
+
printf '\342\206\222 smoke-description-tr: localized picker-description contract\n'
|
|
22
|
+
|
|
23
|
+
# 1. Coverage: every command SKILL.md (incl. the dispatcher) has description-tr
|
|
24
|
+
missing=""
|
|
25
|
+
count=0
|
|
26
|
+
for f in "$CMD_DIR/SKILL.md" "$CMD_DIR"/*/SKILL.md; do
|
|
27
|
+
[ -f "$f" ] || continue
|
|
28
|
+
count=$((count + 1))
|
|
29
|
+
grep -q '^description-tr:' "$f" || missing="$missing ${f#"$CMD_DIR/"}"
|
|
30
|
+
done
|
|
31
|
+
if [ -z "$missing" ]; then
|
|
32
|
+
record_pass "all $count command SKILL.md files carry description-tr"
|
|
33
|
+
else
|
|
34
|
+
record_fail "missing description-tr in:$missing"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 2. Hygiene: repo descriptions stay English, no sidecars committed
|
|
38
|
+
if grep -rn '^description:.*[çğıöşüÇĞİÖŞÜ]' "$CMD_DIR" >/dev/null 2>&1; then
|
|
39
|
+
record_fail "repo description: lines must stay English (Turkish characters found)"
|
|
40
|
+
else
|
|
41
|
+
record_pass "repo description: lines are English"
|
|
42
|
+
fi
|
|
43
|
+
if grep -rln '^description-en:' "$CMD_DIR" >/dev/null 2>&1; then
|
|
44
|
+
record_fail "description-en sidecar committed to the repo (installed-tree artifact)"
|
|
45
|
+
else
|
|
46
|
+
record_pass "no description-en sidecar in the repo"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# 3. Translation length within the agent-skills description budget
|
|
50
|
+
long=$(awk '/^description-tr:/ && length($0) > 1044 { print FILENAME }' "$CMD_DIR"/*/SKILL.md "$CMD_DIR/SKILL.md" 2>/dev/null | head -1)
|
|
51
|
+
if [ -z "$long" ]; then
|
|
52
|
+
record_pass "every description-tr is within the 1024-char budget"
|
|
53
|
+
else
|
|
54
|
+
record_fail "description-tr exceeds budget in $long"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# 4. Round trip: apply is idempotent, restore is byte-identical
|
|
58
|
+
tmpdir=$(mktemp -d)
|
|
59
|
+
trap 'rm -rf "$tmpdir"' EXIT
|
|
60
|
+
cp -R "$CMD_DIR" "$tmpdir/cmds"
|
|
61
|
+
node "$LOCALIZER" apply --dir "$tmpdir/cmds" >/dev/null 2>&1
|
|
62
|
+
if grep -q '^description:.*[çğıöşüÇĞİÖŞÜ]' "$tmpdir/cmds/dev/SKILL.md" \
|
|
63
|
+
&& grep -q '^description-en:' "$tmpdir/cmds/dev/SKILL.md"; then
|
|
64
|
+
record_pass "apply swaps description and writes the description-en sidecar"
|
|
65
|
+
else
|
|
66
|
+
record_fail "apply did not localize the sample file"
|
|
67
|
+
fi
|
|
68
|
+
second=$(node "$LOCALIZER" apply --dir "$tmpdir/cmds" 2>/dev/null)
|
|
69
|
+
if printf '%s' "$second" | grep -q ' 0 file(s) changed'; then
|
|
70
|
+
record_pass "second apply is a no-op (idempotent)"
|
|
71
|
+
else
|
|
72
|
+
record_fail "second apply changed files again: $second"
|
|
73
|
+
fi
|
|
74
|
+
node "$LOCALIZER" restore --dir "$tmpdir/cmds" >/dev/null 2>&1
|
|
75
|
+
if diff -r "$CMD_DIR" "$tmpdir/cmds" >/dev/null 2>&1; then
|
|
76
|
+
record_pass "restore returns a byte-identical tree"
|
|
77
|
+
else
|
|
78
|
+
record_fail "restore left differences vs the original tree"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
printf '\n\342\225\220\342\225\220 smoke-description-tr: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
|
|
82
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# smoke-gc-tmp.sh - gc-tmp.sh sweeps only known scratch prefixes, is dry-run
|
|
3
|
-
# by default,
|
|
3
|
+
# by default, never touches unrelated files, refuses unsafe scratch roots,
|
|
4
|
+
# never deletes the root itself, and spares fresh scratch by default.
|
|
4
5
|
#
|
|
5
6
|
# Exit 0 = all pass, 1 = any failure.
|
|
6
7
|
|
|
@@ -40,7 +41,7 @@ seed() {
|
|
|
40
41
|
echo "→ 1. dry-run lists matches and deletes nothing"
|
|
41
42
|
seed
|
|
42
43
|
before=$(find "$SB" -mindepth 1 | wc -l | tr -d ' ')
|
|
43
|
-
out=$(GC_TMP_ROOT="$SB" bash "$GC" 2>&1)
|
|
44
|
+
out=$(GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" 2>&1)
|
|
44
45
|
after=$(find "$SB" -mindepth 1 | wc -l | tr -d ' ')
|
|
45
46
|
echo "$out" | grep -q "DRY-RUN" && pass "dry-run banner shown" || fail "dry-run banner missing"
|
|
46
47
|
[ "$before" = "$after" ] && pass "dry-run deleted nothing ($after entries intact)" || fail "dry-run changed the tree ($before -> $after)"
|
|
@@ -49,7 +50,7 @@ echo "$out" | grep -q "unrelated-file.txt" && fail "dry-run matched an unrelated
|
|
|
49
50
|
echo ""
|
|
50
51
|
echo "→ 2. --yes removes only matched scratch"
|
|
51
52
|
seed
|
|
52
|
-
GC_TMP_ROOT="$SB" bash "$GC" --yes >/dev/null 2>&1
|
|
53
|
+
GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" --yes >/dev/null 2>&1
|
|
53
54
|
[ -f "$SB/unrelated-file.txt" ] && pass "unrelated file preserved" || fail "unrelated file was deleted"
|
|
54
55
|
[ ! -e "$SB/multi-agent-picker-PROJ-1.json" ] && pass "multi-agent-* file removed" || fail "multi-agent-* file survived"
|
|
55
56
|
[ ! -e "$SB/multi-agent-review-PROJ-1-x" ] && pass "scratch dir removed" || fail "scratch dir survived"
|
|
@@ -61,7 +62,7 @@ remaining=$(find "$SB" -mindepth 1 | wc -l | tr -d ' ')
|
|
|
61
62
|
echo ""
|
|
62
63
|
echo "→ 3. --older-than excludes fresh scratch"
|
|
63
64
|
seed
|
|
64
|
-
out=$(GC_TMP_ROOT="$SB" bash "$GC" --older-than=1 2>&1)
|
|
65
|
+
out=$(GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" --older-than=1 2>&1)
|
|
65
66
|
echo "$out" | grep -q "nothing to do" && pass "freshly-created scratch excluded by --older-than=1" || fail "--older-than did not exclude fresh files"
|
|
66
67
|
|
|
67
68
|
echo ""
|
|
@@ -79,6 +80,51 @@ rc=$?
|
|
|
79
80
|
rm -rf "$EMPTY"
|
|
80
81
|
{ [ "$rc" = "0" ] && echo "$out" | grep -q "nothing to do"; } && pass "empty root exits 0 with no-op message" || fail "empty root not a clean no-op"
|
|
81
82
|
|
|
83
|
+
echo ""
|
|
84
|
+
echo "→ 6. unsafe scratch roots are refused (exit 2)"
|
|
85
|
+
GC_TMP_ROOT="$HOME" bash "$GC" >/dev/null 2>&1
|
|
86
|
+
[ "$?" = "2" ] && pass "\$HOME as scratch root refused" || fail "\$HOME as scratch root not refused"
|
|
87
|
+
GC_TMP_ROOT="/" bash "$GC" >/dev/null 2>&1
|
|
88
|
+
[ "$?" = "2" ] && pass "/ as scratch root refused" || fail "/ as scratch root not refused"
|
|
89
|
+
GITROOT="$(mktemp -d)"
|
|
90
|
+
mkdir -p "$GITROOT/.git"
|
|
91
|
+
touch "$GITROOT/multi-agent-bait.json"
|
|
92
|
+
GC_TMP_ROOT="$GITROOT" bash "$GC" --yes >/dev/null 2>&1
|
|
93
|
+
rc=$?
|
|
94
|
+
[ "$rc" = "2" ] && pass "git work tree as scratch root refused" || fail "git work tree as scratch root not refused"
|
|
95
|
+
[ -f "$GITROOT/multi-agent-bait.json" ] && pass "bait file inside refused root untouched" || fail "refused root was still swept"
|
|
96
|
+
rm -rf "$GITROOT"
|
|
97
|
+
|
|
98
|
+
echo ""
|
|
99
|
+
echo "→ 7. a root itself named like scratch is never deleted"
|
|
100
|
+
SR="$(mktemp -d)"
|
|
101
|
+
mkdir -p "$SR/multi-agent-root"
|
|
102
|
+
touch "$SR/multi-agent-root/multi-agent-inner.json"
|
|
103
|
+
GC_TMP_ROOT="$SR/multi-agent-root" GC_TMP_GRACE_MIN=0 bash "$GC" --yes >/dev/null 2>&1
|
|
104
|
+
[ -d "$SR/multi-agent-root" ] && pass "matching-named root survives --yes" || fail "root dir itself was deleted"
|
|
105
|
+
[ ! -e "$SR/multi-agent-root/multi-agent-inner.json" ] && pass "inner scratch still removed" || fail "inner scratch survived"
|
|
106
|
+
rm -rf "$SR"
|
|
107
|
+
|
|
108
|
+
echo ""
|
|
109
|
+
echo "→ 8. symlinked scratch is unlinked without following it"
|
|
110
|
+
seed
|
|
111
|
+
VICTIM="$(mktemp -d)"
|
|
112
|
+
echo keep >"$VICTIM/keep.txt"
|
|
113
|
+
ln -s "$VICTIM" "$SB/multi-agent-linked"
|
|
114
|
+
GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" --yes >/dev/null 2>&1
|
|
115
|
+
[ ! -e "$SB/multi-agent-linked" ] && pass "symlink itself removed" || fail "symlink survived"
|
|
116
|
+
[ -f "$VICTIM/keep.txt" ] && pass "symlink target untouched" || fail "symlink target was deleted"
|
|
117
|
+
rm -rf "$VICTIM"
|
|
118
|
+
|
|
119
|
+
echo ""
|
|
120
|
+
echo "→ 9. default grace spares fresh scratch"
|
|
121
|
+
seed
|
|
122
|
+
out=$(GC_TMP_ROOT="$SB" bash "$GC" --yes 2>&1)
|
|
123
|
+
rc=$?
|
|
124
|
+
[ "$rc" = "0" ] && pass "grace run exits 0" || fail "grace run exited $rc"
|
|
125
|
+
[ -f "$SB/multi-agent-picker-PROJ-1.json" ] && pass "fresh scratch spared by default grace" || fail "default grace deleted fresh scratch"
|
|
126
|
+
echo "$out" | grep -qi "spared" && pass "output mentions spared fresh items" || fail "no spared note in output"
|
|
127
|
+
|
|
82
128
|
echo ""
|
|
83
129
|
echo "══ gc-tmp smoke: $PASS passed, $FAIL failed ══"
|
|
84
130
|
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-gc-worktrees.sh - contract of the worktree residue sweeper.
|
|
3
|
+
#
|
|
4
|
+
# Verifies gc-worktrees.sh: dry-run-by-default, orphan-dir removal only with
|
|
5
|
+
# --yes, registered worktrees untouched, gitlink residue unstaged, exclude
|
|
6
|
+
# guard added idempotently, --older-than sparing, non-repo no-op, and flag
|
|
7
|
+
# validation.
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
SCRIPT="$SCRIPT_DIR/gc-worktrees.sh"
|
|
13
|
+
|
|
14
|
+
PASS=0
|
|
15
|
+
FAIL=0
|
|
16
|
+
record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
|
|
17
|
+
record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
|
|
18
|
+
|
|
19
|
+
printf '\342\206\222 smoke-gc-worktrees: worktree residue sweep contract\n'
|
|
20
|
+
|
|
21
|
+
tmpdir=$(mktemp -d)
|
|
22
|
+
trap 'rm -rf "$tmpdir"' EXIT
|
|
23
|
+
|
|
24
|
+
# Fixture repo: one registered worktree, one orphan dir, one gitlink residue.
|
|
25
|
+
REPO="$tmpdir/repo"
|
|
26
|
+
mkdir -p "$REPO"
|
|
27
|
+
git -C "$REPO" init -q -b main
|
|
28
|
+
git -C "$REPO" config user.email smoke@test
|
|
29
|
+
git -C "$REPO" config user.name smoke
|
|
30
|
+
echo hi > "$REPO/f.txt"
|
|
31
|
+
git -C "$REPO" add f.txt
|
|
32
|
+
git -C "$REPO" commit -qm init
|
|
33
|
+
git -C "$REPO" worktree add -q "$REPO/.worktrees/REG" -b reg-branch
|
|
34
|
+
mkdir -p "$REPO/.worktrees/ORPHAN"
|
|
35
|
+
echo x > "$REPO/.worktrees/ORPHAN/file"
|
|
36
|
+
git -C "$REPO" update-index --add --cacheinfo "160000,$(git -C "$REPO" rev-parse HEAD),.worktrees/GONE"
|
|
37
|
+
|
|
38
|
+
# 1. Script parses
|
|
39
|
+
if bash -n "$SCRIPT" 2>/dev/null; then
|
|
40
|
+
record_pass "gc-worktrees.sh parses (bash -n)"
|
|
41
|
+
else
|
|
42
|
+
record_fail "gc-worktrees.sh has syntax errors"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# 2. Dry-run lists all three findings and deletes nothing
|
|
46
|
+
out=$(bash "$SCRIPT" --repo "$REPO" 2>&1); rc=$?
|
|
47
|
+
if [ "$rc" -eq 0 ] \
|
|
48
|
+
&& printf '%s' "$out" | grep -q "would remove orphan worktree dir: .worktrees/ORPHAN" \
|
|
49
|
+
&& printf '%s' "$out" | grep -q "would unstage gitlink residue from the index: .worktrees/GONE" \
|
|
50
|
+
&& printf '%s' "$out" | grep -q "would add .worktrees/ to .git/info/exclude"; then
|
|
51
|
+
record_pass "dry-run lists orphan dir + gitlink + missing guard"
|
|
52
|
+
else
|
|
53
|
+
record_fail "dry-run should list all three findings (rc=$rc): $out"
|
|
54
|
+
fi
|
|
55
|
+
if [ -d "$REPO/.worktrees/ORPHAN" ] && git -C "$REPO" ls-files -s -- .worktrees | grep -q 160000; then
|
|
56
|
+
record_pass "dry-run changed nothing"
|
|
57
|
+
else
|
|
58
|
+
record_fail "dry-run must not delete or unstage anything"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# 3. --older-than spares the freshly-touched orphan
|
|
62
|
+
out=$(bash "$SCRIPT" --repo "$REPO" --older-than=60 2>&1)
|
|
63
|
+
if printf '%s' "$out" | grep -q "would remove orphan"; then
|
|
64
|
+
record_fail "--older-than=60 should spare the fresh orphan dir"
|
|
65
|
+
else
|
|
66
|
+
record_pass "--older-than spares recently-touched orphan dirs"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# 4. --yes applies all three
|
|
70
|
+
out=$(bash "$SCRIPT" --repo "$REPO" --yes 2>&1); rc=$?
|
|
71
|
+
if [ "$rc" -eq 0 ] && [ ! -d "$REPO/.worktrees/ORPHAN" ]; then
|
|
72
|
+
record_pass "--yes removes the orphan dir"
|
|
73
|
+
else
|
|
74
|
+
record_fail "--yes should remove the orphan dir (rc=$rc): $out"
|
|
75
|
+
fi
|
|
76
|
+
if git -C "$REPO" ls-files -s -- .worktrees | grep -q 160000; then
|
|
77
|
+
record_fail "--yes should unstage the gitlink residue"
|
|
78
|
+
else
|
|
79
|
+
record_pass "--yes unstages the gitlink residue"
|
|
80
|
+
fi
|
|
81
|
+
if [ "$(grep -cxF '.worktrees/' "$REPO/.git/info/exclude" 2>/dev/null)" = "1" ]; then
|
|
82
|
+
record_pass "--yes adds the exclude guard exactly once"
|
|
83
|
+
else
|
|
84
|
+
record_fail "exclude guard missing or duplicated"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# 5. Registered worktree untouched
|
|
88
|
+
if [ -d "$REPO/.worktrees/REG" ] && git -C "$REPO" worktree list | grep -q "REG"; then
|
|
89
|
+
record_pass "registered worktree left untouched"
|
|
90
|
+
else
|
|
91
|
+
record_fail "registered worktree must never be touched"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# 6. Re-run -> nothing to do, guard not duplicated
|
|
95
|
+
out=$(bash "$SCRIPT" --repo "$REPO" 2>&1); rc=$?
|
|
96
|
+
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q "nothing to do"; then
|
|
97
|
+
record_pass "clean repo -> nothing to do (exit 0)"
|
|
98
|
+
else
|
|
99
|
+
record_fail "clean repo should be a no-op (rc=$rc): $out"
|
|
100
|
+
fi
|
|
101
|
+
bash "$SCRIPT" --repo "$REPO" --yes >/dev/null 2>&1
|
|
102
|
+
if [ "$(grep -cxF '.worktrees/' "$REPO/.git/info/exclude")" = "1" ]; then
|
|
103
|
+
record_pass "exclude guard is idempotent across --yes reruns"
|
|
104
|
+
else
|
|
105
|
+
record_fail "exclude guard duplicated on rerun"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# 7. Non-repo -> silent no-op exit 0
|
|
109
|
+
out=$(bash "$SCRIPT" --repo "$tmpdir" 2>&1); rc=$?
|
|
110
|
+
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q "not a git work tree"; then
|
|
111
|
+
record_pass "non-repo -> exit 0 no-op"
|
|
112
|
+
else
|
|
113
|
+
record_fail "non-repo should exit 0 with a no-op message (rc=$rc)"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# 8. Unknown flag -> usage error exit 2
|
|
117
|
+
bash "$SCRIPT" --definitely-not-a-flag >/dev/null 2>&1; rc=$?
|
|
118
|
+
if [ "$rc" -eq 2 ]; then
|
|
119
|
+
record_pass "unknown flag -> exit 2"
|
|
120
|
+
else
|
|
121
|
+
record_fail "unknown flag should exit 2 (got $rc)"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
printf '\n\342\225\220\342\225\220 smoke-gc-worktrees: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
|
|
125
|
+
[ "$FAIL" -eq 0 ]
|