@mmerterden/multi-agent-pipeline 11.4.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 +172 -0
- package/README.md +1 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +95 -11
- package/install/copilot.mjs +75 -6
- package/install/index.mjs +34 -1
- package/package.json +4 -10
- 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 +2 -1
- 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/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -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/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/gc-tmp.sh +67 -8
- package/pipeline/scripts/gc-worktrees.sh +140 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/localize-commands.mjs +130 -0
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-description-tr.sh +82 -0
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
- package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-prune-logs.sh +57 -7
- package/pipeline/scripts/smoke-purge.sh +138 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-update-check.sh +13 -0
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +146 -42
- package/pipeline/scripts/update-check.sh +9 -0
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- 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
|
@@ -136,6 +136,18 @@ else
|
|
|
136
136
|
else
|
|
137
137
|
fail "jira-id path output unexpected"
|
|
138
138
|
fi
|
|
139
|
+
|
|
140
|
+
# Bare "#316" and "316" must land in the gh-num branch (needsRepoPicker
|
|
141
|
+
# descriptor when no default owner/repo), NOT gh-short with an empty repo
|
|
142
|
+
# prefix that builds https://github.com/<owner>//issues/316.
|
|
143
|
+
for bare in "#316" "316"; do
|
|
144
|
+
out=$(ACCOUNT_DEFAULT_OWNER="" ACCOUNT_DEFAULT_REPO="" "$FETCHER" "$bare" 2>/dev/null || echo "")
|
|
145
|
+
if printf '%s' "$out" | python3 -c 'import json,sys; d=json.loads(sys.stdin.read()); assert d["kind"]=="github", d["kind"]; assert d["key"]=="316", d["key"]; assert d.get("needsRepoPicker") is True, d' 2>/dev/null; then
|
|
146
|
+
ok "bare '$bare' classified as gh-num (key=316, needsRepoPicker)"
|
|
147
|
+
else
|
|
148
|
+
fail "bare '$bare' misclassified: $(printf '%s' "$out" | head -c 200)"
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
139
151
|
fi
|
|
140
152
|
|
|
141
153
|
# --- multi-repo-pipeline.sh: bridge --------------------------------------
|
|
@@ -294,6 +306,20 @@ assert d["projects"][1]["project"] == "repo-b"
|
|
|
294
306
|
fail "bridge output missing projects[] entries"
|
|
295
307
|
fi
|
|
296
308
|
|
|
309
|
+
# teardown guard: a worktreeRoot of "." (the current project root) must be
|
|
310
|
+
# refused loudly, never rm -rf'd.
|
|
311
|
+
UNSAFE_STATE="$FIXTURE_ROOT/unsafe-state.json"
|
|
312
|
+
sed 's|"worktreeRoot":"[^"]*"|"worktreeRoot":"."|' "$FIXTURE_ROOT/state.json" > "$UNSAFE_STATE"
|
|
313
|
+
GUARD_DIR=$(mktemp -d)
|
|
314
|
+
( cd "$GUARD_DIR" && "$MRP" teardown "$UNSAFE_STATE" ) >/dev/null 2>&1
|
|
315
|
+
guard_rc=$?
|
|
316
|
+
if [ "$guard_rc" -ne 0 ] && [ -d "$GUARD_DIR" ]; then
|
|
317
|
+
ok "teardown refuses worktreeRoot='.' (exit=$guard_rc, dir intact)"
|
|
318
|
+
else
|
|
319
|
+
fail "teardown did not refuse unsafe worktreeRoot='.' (exit=$guard_rc)"
|
|
320
|
+
fi
|
|
321
|
+
rm -rf "$GUARD_DIR"
|
|
322
|
+
|
|
297
323
|
# teardown
|
|
298
324
|
"$MRP" teardown "$FIXTURE_ROOT/state.json" >/dev/null 2>&1
|
|
299
325
|
if [ ! -d "$WORKTREE_ROOT" ]; then
|
|
@@ -357,13 +383,40 @@ EOF
|
|
|
357
383
|
|
|
358
384
|
# render-jira: bulleted PR list at top
|
|
359
385
|
rendered=$("$CMR" render-jira "$TMP/multi.json" "$TMP/body.md" 2>/dev/null)
|
|
360
|
-
bullet_count=$(printf '%s' "$rendered" | grep -c '^\* PR:' ||
|
|
386
|
+
bullet_count=$(printf '%s' "$rendered" | grep -c '^\* PR:' || true)
|
|
361
387
|
if [ "$bullet_count" = "2" ]; then
|
|
362
388
|
ok "render-jira prepends bulleted PR list (2 entries)"
|
|
363
389
|
else
|
|
364
390
|
fail "render-jira bullet count $bullet_count != 2"
|
|
365
391
|
fi
|
|
366
392
|
|
|
393
|
+
# render-jira/-conf with ZERO PR urls: must pass the body through unchanged
|
|
394
|
+
# (the old `grep -c || echo 0` idiom emitted "0\n0" here and prepended
|
|
395
|
+
# bogus "* PR:" bullets to the posted body).
|
|
396
|
+
cat > "$TMP/noprs.json" <<'EOF'
|
|
397
|
+
{
|
|
398
|
+
"schemaVersion":"2.1.0","taskId":"Z","shortId":1,"project":"my-app",
|
|
399
|
+
"branch":"feature/Z","baseBranch":"develop","currentPhase":6,"status":"in_progress",
|
|
400
|
+
"startedAt":"2026-04-27T12:00:00Z","phases":{},"identity":{"name":"T","email":"t@e.com"},
|
|
401
|
+
"projects":[
|
|
402
|
+
{"project":"my-app","projectRoot":"/tmp/a","branch":"feature/Z","baseBranch":"develop","identity":{"name":"T","email":"t@e.com"},"pr":null},
|
|
403
|
+
{"project":"common","projectRoot":"/tmp/b","branch":"feature/Z","baseBranch":"develop","identity":{"name":"T","email":"t@e.com"},"pr":null}
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
EOF
|
|
407
|
+
rendered=$("$CMR" render-jira "$TMP/noprs.json" "$TMP/body.md" 2>/dev/null)
|
|
408
|
+
if [ "$rendered" = "body content" ]; then
|
|
409
|
+
ok "render-jira with zero PR urls passes body through unchanged"
|
|
410
|
+
else
|
|
411
|
+
fail "render-jira with zero PR urls mutated body: $rendered"
|
|
412
|
+
fi
|
|
413
|
+
rendered=$("$CMR" render-conf "$TMP/noprs.json" "$TMP/body.md" 2>/dev/null)
|
|
414
|
+
if [ "$rendered" = "body content" ]; then
|
|
415
|
+
ok "render-conf with zero PR urls passes body through unchanged"
|
|
416
|
+
else
|
|
417
|
+
fail "render-conf with zero PR urls mutated body: $rendered"
|
|
418
|
+
fi
|
|
419
|
+
|
|
367
420
|
# render-conf: ## Related PRs heading
|
|
368
421
|
rendered=$("$CMR" render-conf "$TMP/multi.json" "$TMP/body.md" 2>/dev/null)
|
|
369
422
|
if printf '%s' "$rendered" | grep -q "## Related PRs"; then
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-pack-contents.sh - guard what `npm publish` would actually ship.
|
|
4
|
+
#
|
|
5
|
+
# 1. Deny patterns: package.json files[] carries negations (`!...`) for
|
|
6
|
+
# dev-only / personal-data files. The published tarball must contain
|
|
7
|
+
# no file matching any of them. Patterns are read from package.json at
|
|
8
|
+
# runtime (via node), so the smoke never goes stale when the deny list
|
|
9
|
+
# changes.
|
|
10
|
+
# 2. File-count band: the total packed file count is compared against the
|
|
11
|
+
# committed fixture pipeline/scripts/fixtures/pack-expected-count.txt.
|
|
12
|
+
# Drift beyond +-15 percent fails - that magnitude means the files[]
|
|
13
|
+
# globs picked up (or dropped) a whole subtree, which deserves a human
|
|
14
|
+
# look. Refresh the fixture after reviewing:
|
|
15
|
+
# npm pack --dry-run --json 2>/dev/null \
|
|
16
|
+
# | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s)[0].files.length))' \
|
|
17
|
+
# > pipeline/scripts/fixtures/pack-expected-count.txt
|
|
18
|
+
# 3. publint (optional): linted via `npx --no-install` so it only runs
|
|
19
|
+
# when the package is already installed - never a network fetch.
|
|
20
|
+
#
|
|
21
|
+
# Exit codes: 0=pass, 1=violation, 2=environment broken
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
26
|
+
FIXTURE="$REPO_ROOT/pipeline/scripts/fixtures/pack-expected-count.txt"
|
|
27
|
+
|
|
28
|
+
command -v npm >/dev/null 2>&1 || { echo "FAIL: npm not on PATH"; exit 2; }
|
|
29
|
+
command -v node >/dev/null 2>&1 || { echo "FAIL: node not on PATH"; exit 2; }
|
|
30
|
+
|
|
31
|
+
PASS=0
|
|
32
|
+
FAIL=0
|
|
33
|
+
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
34
|
+
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
35
|
+
|
|
36
|
+
cleanup() { [ -n "${TMP:-}" ] && rm -rf "$TMP"; }
|
|
37
|
+
trap cleanup EXIT
|
|
38
|
+
TMP=$(mktemp -d)
|
|
39
|
+
|
|
40
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
41
|
+
echo "→ 1. npm pack --dry-run --json"
|
|
42
|
+
if ! (cd "$REPO_ROOT" && npm pack --dry-run --json 2>/dev/null > "$TMP/pack.json"); then
|
|
43
|
+
echo "FAIL: npm pack --dry-run failed"
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Deny check runs in node so the patterns come straight from package.json
|
|
48
|
+
# files[] negations - never hardcoded here.
|
|
49
|
+
cat > "$TMP/check.mjs" <<'MJS'
|
|
50
|
+
import { readFileSync } from "node:fs";
|
|
51
|
+
|
|
52
|
+
const [packJsonPath, pkgJsonPath] = process.argv.slice(2);
|
|
53
|
+
const packed = JSON.parse(readFileSync(packJsonPath, "utf-8"))[0].files.map((f) => f.path);
|
|
54
|
+
const files = JSON.parse(readFileSync(pkgJsonPath, "utf-8")).files || [];
|
|
55
|
+
const denies = files.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
|
|
56
|
+
|
|
57
|
+
// Minimal glob -> regex: '**' spans path separators, '*' does not.
|
|
58
|
+
const toRegex = (glob) => {
|
|
59
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
60
|
+
const body = escaped
|
|
61
|
+
.split("**")
|
|
62
|
+
.map((part) => part.replace(/\*/g, "[^/]*"))
|
|
63
|
+
.join(".*");
|
|
64
|
+
return new RegExp("^" + body + "$");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const regexes = denies.map((d) => [d, toRegex(d)]);
|
|
68
|
+
let bad = 0;
|
|
69
|
+
for (const path of packed) {
|
|
70
|
+
for (const [glob, re] of regexes) {
|
|
71
|
+
if (re.test(path)) {
|
|
72
|
+
console.log(`DENY ${path} (matches !${glob})`);
|
|
73
|
+
bad++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log(`DENYCOUNT ${bad}`);
|
|
78
|
+
console.log(`PATTERNS ${denies.length}`);
|
|
79
|
+
console.log(`COUNT ${packed.length}`);
|
|
80
|
+
MJS
|
|
81
|
+
|
|
82
|
+
CHECK=$(node "$TMP/check.mjs" "$TMP/pack.json" "$REPO_ROOT/package.json")
|
|
83
|
+
COUNT=$(echo "$CHECK" | awk '/^COUNT /{print $2}')
|
|
84
|
+
DENYCOUNT=$(echo "$CHECK" | awk '/^DENYCOUNT /{print $2}')
|
|
85
|
+
PATTERNS=$(echo "$CHECK" | awk '/^PATTERNS /{print $2}')
|
|
86
|
+
|
|
87
|
+
if [ -z "$COUNT" ] || [ -z "$DENYCOUNT" ]; then
|
|
88
|
+
echo "FAIL: deny-pattern checker produced no result (node error?)"
|
|
89
|
+
exit 2
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
pass "pack dry-run produced $COUNT files ($PATTERNS deny patterns loaded from package.json)"
|
|
93
|
+
|
|
94
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
95
|
+
echo "→ 2. deny patterns (files[] negations must not ship)"
|
|
96
|
+
if [ "$DENYCOUNT" -eq 0 ]; then
|
|
97
|
+
pass "no packed file matches a files[] negation"
|
|
98
|
+
else
|
|
99
|
+
fail "$DENYCOUNT packed file(s) match dev-only/personal-data exclusions:"
|
|
100
|
+
echo "$CHECK" | grep '^DENY ' | sed 's/^/ /'
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
104
|
+
echo "→ 3. file-count band vs fixture (+-15 percent)"
|
|
105
|
+
if [ ! -f "$FIXTURE" ]; then
|
|
106
|
+
fail "fixture missing: $FIXTURE - create it from the refresh command in this script's header"
|
|
107
|
+
else
|
|
108
|
+
EXPECTED=$(tr -cd '0-9' < "$FIXTURE")
|
|
109
|
+
if [ -z "$EXPECTED" ] || [ "$EXPECTED" -eq 0 ]; then
|
|
110
|
+
fail "fixture unreadable or zero: $FIXTURE"
|
|
111
|
+
else
|
|
112
|
+
DIFF=$((COUNT - EXPECTED))
|
|
113
|
+
[ "$DIFF" -lt 0 ] && DIFF=$((-DIFF))
|
|
114
|
+
LIMIT=$((EXPECTED * 15 / 100))
|
|
115
|
+
if [ "$DIFF" -le "$LIMIT" ]; then
|
|
116
|
+
pass "count $COUNT within +-15 percent of expected $EXPECTED (drift $DIFF, limit $LIMIT)"
|
|
117
|
+
else
|
|
118
|
+
fail "count $COUNT drifted beyond +-15 percent of expected $EXPECTED (drift $DIFF, limit $LIMIT)"
|
|
119
|
+
echo " Review what the files[] globs now include, then refresh the fixture"
|
|
120
|
+
echo " (refresh command is in this script's header comment)."
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
126
|
+
echo "→ 4. publint (optional, npx --no-install only)"
|
|
127
|
+
if (cd "$REPO_ROOT" && npx --no-install publint --help >/dev/null 2>&1); then
|
|
128
|
+
if OUT=$(cd "$REPO_ROOT" && npx --no-install publint 2>&1); then
|
|
129
|
+
pass "publint: no errors"
|
|
130
|
+
else
|
|
131
|
+
fail "publint reported errors"
|
|
132
|
+
echo "$OUT" | sed 's/^/ /'
|
|
133
|
+
fi
|
|
134
|
+
else
|
|
135
|
+
echo " ↷ SKIP: publint not installed (npx --no-install found no local package)"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
echo ""
|
|
139
|
+
echo "══ smoke-pack-contents: $PASS passed, $FAIL failed ══"
|
|
140
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-plugin-validate.sh - run `claude plugin validate --strict` against
|
|
4
|
+
# every plugin / marketplace surface committed to this repo.
|
|
5
|
+
#
|
|
6
|
+
# Discovery: any directory containing `.claude-plugin/plugin.json` or
|
|
7
|
+
# `.claude-plugin/marketplace.json` (node_modules excluded). The `claude`
|
|
8
|
+
# CLI validates the containing directory; --strict promotes warnings
|
|
9
|
+
# (unrecognized fields, missing metadata) to failures so CI catches drift
|
|
10
|
+
# the runtime would silently tolerate.
|
|
11
|
+
#
|
|
12
|
+
# Skip semantics (exit 0 with a SKIP line):
|
|
13
|
+
# - `claude` binary not on PATH (optional tooling; billing-friendly CI
|
|
14
|
+
# runners don't install it)
|
|
15
|
+
# - no validatable surface in the repo (the marketplace plugins live in
|
|
16
|
+
# the separate multi-agent-plugins repo; this repo may carry none)
|
|
17
|
+
#
|
|
18
|
+
# Exit codes: 0=pass or skip, 1=validation failure
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
23
|
+
|
|
24
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
25
|
+
echo "SKIP smoke-plugin-validate: claude binary not on PATH"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Each hit is a manifest; the validatable surface is its .claude-plugin parent's parent.
|
|
30
|
+
MANIFESTS=$(find "$REPO_ROOT" \
|
|
31
|
+
-path "$REPO_ROOT/node_modules" -prune -o \
|
|
32
|
+
-path "$REPO_ROOT/coverage" -prune -o \
|
|
33
|
+
-type f -path '*/.claude-plugin/*' \( -name plugin.json -o -name marketplace.json \) -print \
|
|
34
|
+
2>/dev/null | sort)
|
|
35
|
+
|
|
36
|
+
if [ -z "$MANIFESTS" ]; then
|
|
37
|
+
echo "SKIP smoke-plugin-validate: no .claude-plugin plugin/marketplace manifest in this repo"
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
PASS=0
|
|
42
|
+
FAIL=0
|
|
43
|
+
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
44
|
+
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
45
|
+
|
|
46
|
+
# Validate each surface directory once, even if it carries both manifests.
|
|
47
|
+
SURFACES=$(echo "$MANIFESTS" | sed 's|/\.claude-plugin/[^/]*$||' | sort -u)
|
|
48
|
+
|
|
49
|
+
for surface in $SURFACES; do
|
|
50
|
+
rel="${surface#"$REPO_ROOT"/}"
|
|
51
|
+
[ "$surface" = "$REPO_ROOT" ] && rel="."
|
|
52
|
+
echo ""
|
|
53
|
+
echo "→ claude plugin validate --strict $rel"
|
|
54
|
+
if OUT=$(claude plugin validate --strict "$surface" 2>&1); then
|
|
55
|
+
pass "$rel validates clean (strict)"
|
|
56
|
+
else
|
|
57
|
+
fail "$rel failed strict validation"
|
|
58
|
+
echo "$OUT" | sed 's/^/ /'
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo "══ smoke-plugin-validate: $PASS passed, $FAIL failed ══"
|
|
64
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# smoke-prune-logs.sh - prune-logs.sh removes per-task log dirs, honors the
|
|
3
|
-
# project/task/age filters, is dry-run by default,
|
|
4
|
-
# trail or metrics corpus
|
|
3
|
+
# project/task/age filters, is dry-run by default, NEVER deletes the audit
|
|
4
|
+
# trail or metrics corpus (even under adversarial dir names), refuses unsafe
|
|
5
|
+
# log roots, and spares active task dirs.
|
|
5
6
|
#
|
|
6
7
|
# Exit 0 = all pass, 1 = any failure.
|
|
7
8
|
|
|
@@ -40,7 +41,7 @@ seed() {
|
|
|
40
41
|
|
|
41
42
|
echo "→ 1. dry-run lists task dirs and deletes nothing"
|
|
42
43
|
seed
|
|
43
|
-
out=$(PRUNE_LOG_ROOT="$LR" bash "$PL" 2>&1)
|
|
44
|
+
out=$(PRUNE_LOG_ROOT="$LR" PRUNE_GRACE_MIN=0 bash "$PL" 2>&1)
|
|
44
45
|
echo "$out" | grep -q "DRY-RUN" && pass "dry-run banner shown" || fail "dry-run banner missing"
|
|
45
46
|
[ -f "$LR/my-ios-app/PROJ-1/agent-state.json" ] && pass "dry-run kept task dirs" || fail "dry-run deleted a task dir"
|
|
46
47
|
echo "$out" | grep -qi "preserved" && pass "dry-run states audit/metrics are preserved" || fail "no preservation note"
|
|
@@ -48,7 +49,7 @@ echo "$out" | grep -qi "preserved" && pass "dry-run states audit/metrics are pre
|
|
|
48
49
|
echo ""
|
|
49
50
|
echo "→ 2. --yes deletes task dirs but preserves audit + metrics + .counter"
|
|
50
51
|
seed
|
|
51
|
-
PRUNE_LOG_ROOT="$LR" bash "$PL" --yes >/dev/null 2>&1
|
|
52
|
+
PRUNE_LOG_ROOT="$LR" PRUNE_GRACE_MIN=0 bash "$PL" --yes >/dev/null 2>&1
|
|
52
53
|
[ -f "$LR/audit.jsonl" ] && pass "audit.jsonl preserved" || fail "audit.jsonl was deleted"
|
|
53
54
|
[ -f "$LR/metrics.jsonl" ] && pass "metrics.jsonl preserved" || fail "metrics.jsonl was deleted"
|
|
54
55
|
[ -f "$LR/my-ios-app/.counter" ] && pass ".counter preserved" || fail ".counter was deleted"
|
|
@@ -58,21 +59,21 @@ PRUNE_LOG_ROOT="$LR" bash "$PL" --yes >/dev/null 2>&1
|
|
|
58
59
|
echo ""
|
|
59
60
|
echo "→ 3. --project scopes deletion to one project"
|
|
60
61
|
seed
|
|
61
|
-
PRUNE_LOG_ROOT="$LR" bash "$PL" --project=my-ios-app --yes >/dev/null 2>&1
|
|
62
|
+
PRUNE_LOG_ROOT="$LR" PRUNE_GRACE_MIN=0 bash "$PL" --project=my-ios-app --yes >/dev/null 2>&1
|
|
62
63
|
[ ! -e "$LR/my-ios-app/PROJ-1" ] && pass "my-ios-app task removed" || fail "my-ios-app task survived"
|
|
63
64
|
[ -f "$LR/other/PROJ-9/agent-log.md" ] && pass "other project untouched" || fail "other project was deleted"
|
|
64
65
|
|
|
65
66
|
echo ""
|
|
66
67
|
echo "→ 4. --task scopes deletion to one task dir"
|
|
67
68
|
seed
|
|
68
|
-
PRUNE_LOG_ROOT="$LR" bash "$PL" --task=PROJ-1 --yes >/dev/null 2>&1
|
|
69
|
+
PRUNE_LOG_ROOT="$LR" PRUNE_GRACE_MIN=0 bash "$PL" --task=PROJ-1 --yes >/dev/null 2>&1
|
|
69
70
|
[ ! -e "$LR/my-ios-app/PROJ-1" ] && pass "PROJ-1 removed" || fail "PROJ-1 survived"
|
|
70
71
|
[ -f "$LR/my-ios-app/PROJ-2/tracker-state.json" ] && pass "PROJ-2 untouched" || fail "PROJ-2 was deleted"
|
|
71
72
|
|
|
72
73
|
echo ""
|
|
73
74
|
echo "→ 5. --older-than excludes fresh task dirs"
|
|
74
75
|
seed
|
|
75
|
-
out=$(PRUNE_LOG_ROOT="$LR" bash "$PL" --older-than=1 2>&1)
|
|
76
|
+
out=$(PRUNE_LOG_ROOT="$LR" PRUNE_GRACE_MIN=0 bash "$PL" --older-than=1 2>&1)
|
|
76
77
|
echo "$out" | grep -q "nothing" && pass "freshly-created dirs excluded by --older-than=1" || fail "--older-than did not exclude fresh dirs"
|
|
77
78
|
|
|
78
79
|
echo ""
|
|
@@ -82,6 +83,55 @@ PRUNE_LOG_ROOT="$LR" bash "$PL" --bogus >/dev/null 2>&1
|
|
|
82
83
|
PRUNE_LOG_ROOT="/nonexistent-$$-dir" bash "$PL" >/dev/null 2>&1
|
|
83
84
|
[ "$?" = "0" ] && pass "missing log root exits 0 (no-op)" || fail "missing log root did not exit 0"
|
|
84
85
|
|
|
86
|
+
echo ""
|
|
87
|
+
echo "→ 7. adversarial dirs named like preserved files survive --yes"
|
|
88
|
+
rm -rf "${LR:?}"/*
|
|
89
|
+
mkdir -p "$LR/audit.jsonl" "$LR/proj/metrics.jsonl" "$LR/proj/metrics-summary.md" "$LR/proj/.counter" "$LR/proj/PROJ-5"
|
|
90
|
+
echo '{}' >"$LR/audit.jsonl/agent-state.json"
|
|
91
|
+
echo '{}' >"$LR/proj/metrics.jsonl/agent-log.md"
|
|
92
|
+
echo '{}' >"$LR/proj/metrics-summary.md/tracker-state.json"
|
|
93
|
+
echo '{}' >"$LR/proj/.counter/agent-state.json"
|
|
94
|
+
echo '{}' >"$LR/proj/PROJ-5/agent-state.json"
|
|
95
|
+
PRUNE_LOG_ROOT="$LR" PRUNE_GRACE_MIN=0 bash "$PL" --yes >/dev/null 2>&1
|
|
96
|
+
[ -d "$LR/audit.jsonl" ] && pass "dir named audit.jsonl spared" || fail "dir named audit.jsonl deleted"
|
|
97
|
+
[ -d "$LR/proj/metrics.jsonl" ] && pass "dir named metrics.jsonl spared" || fail "dir named metrics.jsonl deleted"
|
|
98
|
+
[ -d "$LR/proj/metrics-summary.md" ] && pass "dir named metrics-summary.md spared" || fail "dir named metrics-summary.md deleted"
|
|
99
|
+
[ -d "$LR/proj/.counter" ] && pass "dir named .counter spared" || fail "dir named .counter deleted"
|
|
100
|
+
[ ! -e "$LR/proj/PROJ-5" ] && pass "normal task dir beside them still removed" || fail "normal task dir survived"
|
|
101
|
+
|
|
102
|
+
echo ""
|
|
103
|
+
echo "→ 8. unsafe log roots are refused (exit 2, dry-run only)"
|
|
104
|
+
PRUNE_LOG_ROOT="$HOME" bash "$PL" >/dev/null 2>&1
|
|
105
|
+
[ "$?" = "2" ] && pass "\$HOME as log root refused" || fail "\$HOME as log root not refused"
|
|
106
|
+
GITROOT="$(mktemp -d)"
|
|
107
|
+
mkdir -p "$GITROOT/.git"
|
|
108
|
+
PRUNE_LOG_ROOT="$GITROOT" bash "$PL" --yes >/dev/null 2>&1
|
|
109
|
+
rc=$?
|
|
110
|
+
rm -rf "$GITROOT"
|
|
111
|
+
[ "$rc" = "2" ] && pass "git work tree as log root refused" || fail "git work tree as log root not refused"
|
|
112
|
+
|
|
113
|
+
echo ""
|
|
114
|
+
echo "→ 9. default grace spares freshly-touched (active) task dirs"
|
|
115
|
+
seed
|
|
116
|
+
out=$(PRUNE_LOG_ROOT="$LR" bash "$PL" --yes 2>&1)
|
|
117
|
+
rc=$?
|
|
118
|
+
[ "$rc" = "0" ] && pass "grace run exits 0" || fail "grace run exited $rc"
|
|
119
|
+
[ -f "$LR/my-ios-app/PROJ-1/agent-state.json" ] && pass "active task dir spared by default grace" || fail "default grace deleted an active task dir"
|
|
120
|
+
echo "$out" | grep -qi "spared" && pass "output mentions spared active dirs" || fail "no spared-active note in output"
|
|
121
|
+
PRUNE_LOG_ROOT="$LR" bash "$PL" --task=PROJ-1 --yes >/dev/null 2>&1
|
|
122
|
+
[ ! -e "$LR/my-ios-app/PROJ-1" ] && pass "explicit --task bypasses the grace" || fail "--task did not bypass the grace"
|
|
123
|
+
|
|
124
|
+
echo ""
|
|
125
|
+
echo "→ 10. traversal-shaped --task/--project values are rejected"
|
|
126
|
+
PRUNE_LOG_ROOT="$LR" bash "$PL" "--task=../evil" >/dev/null 2>&1
|
|
127
|
+
[ "$?" = "2" ] && pass "--task=../evil exits 2" || fail "--task=../evil accepted"
|
|
128
|
+
PRUNE_LOG_ROOT="$LR" bash "$PL" "--task=.counter" >/dev/null 2>&1
|
|
129
|
+
[ "$?" = "2" ] && pass "--task=.counter (leading dot) exits 2" || fail "--task=.counter accepted"
|
|
130
|
+
PRUNE_LOG_ROOT="$LR" bash "$PL" "--project=a/b" >/dev/null 2>&1
|
|
131
|
+
[ "$?" = "2" ] && pass "--project=a/b exits 2" || fail "--project=a/b accepted"
|
|
132
|
+
PRUNE_LOG_ROOT="$LR" bash "$PL" "--task=" >/dev/null 2>&1
|
|
133
|
+
[ "$?" = "2" ] && pass "empty --task exits 2" || fail "empty --task accepted"
|
|
134
|
+
|
|
85
135
|
echo ""
|
|
86
136
|
echo "══ prune-logs smoke: $PASS passed, $FAIL failed ══"
|
|
87
137
|
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-purge.sh - purge.sh wipes .worktrees/ worktrees (registered and
|
|
3
|
+
# orphaned), their local branches, and the project task counter; is dry-run
|
|
4
|
+
# by default; preserves the main worktree, protected branches, and the
|
|
5
|
+
# audit/metrics corpus; and never follows symlinks out of .worktrees/.
|
|
6
|
+
#
|
|
7
|
+
# Exit 0 = all pass, 1 = any failure.
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
12
|
+
PG="$ROOT/pipeline/scripts/purge.sh"
|
|
13
|
+
PASS=0
|
|
14
|
+
FAIL=0
|
|
15
|
+
pass() {
|
|
16
|
+
PASS=$((PASS + 1))
|
|
17
|
+
echo " ✓ $1"
|
|
18
|
+
}
|
|
19
|
+
fail() {
|
|
20
|
+
FAIL=$((FAIL + 1))
|
|
21
|
+
echo " ✗ $1"
|
|
22
|
+
}
|
|
23
|
+
[ -f "$PG" ] || {
|
|
24
|
+
echo "FAIL: purge.sh missing" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
SB="$(cd "$(mktemp -d)" && pwd -P)"
|
|
29
|
+
trap 'rm -rf "$SB"' EXIT
|
|
30
|
+
|
|
31
|
+
REPO="$SB/repo"
|
|
32
|
+
LR="$SB/logs"
|
|
33
|
+
VICTIM="$SB/victim"
|
|
34
|
+
|
|
35
|
+
seed() {
|
|
36
|
+
rm -rf "$REPO" "$LR" "$VICTIM"
|
|
37
|
+
mkdir -p "$REPO" "$LR/repo" "$VICTIM"
|
|
38
|
+
echo keep >"$VICTIM/keep.txt"
|
|
39
|
+
git -C "$REPO" init -q -b main
|
|
40
|
+
git -C "$REPO" -c user.email=t@t.t -c user.name=t commit -q --allow-empty -m init
|
|
41
|
+
git -C "$REPO" worktree add -q -b feature/PROJ-1-x "$REPO/.worktrees/PROJ-1" >/dev/null 2>&1
|
|
42
|
+
git -C "$REPO" worktree add -q -b bugfix/PROJ-2-y "$REPO/.worktrees/PROJ-2" >/dev/null 2>&1
|
|
43
|
+
mkdir -p "$REPO/.worktrees/PROJ-9" # orphan: dir git no longer registers
|
|
44
|
+
echo '{}' >"$REPO/.worktrees/PROJ-9/agent-state.json"
|
|
45
|
+
ln -s "$VICTIM" "$REPO/.worktrees/evil-link" # symlink escape attempt
|
|
46
|
+
echo '7' >"$LR/repo/.counter"
|
|
47
|
+
echo audit >"$LR/audit.jsonl"
|
|
48
|
+
echo metric >"$LR/metrics.jsonl"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
has_branch() {
|
|
52
|
+
git -C "$REPO" show-ref --verify -q "refs/heads/$1"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
echo "→ 1. dry-run lists everything and deletes nothing"
|
|
56
|
+
seed
|
|
57
|
+
out=$(PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$REPO" 2>&1)
|
|
58
|
+
rc=$?
|
|
59
|
+
[ "$rc" = "0" ] && pass "dry-run exits 0" || fail "dry-run exited $rc"
|
|
60
|
+
echo "$out" | grep -q "DRY-RUN" && pass "dry-run banner shown" || fail "dry-run banner missing"
|
|
61
|
+
echo "$out" | grep -q "PROJ-1" && pass "worktree PROJ-1 listed" || fail "worktree PROJ-1 not listed"
|
|
62
|
+
echo "$out" | grep -q "feature/PROJ-1-x" && pass "branch listed" || fail "branch not listed"
|
|
63
|
+
echo "$out" | grep -q ".counter" && pass "counter listed" || fail "counter not listed"
|
|
64
|
+
[ -d "$REPO/.worktrees/PROJ-1" ] && pass "dry-run kept worktrees" || fail "dry-run deleted a worktree"
|
|
65
|
+
[ -f "$LR/repo/.counter" ] && pass "dry-run kept the counter" || fail "dry-run deleted the counter"
|
|
66
|
+
has_branch feature/PROJ-1-x && pass "dry-run kept branches" || fail "dry-run deleted a branch"
|
|
67
|
+
|
|
68
|
+
echo ""
|
|
69
|
+
echo "→ 2. --yes wipes worktrees + branches + counter, preserves the rest"
|
|
70
|
+
seed
|
|
71
|
+
PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$REPO" --yes >/dev/null 2>&1
|
|
72
|
+
rc=$?
|
|
73
|
+
[ "$rc" = "0" ] && pass "--yes exits 0" || fail "--yes exited $rc"
|
|
74
|
+
[ ! -e "$REPO/.worktrees/PROJ-1" ] && pass "registered worktree PROJ-1 removed" || fail "PROJ-1 survived"
|
|
75
|
+
[ ! -e "$REPO/.worktrees/PROJ-2" ] && pass "registered worktree PROJ-2 removed" || fail "PROJ-2 survived"
|
|
76
|
+
[ ! -e "$REPO/.worktrees/PROJ-9" ] && pass "orphan worktree dir removed" || fail "orphan PROJ-9 survived"
|
|
77
|
+
! has_branch feature/PROJ-1-x && pass "branch feature/PROJ-1-x deleted" || fail "feature/PROJ-1-x survived"
|
|
78
|
+
! has_branch bugfix/PROJ-2-y && pass "branch bugfix/PROJ-2-y deleted" || fail "bugfix/PROJ-2-y survived"
|
|
79
|
+
has_branch main && pass "main branch preserved" || fail "main branch was deleted"
|
|
80
|
+
git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 && pass "main worktree still a git repo" || fail "main worktree destroyed"
|
|
81
|
+
wt_count=$(git -C "$REPO" worktree list 2>/dev/null | wc -l | tr -d ' ')
|
|
82
|
+
[ "$wt_count" = "1" ] && pass "only the main worktree remains registered" || fail "expected 1 registered worktree, got $wt_count"
|
|
83
|
+
[ ! -e "$LR/repo/.counter" ] && pass "task counter removed" || fail "task counter survived"
|
|
84
|
+
[ -f "$LR/audit.jsonl" ] && pass "audit.jsonl preserved" || fail "audit.jsonl was deleted"
|
|
85
|
+
[ -f "$LR/metrics.jsonl" ] && pass "metrics.jsonl preserved" || fail "metrics.jsonl was deleted"
|
|
86
|
+
[ -f "$VICTIM/keep.txt" ] && pass "symlink target outside repo untouched" || fail "symlink target was deleted"
|
|
87
|
+
|
|
88
|
+
echo ""
|
|
89
|
+
echo "→ 3. protected branch carried by a worktree survives, worktree still removed"
|
|
90
|
+
seed
|
|
91
|
+
git -C "$REPO" worktree add -q -b develop "$REPO/.worktrees/DEV-1" >/dev/null 2>&1
|
|
92
|
+
PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$REPO" --yes >/dev/null 2>&1
|
|
93
|
+
[ ! -e "$REPO/.worktrees/DEV-1" ] && pass "develop worktree removed" || fail "develop worktree survived"
|
|
94
|
+
has_branch develop && pass "develop branch preserved" || fail "develop branch was deleted"
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
echo "→ 4. clean repo and non-repo are no-ops (exit 0)"
|
|
98
|
+
CLEAN="$SB/clean"
|
|
99
|
+
mkdir -p "$CLEAN"
|
|
100
|
+
git -C "$CLEAN" init -q -b main
|
|
101
|
+
out=$(PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$CLEAN" 2>&1)
|
|
102
|
+
rc=$?
|
|
103
|
+
{ [ "$rc" = "0" ] && echo "$out" | grep -q "nothing to do"; } && pass "clean repo is a no-op" || fail "clean repo not a clean no-op (rc=$rc)"
|
|
104
|
+
PLAIN="$SB/plain"
|
|
105
|
+
mkdir -p "$PLAIN"
|
|
106
|
+
out=$(bash "$PG" --repo "$PLAIN" 2>&1)
|
|
107
|
+
rc=$?
|
|
108
|
+
{ [ "$rc" = "0" ] && echo "$out" | grep -q "not a git work tree"; } && pass "non-repo dir is a no-op" || fail "non-repo dir not a clean no-op (rc=$rc)"
|
|
109
|
+
|
|
110
|
+
echo ""
|
|
111
|
+
echo "→ 5. usage errors exit 2"
|
|
112
|
+
bash "$PG" --bogus >/dev/null 2>&1
|
|
113
|
+
[ "$?" = "2" ] && pass "unknown argument exits 2" || fail "unknown argument did not exit 2"
|
|
114
|
+
bash "$PG" --repo >/dev/null 2>&1
|
|
115
|
+
[ "$?" = "2" ] && pass "--repo without a path exits 2" || fail "--repo without a path did not exit 2"
|
|
116
|
+
|
|
117
|
+
echo ""
|
|
118
|
+
echo "→ 6. --delete-remote without a remote warns but does not fail"
|
|
119
|
+
seed
|
|
120
|
+
out=$(PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$REPO" --delete-remote 2>&1)
|
|
121
|
+
echo "$out" | grep -q "remote branch origin/feature/PROJ-1-x" && pass "dry-run lists remote branch deletion" || fail "dry-run missing remote branch line"
|
|
122
|
+
out=$(PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$REPO" --delete-remote --yes 2>&1)
|
|
123
|
+
rc=$?
|
|
124
|
+
[ "$rc" = "0" ] && pass "--delete-remote --yes exits 0 without a remote" || fail "--delete-remote --yes exited $rc"
|
|
125
|
+
echo "$out" | grep -qi "warning" && pass "missing remote surfaces a warning" || fail "no warning for missing remote"
|
|
126
|
+
|
|
127
|
+
echo ""
|
|
128
|
+
echo "→ 7. purge from inside a linked worktree still anchors on the main repo"
|
|
129
|
+
seed
|
|
130
|
+
out=$(PURGE_LOG_ROOT="$LR" bash "$PG" --repo "$REPO/.worktrees/PROJ-1" --yes 2>&1)
|
|
131
|
+
rc=$?
|
|
132
|
+
[ "$rc" = "0" ] && pass "run from linked worktree exits 0" || fail "run from linked worktree exited $rc"
|
|
133
|
+
[ ! -e "$REPO/.worktrees/PROJ-1" ] && pass "worktrees removed when invoked from inside one" || fail "worktrees survived"
|
|
134
|
+
git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 && pass "main repo intact" || fail "main repo destroyed"
|
|
135
|
+
|
|
136
|
+
echo ""
|
|
137
|
+
echo "══ purge smoke: $PASS passed, $FAIL failed ══"
|
|
138
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -98,8 +98,22 @@ else
|
|
|
98
98
|
record_fail "node_modules leaked into shadow tracking"
|
|
99
99
|
fi
|
|
100
100
|
|
|
101
|
-
#
|
|
101
|
+
# 7b. Forced excludes apply even when NOT in the project .gitignore
|
|
102
|
+
# (DerivedData is not in this fixture's .gitignore; it must be excluded via
|
|
103
|
+
# the shadow repo's .git/info/exclude).
|
|
104
|
+
mkdir -p DerivedData && echo "buildjunk" > DerivedData/junk.txt
|
|
105
|
+
bash "$SG" snapshot "$TASK_ID" "$TMP" "derived-data-probe" >/dev/null 2>&1
|
|
106
|
+
if ! git --git-dir="$HOME/.claude/state/shadow-git/$TASK_ID/.git" --work-tree="$TMP" \
|
|
107
|
+
ls-files 2>/dev/null | grep -q "^DerivedData/"; then
|
|
108
|
+
record_pass "DerivedData NOT tracked (forced exclude in .git/info/exclude)"
|
|
109
|
+
else
|
|
110
|
+
record_fail "DerivedData leaked into shadow tracking (forced excludes dead)"
|
|
111
|
+
fi
|
|
112
|
+
rm -rf DerivedData
|
|
113
|
+
|
|
114
|
+
# Make a real edit + a brand-new file + snapshot
|
|
102
115
|
echo "hello-changed" > a.txt
|
|
116
|
+
echo "post-baseline" > b.txt
|
|
103
117
|
sha_edit=$(bash "$SG" snapshot "$TASK_ID" "$TMP" "edited a.txt" 2>/dev/null | head -1)
|
|
104
118
|
if [ -n "$sha_edit" ] && [ "${#sha_edit}" -ge 7 ]; then
|
|
105
119
|
record_pass "snapshot after edit produced a sha: $sha_edit"
|
|
@@ -115,6 +129,7 @@ else
|
|
|
115
129
|
fi
|
|
116
130
|
|
|
117
131
|
# 8. Restore the baseline
|
|
132
|
+
echo "never-snapshotted" > c.txt # created after the last snapshot
|
|
118
133
|
baseline_sha=$(bash "$SG" list "$TASK_ID" 2>/dev/null | tail -1 | awk '{print $1}')
|
|
119
134
|
bash "$SG" restore "$TASK_ID" "$TMP" "$baseline_sha" --files >/dev/null 2>&1
|
|
120
135
|
if [ "$(cat "$TMP/a.txt")" = "hello" ]; then
|
|
@@ -123,6 +138,23 @@ else
|
|
|
123
138
|
record_fail "restore did not revert a.txt (got '$(cat "$TMP/a.txt")')"
|
|
124
139
|
fi
|
|
125
140
|
|
|
141
|
+
# 8b. Restore removes files that did not exist in the snapshot
|
|
142
|
+
if [ ! -f "$TMP/b.txt" ]; then
|
|
143
|
+
record_pass "restore removed b.txt (committed after baseline)"
|
|
144
|
+
else
|
|
145
|
+
record_fail "restore left b.txt in place (created after snapshot must be removed)"
|
|
146
|
+
fi
|
|
147
|
+
if [ ! -f "$TMP/c.txt" ]; then
|
|
148
|
+
record_pass "restore removed c.txt (never snapshotted)"
|
|
149
|
+
else
|
|
150
|
+
record_fail "restore left c.txt in place (untracked new file must be removed)"
|
|
151
|
+
fi
|
|
152
|
+
if [ -f "$TMP/node_modules/x.txt" ]; then
|
|
153
|
+
record_pass "restore left ignored node_modules untouched"
|
|
154
|
+
else
|
|
155
|
+
record_fail "restore deleted ignored files (node_modules must survive clean)"
|
|
156
|
+
fi
|
|
157
|
+
|
|
126
158
|
# 9. Prune
|
|
127
159
|
bash "$SG" prune "$TASK_ID" >/dev/null 2>&1
|
|
128
160
|
if [ ! -d "$HOME/.claude/state/shadow-git/$TASK_ID" ]; then
|
|
@@ -131,6 +163,21 @@ else
|
|
|
131
163
|
record_fail "prune did not remove the shadow dir"
|
|
132
164
|
fi
|
|
133
165
|
|
|
166
|
+
# 9b. Task-id validation: traversal and separator ids are rejected before any
|
|
167
|
+
# rm -rf (a "prune .." must never delete ~/.claude/state).
|
|
168
|
+
for bad_id in ".." "." "a/b" "-x!"; do
|
|
169
|
+
if bash "$SG" prune "$bad_id" >/dev/null 2>&1; then
|
|
170
|
+
record_fail "prune accepted unsafe task id '$bad_id'"
|
|
171
|
+
else
|
|
172
|
+
record_pass "prune rejects unsafe task id '$bad_id'"
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
if [ -d "$HOME/.claude/state" ]; then
|
|
176
|
+
record_pass "~/.claude/state survived unsafe prune attempts"
|
|
177
|
+
else
|
|
178
|
+
record_fail "~/.claude/state is GONE after unsafe prune attempts"
|
|
179
|
+
fi
|
|
180
|
+
|
|
134
181
|
cd - >/dev/null
|
|
135
182
|
rm -rf "$TMP"
|
|
136
183
|
|
|
@@ -51,7 +51,7 @@ TARGETS=(
|
|
|
51
51
|
|
|
52
52
|
# Collect the list once.
|
|
53
53
|
SKILLS=$(find "${TARGETS[@]}" -name 'SKILL.md' -type f 2>/dev/null | sort)
|
|
54
|
-
SKILL_COUNT=$(echo "$SKILLS" | grep -c . ||
|
|
54
|
+
SKILL_COUNT=$(echo "$SKILLS" | grep -c . || true)
|
|
55
55
|
|
|
56
56
|
echo "→ Scope: $SKILL_COUNT authored SKILL.md files under shared/core/"
|
|
57
57
|
echo " (external/ intentionally excluded - upstream conventions)"
|
|
@@ -67,6 +67,19 @@ else
|
|
|
67
67
|
record_fail "local-ahead should be silent (got '$out', rc=$rc)"
|
|
68
68
|
fi
|
|
69
69
|
|
|
70
|
+
# 3b2. No --local, no repo clone -> falls back to the install-time version
|
|
71
|
+
# marker (npx-only installs have no clone to read a version from)
|
|
72
|
+
marker_home="$tmpdir/marker-home"
|
|
73
|
+
mkdir -p "$marker_home/.claude"
|
|
74
|
+
printf '10.0.0\n' > "$marker_home/.claude/.pipeline-version"
|
|
75
|
+
printf '%s|99.0.0\n' "$now" > "$CACHE"
|
|
76
|
+
out=$(HOME="$marker_home" UPDATE_CHECK_CACHE="$CACHE" bash "$SCRIPT" 2>/dev/null); rc=$?
|
|
77
|
+
if [ "$rc" -eq 0 ] && [ "$out" = "10.0.0|99.0.0" ]; then
|
|
78
|
+
record_pass "no clone -> .pipeline-version marker fallback works"
|
|
79
|
+
else
|
|
80
|
+
record_fail "marker fallback should print '10.0.0|99.0.0' (got '$out', rc=$rc)"
|
|
81
|
+
fi
|
|
82
|
+
|
|
70
83
|
# 3c. Offline + stale cache -> silent exit 0 (advisory: never blocks)
|
|
71
84
|
printf '0|10.0.0\n' > "$CACHE"
|
|
72
85
|
out=$(UPDATE_CHECK_CACHE="$CACHE" http_proxy="http://127.0.0.1:1" https_proxy="http://127.0.0.1:1" \
|