@mmerterden/multi-agent-pipeline 14.0.0 → 14.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 +85 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +2 -2
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +72 -0
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +66 -0
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +17 -4
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +4 -1
- package/pipeline/preferences-template.json +2 -1
- package/pipeline/schemas/agent-state.schema.json +13 -0
- package/pipeline/schemas/criteria-manifest.schema.json +223 -53
- package/pipeline/schemas/prefs.schema.json +5 -0
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/gc-worktrees.sh +3 -2
- package/pipeline/scripts/migrate-prefs.mjs +1 -0
- package/pipeline/scripts/render-work-summary.sh +51 -3
- package/pipeline/scripts/skill-conformance.mjs +10 -0
- package/pipeline/scripts/test-integrity-gate.mjs +10 -2
- package/pipeline/scripts/worktree-finalize.sh +299 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# worktree-finalize.sh - salvage a finished task's artefacts, then remove its
|
|
4
|
+
# worktree. Called by Phase 6 once the PR is open.
|
|
5
|
+
#
|
|
6
|
+
# The problem it solves: a task's worktree is dead weight the moment its PR
|
|
7
|
+
# exists, but people forget to remove it, so `.worktrees/` accumulates full
|
|
8
|
+
# checkouts. Removing it at PR-open is safe ONLY if the files Phase 7 and
|
|
9
|
+
# `:resume` read are moved somewhere that outlives it - hence salvage first.
|
|
10
|
+
#
|
|
11
|
+
# What it deliberately does NOT do:
|
|
12
|
+
# * NO `git checkout` of the task branch. `git worktree remove` leaves the
|
|
13
|
+
# branch as an ordinary local branch, so nothing is lost and the user's own
|
|
14
|
+
# HEAD and uncommitted work are untouched. Phase 5's flow checks the branch
|
|
15
|
+
# out on purpose (it is a test handoff); doing that here would move the
|
|
16
|
+
# user's HEAD out from under them mid-task, which is the one thing they
|
|
17
|
+
# asked not to happen.
|
|
18
|
+
# * NO `git branch -D`. The branch is the deliverable.
|
|
19
|
+
# * NO `rm -rf`. `git worktree remove` refuses on a dirty tree, which is a
|
|
20
|
+
# safety feature; `rm -rf` would silently destroy uncommitted work.
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# worktree-finalize.sh --worktree <path> --project-root <path>
|
|
24
|
+
# --task-id <id> [--project <slug>] [--branch <name>]
|
|
25
|
+
# [--logs-root <dir>] [--dry-run] [--json]
|
|
26
|
+
#
|
|
27
|
+
# Exit codes:
|
|
28
|
+
# 0 removed (or dry-run that would remove)
|
|
29
|
+
# 3 SKIPPED by a safety precondition - not an error, reason is reported
|
|
30
|
+
# 1 usage / hard error
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
WORKTREE=""; PROJECT_ROOT=""; TASK_ID=""; PROJECT=""; BRANCH=""
|
|
34
|
+
LOGS_ROOT="${HOME}/.claude/logs/multi-agent"
|
|
35
|
+
DRY=0; JSON=0
|
|
36
|
+
|
|
37
|
+
while [ $# -gt 0 ]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--worktree) WORKTREE="${2:-}"; shift 2 ;;
|
|
40
|
+
--project-root) PROJECT_ROOT="${2:-}"; shift 2 ;;
|
|
41
|
+
--task-id) TASK_ID="${2:-}"; shift 2 ;;
|
|
42
|
+
--project) PROJECT="${2:-}"; shift 2 ;;
|
|
43
|
+
--branch) BRANCH="${2:-}"; shift 2 ;;
|
|
44
|
+
--logs-root) LOGS_ROOT="${2:-}"; shift 2 ;;
|
|
45
|
+
--dry-run) DRY=1; shift ;;
|
|
46
|
+
--json) JSON=1; shift ;;
|
|
47
|
+
*) echo "usage: worktree-finalize.sh --worktree <p> --project-root <p> --task-id <id> [--project <s>] [--branch <b>] [--logs-root <d>] [--dry-run] [--json]" >&2; exit 1 ;;
|
|
48
|
+
esac
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
[ -n "$WORKTREE" ] && [ -n "$PROJECT_ROOT" ] && [ -n "$TASK_ID" ] || {
|
|
52
|
+
echo "ERR: --worktree, --project-root and --task-id are required" >&2; exit 1; }
|
|
53
|
+
|
|
54
|
+
SALVAGED=""
|
|
55
|
+
REMOVED=false
|
|
56
|
+
REASON=""
|
|
57
|
+
|
|
58
|
+
emit() {
|
|
59
|
+
if [ "$JSON" = "1" ]; then
|
|
60
|
+
# Built by node, not printf: a skip reason embeds a filename straight from
|
|
61
|
+
# `git status --porcelain`, which quotes unusual names, so a file like
|
|
62
|
+
# `"my file".swift` produced invalid JSON and the caller's jq step errored
|
|
63
|
+
# instead of recording a safe skip.
|
|
64
|
+
REMOVED="$REMOVED" REASON="$REASON" BRANCH="$BRANCH" DEST="${DEST:-}" \
|
|
65
|
+
SALVAGED_RAW="$SALVAGED" DRYF="$([ "$DRY" = 1 ] && echo true || echo false)" \
|
|
66
|
+
node -e '
|
|
67
|
+
const s = process.env.SALVAGED_RAW || "";
|
|
68
|
+
const salvaged = s ? s.split(",").map((x) => x.replace(/^"|"$/g, "")) : [];
|
|
69
|
+
process.stdout.write(JSON.stringify({
|
|
70
|
+
removed: process.env.REMOVED === "true",
|
|
71
|
+
reason: process.env.REASON || "",
|
|
72
|
+
salvaged,
|
|
73
|
+
branch: process.env.BRANCH || "",
|
|
74
|
+
artifactsPath: process.env.DEST || "",
|
|
75
|
+
dryRun: process.env.DRYF === "true",
|
|
76
|
+
}) + "\n");
|
|
77
|
+
' 2>/dev/null || printf '{"removed":false,"reason":"emit failed","salvaged":[],"branch":"","artifactsPath":"","dryRun":false}\n'
|
|
78
|
+
else
|
|
79
|
+
if [ "$REMOVED" = "true" ]; then
|
|
80
|
+
echo "worktree-finalize: removed $WORKTREE"
|
|
81
|
+
echo " branch kept locally: ${BRANCH:-<unknown>} (no checkout performed)"
|
|
82
|
+
[ -n "${DEST:-}" ] && echo " artefacts: $DEST"
|
|
83
|
+
else
|
|
84
|
+
echo "worktree-finalize: SKIPPED - $REASON"
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
skip() { REASON="$1"; emit; exit 3; }
|
|
90
|
+
|
|
91
|
+
# --- preference gate -------------------------------------------------------
|
|
92
|
+
# Read here rather than in the caller so EVERY caller honours it. Declared in
|
|
93
|
+
# prefs.schema.json as settings.worktreeAutoRemoveOnPr; a preference that is
|
|
94
|
+
# declared, migrated and documented but never consulted is worse than no
|
|
95
|
+
# preference at all, because turning it off appears to work and does nothing.
|
|
96
|
+
PREFS="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
|
|
97
|
+
if [ -f "$PREFS" ] && command -v node >/dev/null 2>&1; then
|
|
98
|
+
enabled="$(node -e '
|
|
99
|
+
const fs=require("fs");
|
|
100
|
+
try {
|
|
101
|
+
const s=(JSON.parse(fs.readFileSync(process.argv[1],"utf8")).global||{}).settings||{};
|
|
102
|
+
process.stdout.write(s.worktreeAutoRemoveOnPr === false ? "false" : "true");
|
|
103
|
+
} catch { process.stdout.write("true"); }
|
|
104
|
+
' "$PREFS" 2>/dev/null || echo true)"
|
|
105
|
+
if [ "$enabled" = "false" ]; then
|
|
106
|
+
REASON="disabled by prefs.global.settings.worktreeAutoRemoveOnPr"
|
|
107
|
+
emit; exit 3
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# --- preconditions ---------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
# `--local` mode has no worktree: worktreePath equals the project root. Removing
|
|
114
|
+
# it would delete the user's checkout.
|
|
115
|
+
rp_wt="$(cd "$WORKTREE" 2>/dev/null && pwd -P || echo "")"
|
|
116
|
+
rp_pr="$(cd "$PROJECT_ROOT" 2>/dev/null && pwd -P || echo "")"
|
|
117
|
+
[ -n "$rp_wt" ] || skip "worktree path does not exist: $WORKTREE"
|
|
118
|
+
[ -n "$rp_pr" ] || skip "project root does not exist: $PROJECT_ROOT"
|
|
119
|
+
[ "$rp_wt" != "$rp_pr" ] || skip "worktree equals project root (--local mode) - nothing to remove"
|
|
120
|
+
|
|
121
|
+
# Refuse if the caller is standing inside the tree being removed. A shell left on
|
|
122
|
+
# a deleted inode is worse than a leftover directory, and Phase 6 legitimately
|
|
123
|
+
# cd's into the worktree earlier, so this is a real case rather than a theoretical
|
|
124
|
+
# one. The caller must cd out first.
|
|
125
|
+
cwd_rp="$(pwd -P)"
|
|
126
|
+
case "$cwd_rp/" in
|
|
127
|
+
"$rp_wt"/*) skip "current directory is inside the worktree - cd to the project root first" ;;
|
|
128
|
+
esac
|
|
129
|
+
|
|
130
|
+
# Must be a worktree git actually knows about, so a mistyped path cannot delete an
|
|
131
|
+
# unrelated directory.
|
|
132
|
+
if ! git -C "$rp_pr" worktree list --porcelain 2>/dev/null \
|
|
133
|
+
| awk '/^worktree /{print substr($0,10)}' \
|
|
134
|
+
| while IFS= read -r w; do (cd "$w" 2>/dev/null && pwd -P); done \
|
|
135
|
+
| grep -qxF "$rp_wt"; then
|
|
136
|
+
skip "not a registered worktree of $rp_pr"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
[ -n "$BRANCH" ] || BRANCH="$(git -C "$rp_wt" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
140
|
+
|
|
141
|
+
# A detached worktree resolves BRANCH to the literal "HEAD", which then builds
|
|
142
|
+
# refs/remotes/origin/HEAD - the remote's default-branch symref, unrelated to this
|
|
143
|
+
# work. The pushed-HEAD check would pass against the wrong ref and the
|
|
144
|
+
# "branch is kept" guarantee would be vacuous, because no branch ref exists.
|
|
145
|
+
[ "$BRANCH" != "HEAD" ] || skip "detached HEAD - no branch would survive the removal, refusing to finalize"
|
|
146
|
+
|
|
147
|
+
# Uncommitted work is never discarded. Phase 5 states the same rule; this is the
|
|
148
|
+
# second place it has to hold, because here there is no WIP-commit step to fall
|
|
149
|
+
# back on - the PR is already open and a surprise WIP commit would not be in it.
|
|
150
|
+
#
|
|
151
|
+
# The pipeline's OWN artefacts are excluded from that judgement. They live inside
|
|
152
|
+
# the worktree and are untracked, so a raw `status --porcelain` is never empty at
|
|
153
|
+
# PR-open and the removal would never fire - the feature would look implemented
|
|
154
|
+
# and do nothing. They are also exactly the files salvaged below, so losing them
|
|
155
|
+
# from the worktree is the intent rather than a risk.
|
|
156
|
+
#
|
|
157
|
+
# Only these specific paths are forgiven. `--untracked-files=no` would have been
|
|
158
|
+
# one character cheaper and wrong: a source file the dev created but never
|
|
159
|
+
# `git add`ed is invisible to it, and that file would be destroyed silently.
|
|
160
|
+
# Matched on NAME ALONE this forgave a project's own tracked files: a repo that
|
|
161
|
+
# tracks `.build.log` or `.pipeline/` had its uncommitted modification forgiven
|
|
162
|
+
# here and the file deleted below, leaving a kept worktree with ` D .build.log`
|
|
163
|
+
# and the only copy of the change in a salvage dir nobody was told about.
|
|
164
|
+
# So the carve-out applies only to paths git reports as UNTRACKED (`??`), and the
|
|
165
|
+
# alternatives are end-anchored - unanchored, `.build.log.old` was forgiven here
|
|
166
|
+
# but not deletable below, which made every removal refuse forever.
|
|
167
|
+
ARTIFACTS_RE='^(agent-state\.json|phase-tracker\.json|triage-output\.json|\.review-diff\.txt|\.build\.log|\.test\.log)$|^\.pipeline/'
|
|
168
|
+
dirty="$(git -C "$rp_wt" status --porcelain 2>/dev/null | awk '
|
|
169
|
+
{
|
|
170
|
+
st = substr($0, 1, 2); path = substr($0, 4);
|
|
171
|
+
if (st == "??") { untracked = 1 } else { untracked = 0 }
|
|
172
|
+
print (untracked ? "U" : "T") "\t" path;
|
|
173
|
+
}' | while IFS="$(printf '\t')" read -r kind path; do
|
|
174
|
+
if [ "$kind" = "U" ] && printf '%s' "$path" | grep -Eq "$ARTIFACTS_RE"; then
|
|
175
|
+
continue # our own untracked artefact
|
|
176
|
+
fi
|
|
177
|
+
printf '%s\n' "$path"
|
|
178
|
+
done)"
|
|
179
|
+
if [ -n "$dirty" ]; then
|
|
180
|
+
first="$(printf '%s\n' "$dirty" | head -1)"
|
|
181
|
+
skip "worktree has uncommitted changes (e.g. $first) - refusing to remove"
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# HEAD must be on the remote. Removing a worktree whose commits exist nowhere else
|
|
185
|
+
# is not cleanup, it is data loss: the branch ref survives locally, but a later
|
|
186
|
+
# `branch -D` or a fresh clone loses the work.
|
|
187
|
+
# --verify --quiet, not a bare rev-parse: on a missing ref a bare rev-parse
|
|
188
|
+
# prints the ref NAME to stdout and errors, so `2>/dev/null` leaves the name in
|
|
189
|
+
# the variable and it gets compared against a sha - producing the nonsense
|
|
190
|
+
# "HEAD differs from remote (refs/rem...)" instead of "no remote counterpart".
|
|
191
|
+
head_sha="$(git -C "$rp_wt" rev-parse --verify --quiet HEAD || echo "")"
|
|
192
|
+
upstream_sha="$(git -C "$rp_wt" rev-parse --verify --quiet '@{u}' || echo "")"
|
|
193
|
+
if [ -z "$upstream_sha" ] && [ -n "$BRANCH" ]; then
|
|
194
|
+
# No upstream configured: accept a remote ref that already points at HEAD.
|
|
195
|
+
upstream_sha="$(git -C "$rp_wt" rev-parse --verify --quiet "refs/remotes/origin/$BRANCH" || echo "")"
|
|
196
|
+
fi
|
|
197
|
+
[ -n "$upstream_sha" ] || skip "branch has no remote counterpart - push before finalizing"
|
|
198
|
+
[ "$head_sha" = "$upstream_sha" ] || skip "HEAD ($(echo "$head_sha" | cut -c1-8)) differs from remote ($(echo "$upstream_sha" | cut -c1-8)) - push before finalizing"
|
|
199
|
+
|
|
200
|
+
# --- salvage ---------------------------------------------------------------
|
|
201
|
+
# Everything Phase 7, :resume, :status and :log read out of the worktree. Each is
|
|
202
|
+
# copied only if present; a task that never reached Phase 4 has no triage output
|
|
203
|
+
# and that is not an error.
|
|
204
|
+
DEST="$LOGS_ROOT/${PROJECT:+$PROJECT/}$TASK_ID/artifacts"
|
|
205
|
+
|
|
206
|
+
copy_one() {
|
|
207
|
+
src="$rp_wt/$1"
|
|
208
|
+
[ -e "$src" ] || return 0
|
|
209
|
+
if [ "$DRY" = "1" ]; then
|
|
210
|
+
SALVAGED="${SALVAGED:+$SALVAGED,}\"$1\""
|
|
211
|
+
return 0
|
|
212
|
+
fi
|
|
213
|
+
mkdir -p "$DEST/$(dirname "$1")" 2>/dev/null
|
|
214
|
+
cp -R "$src" "$DEST/$1" 2>/dev/null && SALVAGED="${SALVAGED:+$SALVAGED,}\"$1\""
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for a in agent-state.json phase-tracker.json triage-output.json \
|
|
218
|
+
.review-diff.txt .build.log .test.log .pipeline; do
|
|
219
|
+
copy_one "$a"
|
|
220
|
+
done
|
|
221
|
+
|
|
222
|
+
# --- remove ----------------------------------------------------------------
|
|
223
|
+
if [ "$DRY" = "1" ]; then
|
|
224
|
+
REMOVED=true; REASON="dry-run"; emit; exit 0
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# The artefacts are now safely in the log dir, and `git worktree remove` refuses
|
|
228
|
+
# on untracked files just as it does on modified ones. Drop exactly the paths that
|
|
229
|
+
# were salvaged - nothing else - so the removal can proceed on a genuinely clean
|
|
230
|
+
# tree. This is what keeps `--force` off the table: a refusal after this point
|
|
231
|
+
# means unexpected dirt, and forcing past unexpected dirt is how work gets lost.
|
|
232
|
+
DROPPED=""
|
|
233
|
+
for a in agent-state.json phase-tracker.json triage-output.json \
|
|
234
|
+
.review-diff.txt .build.log .test.log .pipeline; do
|
|
235
|
+
[ -e "$rp_wt/$a" ] || continue
|
|
236
|
+
case "$a" in
|
|
237
|
+
*/*|"") continue ;; # refuse anything that could escape the worktree
|
|
238
|
+
esac
|
|
239
|
+
# Tracked means it belongs to the project, not to us - even when the name
|
|
240
|
+
# collides with one of ours. Deleting it destroyed a real file in testing.
|
|
241
|
+
if git -C "$rp_wt" ls-files --error-unmatch -- "$a" >/dev/null 2>&1; then
|
|
242
|
+
continue
|
|
243
|
+
fi
|
|
244
|
+
# ${var:?} so an empty rp_wt can never expand to `rm -rf /<name>`.
|
|
245
|
+
rm -rf -- "${rp_wt:?}/$a"
|
|
246
|
+
DROPPED="${DROPPED:+$DROPPED }$a"
|
|
247
|
+
done
|
|
248
|
+
|
|
249
|
+
# Put them back if the removal is refused. The artefacts were dropped BEFORE
|
|
250
|
+
# knowing `git worktree remove` would succeed, so on the refusal path (submodule,
|
|
251
|
+
# lock, permissions) a still-live run lost its agent-state.json and
|
|
252
|
+
# phase-tracker.json from the location every consumer prefers - and a later
|
|
253
|
+
# write-state.mjs merge against that path would recreate the file holding only the
|
|
254
|
+
# patch, corrupting the run record.
|
|
255
|
+
restore_dropped() {
|
|
256
|
+
[ -n "$DROPPED" ] || return 0
|
|
257
|
+
for a in $DROPPED; do
|
|
258
|
+
[ -e "$DEST/$a" ] || continue
|
|
259
|
+
cp -R "$DEST/$a" "$rp_wt/$a" 2>/dev/null || true
|
|
260
|
+
done
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if git -C "$rp_pr" worktree remove "$rp_wt" 2>/dev/null; then
|
|
264
|
+
git -C "$rp_pr" worktree prune 2>/dev/null || true
|
|
265
|
+
REMOVED=true; REASON="removed after PR"
|
|
266
|
+
|
|
267
|
+
# Stamp the outcome into the SALVAGED agent-state.json here, rather than leaving
|
|
268
|
+
# the caller to write it. In single-repo worktree mode the caller's $STATE_FILE
|
|
269
|
+
# is the copy that just went away with the worktree, so a write there fails and
|
|
270
|
+
# `worktreeRemovedAt` / `artifactsPath` land nowhere - which then makes Phase 7
|
|
271
|
+
# read a dead path, fall back to the removed worktree, and silently skip the
|
|
272
|
+
# triage-memory ingest. Exactly the [ -f ]-guarded degradation this step exists
|
|
273
|
+
# to prevent.
|
|
274
|
+
if [ -f "$DEST/agent-state.json" ] && command -v node >/dev/null 2>&1; then
|
|
275
|
+
DEST="$DEST" BRANCH="$BRANCH" node -e '
|
|
276
|
+
const fs = require("fs");
|
|
277
|
+
const p = process.env.DEST + "/agent-state.json";
|
|
278
|
+
try {
|
|
279
|
+
const s = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
280
|
+
s.worktreeRemovedAt = new Date().toISOString();
|
|
281
|
+
s.artifactsPath = process.env.DEST;
|
|
282
|
+
s.worktreePath = null;
|
|
283
|
+
fs.writeFileSync(p, JSON.stringify(s, null, 2) + "\n");
|
|
284
|
+
} catch { /* leave the salvaged copy untouched rather than corrupt it */ }
|
|
285
|
+
' 2>/dev/null || true
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
emit
|
|
289
|
+
exit 0
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
restore_dropped
|
|
293
|
+
|
|
294
|
+
# `worktree remove` refused. Do NOT escalate to --force: the clean-tree check
|
|
295
|
+
# above already passed, so a refusal here means something unexpected (a lock, a
|
|
296
|
+
# submodule, a permission problem) and forcing would destroy whatever it is.
|
|
297
|
+
REASON="git worktree remove refused - left in place (artefacts already salvaged)"
|
|
298
|
+
emit
|
|
299
|
+
exit 3
|
|
@@ -227,10 +227,10 @@ When invoked with the `release` argument:
|
|
|
227
227
|
|
|
228
228
|
```
|
|
229
229
|
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, design-check, dev,
|
|
230
|
-
dev-autopilot, dev-local, dev-local-autopilot, diff-explain,
|
|
230
|
+
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, forget, garbage-collect,
|
|
231
231
|
help, ios-coding-standard, issue, jira, kill, language, local,
|
|
232
232
|
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
233
|
-
routines, save, scan, search, setup, stack, status, sync, test, testflight-validation, uninstall, update
|
|
233
|
+
routines, save, scan, search, setup, ship, stack, status, sync, test, testflight-validation, uninstall, update
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
**NOT synced**: `refs/*` - Lazy-load references, Claude Code specific
|