@mmerterden/multi-agent-pipeline 11.5.0 → 12.0.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 +54 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/install/claude.mjs +25 -1
- package/install/index.mjs +34 -1
- package/package.json +1 -1
- 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/multi-agent-refs/cross-cli-contract.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
- 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/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- 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-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
|
@@ -7,17 +7,26 @@
|
|
|
7
7
|
# These accumulate forever. This prunes whole task dirs, with optional age /
|
|
8
8
|
# project / task filters.
|
|
9
9
|
#
|
|
10
|
-
# PRESERVED ALWAYS: the audit trail (audit.jsonl
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
10
|
+
# PRESERVED ALWAYS: the audit trail (audit.jsonl + rotations), the metrics
|
|
11
|
+
# corpus (metrics.jsonl, metrics-summary.md), and .counter files (per-project
|
|
12
|
+
# task numbering). They live outside task dirs, and as a second line of
|
|
13
|
+
# defense any directory carrying one of those reserved names is skipped even
|
|
14
|
+
# if it looks like a task dir.
|
|
14
15
|
#
|
|
15
|
-
# SAFE BY DEFAULT:
|
|
16
|
-
#
|
|
16
|
+
# SAFE BY DEFAULT:
|
|
17
|
+
# - dry-run: lists what WOULD be removed + space freed; deletes nothing
|
|
18
|
+
# until you pass --yes.
|
|
19
|
+
# - root guard: the log root must be an existing directory and is refused
|
|
20
|
+
# when it resolves to /, $HOME, or a git work tree (a repo checkout is
|
|
21
|
+
# never a log root).
|
|
22
|
+
# - active-task grace: task dirs with files modified in the last
|
|
23
|
+
# PRUNE_GRACE_MIN minutes (default 10) are spared, so pruning while a
|
|
24
|
+
# run is still writing its log dir cannot break resume. An explicit
|
|
25
|
+
# --task=ID bypasses the grace; PRUNE_GRACE_MIN=0 disables it.
|
|
17
26
|
#
|
|
18
27
|
# Usage:
|
|
19
|
-
# prune-logs.sh # dry-run: every task dir
|
|
20
|
-
# prune-logs.sh --yes # delete every task dir
|
|
28
|
+
# prune-logs.sh # dry-run: every idle task dir
|
|
29
|
+
# prune-logs.sh --yes # delete every idle task dir
|
|
21
30
|
# prune-logs.sh --older-than=30 # only task dirs older than 30 days
|
|
22
31
|
# prune-logs.sh --project=my-ios-app # only one project's task dirs
|
|
23
32
|
# prune-logs.sh --task=PROJ-123 # only one task dir
|
|
@@ -25,12 +34,14 @@
|
|
|
25
34
|
#
|
|
26
35
|
# Env:
|
|
27
36
|
# PRUNE_LOG_ROOT override the log root (default $HOME/.claude/logs/multi-agent)
|
|
37
|
+
# PRUNE_GRACE_MIN active-task grace window in minutes (default 10, 0 = off)
|
|
28
38
|
#
|
|
29
|
-
# Exit: 0 on success, 2 on usage error.
|
|
39
|
+
# Exit: 0 on success, 2 on usage error or refused log root.
|
|
30
40
|
|
|
31
41
|
set -uo pipefail
|
|
32
42
|
|
|
33
43
|
LOG_ROOT="${PRUNE_LOG_ROOT:-$HOME/.claude/logs/multi-agent}"
|
|
44
|
+
GRACE_MIN="${PRUNE_GRACE_MIN:-10}"
|
|
34
45
|
DELETE=0
|
|
35
46
|
OLDER_DAYS=0
|
|
36
47
|
PROJECT=""
|
|
@@ -40,6 +51,13 @@ usage() {
|
|
|
40
51
|
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
# --project / --task values are compared against path components; only plain
|
|
55
|
+
# names are meaningful. Rejects empty, ".", "..", anything with a slash, and
|
|
56
|
+
# anything starting with "." or "-".
|
|
57
|
+
valid_id() {
|
|
58
|
+
printf '%s' "$1" | grep -qE '^[A-Za-z0-9][A-Za-z0-9._-]*$'
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
for arg in "$@"; do
|
|
44
62
|
case "$arg" in
|
|
45
63
|
--yes | --force) DELETE=1 ;;
|
|
@@ -50,8 +68,20 @@ for arg in "$@"; do
|
|
|
50
68
|
exit 2
|
|
51
69
|
fi
|
|
52
70
|
;;
|
|
53
|
-
--project=*)
|
|
54
|
-
|
|
71
|
+
--project=*)
|
|
72
|
+
PROJECT="${arg#*=}"
|
|
73
|
+
if ! valid_id "$PROJECT"; then
|
|
74
|
+
echo "prune-logs: --project needs a plain directory name (letters/digits then . _ -), got: '$PROJECT'" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
fi
|
|
77
|
+
;;
|
|
78
|
+
--task=*)
|
|
79
|
+
TASK="${arg#*=}"
|
|
80
|
+
if ! valid_id "$TASK"; then
|
|
81
|
+
echo "prune-logs: --task needs a plain directory name (letters/digits then . _ -), got: '$TASK'" >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
;;
|
|
55
85
|
-h | --help)
|
|
56
86
|
usage
|
|
57
87
|
exit 0
|
|
@@ -63,11 +93,29 @@ for arg in "$@"; do
|
|
|
63
93
|
esac
|
|
64
94
|
done
|
|
65
95
|
|
|
96
|
+
if ! printf '%s' "$GRACE_MIN" | grep -qE '^[0-9]+$'; then
|
|
97
|
+
echo "prune-logs: PRUNE_GRACE_MIN needs a whole number of minutes, got: $GRACE_MIN" >&2
|
|
98
|
+
exit 2
|
|
99
|
+
fi
|
|
100
|
+
|
|
66
101
|
if [ ! -d "$LOG_ROOT" ]; then
|
|
67
102
|
echo "prune-logs: no log root at $LOG_ROOT - nothing to do"
|
|
68
103
|
exit 0
|
|
69
104
|
fi
|
|
70
105
|
|
|
106
|
+
# Root guard: resolve symlinks, then refuse roots that can never be a log
|
|
107
|
+
# root. Everything removed below sits strictly inside this resolved path.
|
|
108
|
+
LOG_ROOT="$(cd "$LOG_ROOT" 2>/dev/null && pwd -P)"
|
|
109
|
+
if [ -z "$LOG_ROOT" ]; then
|
|
110
|
+
echo "prune-logs: cannot resolve log root" >&2
|
|
111
|
+
exit 2
|
|
112
|
+
fi
|
|
113
|
+
HOME_REAL="$(cd "$HOME" 2>/dev/null && pwd -P || printf '%s' "$HOME")"
|
|
114
|
+
if [ "$LOG_ROOT" = "/" ] || [ "$LOG_ROOT" = "$HOME_REAL" ] || [ -e "$LOG_ROOT/.git" ]; then
|
|
115
|
+
echo "prune-logs: refusing log root $LOG_ROOT (/, \$HOME, or a git work tree is never a log root)" >&2
|
|
116
|
+
exit 2
|
|
117
|
+
fi
|
|
118
|
+
|
|
71
119
|
# A task dir is any dir (depth 1-2 under the root) carrying a run marker.
|
|
72
120
|
is_task_dir() {
|
|
73
121
|
local d="$1"
|
|
@@ -75,23 +123,47 @@ is_task_dir() {
|
|
|
75
123
|
[ -f "$d/agent-log.md" ] || [ -f "$d/otel-spans.jsonl" ]
|
|
76
124
|
}
|
|
77
125
|
|
|
126
|
+
# Names promised to survive pruning. A directory carrying one of these names
|
|
127
|
+
# is never a legitimate task dir, so it is skipped outright.
|
|
128
|
+
is_reserved_name() {
|
|
129
|
+
case "$1" in
|
|
130
|
+
audit.jsonl | audit.*.jsonl* | audit.jsonl.* | metrics.jsonl | metrics-summary.md | .counter | *.counter) return 0 ;;
|
|
131
|
+
esac
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
134
|
+
|
|
78
135
|
MTIME_ARGS=()
|
|
79
136
|
if [ "$OLDER_DAYS" -gt 0 ]; then
|
|
80
137
|
MTIME_ARGS=(-mtime "+$OLDER_DAYS")
|
|
81
138
|
fi
|
|
82
139
|
|
|
83
140
|
matches=()
|
|
141
|
+
skipped_active=0
|
|
84
142
|
while IFS= read -r -d '' d; do
|
|
143
|
+
base="$(basename "$d")"
|
|
144
|
+
if is_reserved_name "$base"; then continue; fi
|
|
85
145
|
is_task_dir "$d" || continue
|
|
86
146
|
# --task: dir name must equal the task id
|
|
87
|
-
if [ -n "$TASK" ] && [ "$
|
|
147
|
+
if [ -n "$TASK" ] && [ "$base" != "$TASK" ]; then continue; fi
|
|
88
148
|
# --project: the immediate parent dir name must equal the project
|
|
89
149
|
if [ -n "$PROJECT" ] && [ "$(basename "$(dirname "$d")")" != "$PROJECT" ]; then continue; fi
|
|
150
|
+
# Active-task grace: spare dirs a run touched in the last GRACE_MIN minutes
|
|
151
|
+
# so a concurrent run keeps a resumable log dir. --task is an explicit,
|
|
152
|
+
# deliberate target and bypasses the grace.
|
|
153
|
+
if [ "$GRACE_MIN" -gt 0 ] && [ -z "$TASK" ] &&
|
|
154
|
+
find "$d" -mmin "-$GRACE_MIN" -print 2>/dev/null | head -1 | grep -q .; then
|
|
155
|
+
skipped_active=$((skipped_active + 1))
|
|
156
|
+
continue
|
|
157
|
+
fi
|
|
90
158
|
matches+=("$d")
|
|
91
159
|
done < <(find "$LOG_ROOT" -mindepth 1 -maxdepth 2 -type d ${MTIME_ARGS[@]+"${MTIME_ARGS[@]}"} -print0 2>/dev/null)
|
|
92
160
|
|
|
93
161
|
if [ "${#matches[@]}" -eq 0 ]; then
|
|
94
|
-
|
|
162
|
+
if [ "$skipped_active" -gt 0 ]; then
|
|
163
|
+
echo "prune-logs: only active task dirs matched (touched in the last ${GRACE_MIN}m) - spared, nothing to do"
|
|
164
|
+
else
|
|
165
|
+
echo "prune-logs: no task logs matched under $LOG_ROOT - nothing to do"
|
|
166
|
+
fi
|
|
95
167
|
exit 0
|
|
96
168
|
fi
|
|
97
169
|
|
|
@@ -106,13 +178,26 @@ human="${total_kb} KB"
|
|
|
106
178
|
if [ "$DELETE" -eq 0 ]; then
|
|
107
179
|
echo "DRY-RUN - would remove ${#matches[@]} task log dir(s) (frees ~${human}):"
|
|
108
180
|
for d in "${matches[@]}"; do echo " ${d#"$LOG_ROOT"/}"; done
|
|
181
|
+
[ "$skipped_active" -gt 0 ] && echo "Spared $skipped_active active dir(s) touched in the last ${GRACE_MIN}m."
|
|
109
182
|
echo "Audit trail + metrics corpus are preserved. Re-run with --yes to delete."
|
|
110
183
|
exit 0
|
|
111
184
|
fi
|
|
112
185
|
|
|
113
186
|
removed=0
|
|
114
187
|
for d in "${matches[@]}"; do
|
|
115
|
-
|
|
188
|
+
# Containment guard: resolve the target and require it strictly inside the
|
|
189
|
+
# resolved log root before rm -rf.
|
|
190
|
+
target="$(cd "$d" 2>/dev/null && pwd -P)" || continue
|
|
191
|
+
case "$target" in
|
|
192
|
+
"$LOG_ROOT"/*) ;;
|
|
193
|
+
*)
|
|
194
|
+
echo "prune-logs: skipping path outside log root: $d" >&2
|
|
195
|
+
continue
|
|
196
|
+
;;
|
|
197
|
+
esac
|
|
198
|
+
rm -rf "$target" 2>/dev/null && removed=$((removed + 1))
|
|
116
199
|
done
|
|
117
|
-
|
|
200
|
+
spared_note=""
|
|
201
|
+
[ "$skipped_active" -gt 0 ] && spared_note=", spared $skipped_active active"
|
|
202
|
+
echo "══ prune-logs: removed ${removed}/${#matches[@]} task log dir(s)${spared_note}, freed ~${human} (audit + metrics preserved) ══"
|
|
118
203
|
exit 0
|
|
@@ -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
|
|
@@ -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 ]
|