@mmerterden/multi-agent-pipeline 11.4.0 → 11.5.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 +118 -0
- package/README.md +1 -0
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +70 -10
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- 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/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 +4 -4
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- 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/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -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-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- 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 +105 -36
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
|
@@ -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
|
|
@@ -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)"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-workflow-audit.sh - security + correctness audit of
|
|
4
|
+
# `.github/workflows/` with the two dedicated external linters:
|
|
5
|
+
#
|
|
6
|
+
# - zizmor GitHub Actions security auditor (template injection,
|
|
7
|
+
# credential persistence, unpinned actions, ...). Runs in
|
|
8
|
+
# offline mode so the smoke never touches the network.
|
|
9
|
+
# - actionlint workflow correctness linter (expression syntax, matrix
|
|
10
|
+
# typos, shellcheck of run: blocks, ...).
|
|
11
|
+
#
|
|
12
|
+
# Both tools are optional: each is skipped with a SKIP line when its binary
|
|
13
|
+
# is not on PATH (billing-friendly CI runners install neither; run locally
|
|
14
|
+
# via `brew install zizmor actionlint` for full coverage). Structural
|
|
15
|
+
# baseline checks that need no external binary live in
|
|
16
|
+
# smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
|
|
17
|
+
#
|
|
18
|
+
# Any finding from either tool fails the smoke.
|
|
19
|
+
#
|
|
20
|
+
# Exit codes: 0=pass or all tools skipped, 1=findings, 2=workflows missing
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
25
|
+
WF_DIR="$REPO_ROOT/.github/workflows"
|
|
26
|
+
|
|
27
|
+
[ -d "$WF_DIR" ] || { echo "FAIL: .github/workflows/ missing"; exit 2; }
|
|
28
|
+
|
|
29
|
+
PASS=0
|
|
30
|
+
FAIL=0
|
|
31
|
+
SKIPPED=0
|
|
32
|
+
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
33
|
+
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
34
|
+
skip() { SKIPPED=$((SKIPPED + 1)); echo " ↷ SKIP: $1"; }
|
|
35
|
+
|
|
36
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
37
|
+
echo "→ 1. zizmor (security audit, offline)"
|
|
38
|
+
if command -v zizmor >/dev/null 2>&1; then
|
|
39
|
+
# Flag names shifted across zizmor releases (--offline vs --no-online-audits);
|
|
40
|
+
# probe --help so the smoke works with whichever is installed.
|
|
41
|
+
HELP=$(zizmor --help 2>&1 || true)
|
|
42
|
+
OFFLINE_FLAG="--offline"
|
|
43
|
+
echo "$HELP" | grep -q -- '--offline' || OFFLINE_FLAG="--no-online-audits"
|
|
44
|
+
if OUT=$(zizmor "$WF_DIR" --min-severity medium "$OFFLINE_FLAG" 2>&1); then
|
|
45
|
+
pass "zizmor: no findings at medium+ severity"
|
|
46
|
+
else
|
|
47
|
+
fail "zizmor reported findings (medium+ severity)"
|
|
48
|
+
echo "$OUT" | sed 's/^/ /'
|
|
49
|
+
fi
|
|
50
|
+
else
|
|
51
|
+
skip "zizmor not on PATH (brew install zizmor)"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
55
|
+
echo "→ 2. actionlint (workflow correctness)"
|
|
56
|
+
if command -v actionlint >/dev/null 2>&1; then
|
|
57
|
+
if OUT=$(cd "$REPO_ROOT" && actionlint 2>&1); then
|
|
58
|
+
pass "actionlint: no findings"
|
|
59
|
+
else
|
|
60
|
+
fail "actionlint reported findings"
|
|
61
|
+
echo "$OUT" | sed 's/^/ /'
|
|
62
|
+
fi
|
|
63
|
+
else
|
|
64
|
+
skip "actionlint not on PATH (brew install actionlint)"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo "══ smoke-workflow-audit: $PASS passed, $FAIL failed, $SKIPPED skipped ══"
|
|
69
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
@@ -114,7 +114,18 @@ function isExcluded(path) {
|
|
|
114
114
|
if (pat.endsWith("/")) {
|
|
115
115
|
if (path.includes(pat) || path.startsWith(pat)) return true;
|
|
116
116
|
} else if (pat.includes("**")) {
|
|
117
|
-
|
|
117
|
+
// Tokenize ** before the single-* pass so the * rewrite cannot mangle
|
|
118
|
+
// the already-translated .* (which broke cross-directory matches).
|
|
119
|
+
const DOUBLE_STAR = "\u0000";
|
|
120
|
+
const re = new RegExp(
|
|
121
|
+
"^" +
|
|
122
|
+
pat
|
|
123
|
+
.replace(/\./g, "\\.")
|
|
124
|
+
.replace(/\*\*/g, DOUBLE_STAR)
|
|
125
|
+
.replace(/\*/g, "[^/]*")
|
|
126
|
+
.replace(new RegExp(DOUBLE_STAR, "g"), ".*") +
|
|
127
|
+
"$",
|
|
128
|
+
);
|
|
118
129
|
if (re.test(path)) return true;
|
|
119
130
|
} else if (path.endsWith(pat)) return true;
|
|
120
131
|
}
|
|
@@ -110,7 +110,16 @@ function score(queryTokens, row) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function globToRegExp(glob) {
|
|
113
|
-
|
|
113
|
+
// Tokenize ** before the single-* pass: a sequential replace would turn
|
|
114
|
+
// ** into .* and then the global * pass would rewrite it to .[^/]*,
|
|
115
|
+
// breaking cross-directory matches (src/**/x vs src/a/b/x).
|
|
116
|
+
const DOUBLE_STAR = "\u0000";
|
|
117
|
+
const escaped = glob
|
|
118
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
119
|
+
.replace(/\*\*/g, DOUBLE_STAR)
|
|
120
|
+
.replace(/\*/g, "[^/]*")
|
|
121
|
+
.replace(/\?/g, ".")
|
|
122
|
+
.replace(new RegExp(DOUBLE_STAR, "g"), ".*");
|
|
114
123
|
return new RegExp("^" + escaped + "$", "i");
|
|
115
124
|
}
|
|
116
125
|
|
|
@@ -32,10 +32,38 @@
|
|
|
32
32
|
|
|
33
33
|
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
34
34
|
import { join } from "path";
|
|
35
|
+
import { pathToFileURL } from "url";
|
|
35
36
|
import { createInterface } from "readline";
|
|
36
37
|
|
|
37
38
|
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Agent definition files the pipeline ships (pipeline/agents/). This runs
|
|
42
|
+
* standalone from ~/.claude/scripts, so the list is static here; a unit test
|
|
43
|
+
* (test/uninstall.test.mjs) asserts it matches the source tree so it cannot
|
|
44
|
+
* go stale. Only these file names are removed from the installed agents
|
|
45
|
+
* dirs - user-authored agent files are never touched.
|
|
46
|
+
*/
|
|
47
|
+
export const PIPELINE_AGENT_FILES = [
|
|
48
|
+
"android-architect.md",
|
|
49
|
+
"backend-architect.md",
|
|
50
|
+
"code-reviewer.md",
|
|
51
|
+
"dev-critic.md",
|
|
52
|
+
"explorer.md",
|
|
53
|
+
"ios-architect.md",
|
|
54
|
+
"security-auditor.md",
|
|
55
|
+
"task-clarifier.md",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Non-multi-agent core skills the installer ships from
|
|
60
|
+
* pipeline/skills/shared/core/ (the multi-agent* dirs are matched by prefix).
|
|
61
|
+
*/
|
|
62
|
+
export const PIPELINE_CORE_SKILL_DIRS = [
|
|
63
|
+
"apple-archive-compliance",
|
|
64
|
+
"google-play-compliance",
|
|
65
|
+
];
|
|
66
|
+
|
|
39
67
|
const flags = process.argv.slice(2).filter((a) => a !== "uninstall");
|
|
40
68
|
|
|
41
69
|
const TOOL_FLAGS = [
|
|
@@ -122,6 +150,33 @@ function rmMatchingFiles(parent, predicate) {
|
|
|
122
150
|
return count;
|
|
123
151
|
}
|
|
124
152
|
|
|
153
|
+
// Explicit terminator install/copilot.mjs writes after the pipeline section
|
|
154
|
+
// (v11.4.1+). Must stay in sync with INSTRUCTIONS_END_MARKER there.
|
|
155
|
+
const COPILOT_END_MARKER = "<!-- multi-agent-pipeline:copilot-instructions:end -->";
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Legacy copilot-instructions files (written before the end marker existed)
|
|
159
|
+
* have no explicit terminator. Bound the pipeline span at the next top-level
|
|
160
|
+
* "# " heading after the start marker when one exists OUTSIDE fenced code
|
|
161
|
+
* blocks (the pipeline body carries bash comments like "# Bootstrap once ..."
|
|
162
|
+
* inside fences); otherwise the span runs to EOF (pre-marker behavior).
|
|
163
|
+
* Mirrors install/copilot.mjs legacyTrailingContent.
|
|
164
|
+
* @param {string} section - content from the start marker onward
|
|
165
|
+
* @returns {string} user content trailing the pipeline span ("" if none)
|
|
166
|
+
*/
|
|
167
|
+
function legacyTrailingContent(section) {
|
|
168
|
+
const lines = section.split("\n");
|
|
169
|
+
let inFence = false;
|
|
170
|
+
for (let i = 1; i < lines.length; i++) {
|
|
171
|
+
if (/^\s*(```|~~~)/.test(lines[i])) {
|
|
172
|
+
inFence = !inFence;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!inFence && /^# /.test(lines[i])) return lines.slice(i).join("\n");
|
|
176
|
+
}
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
|
|
125
180
|
/**
|
|
126
181
|
* Strip a marker-wrapped `<!-- multi-agent-pipeline:begin/end -->` block from
|
|
127
182
|
* a user-owned file. Preserves everything outside the markers. Deletes the
|
|
@@ -135,25 +190,33 @@ function stripManagedBlock(filePath) {
|
|
|
135
190
|
/\s*<!-- multi-agent-pipeline:begin -->[\s\S]*?<!-- multi-agent-pipeline:end -->\s*/m;
|
|
136
191
|
if (!re.test(content)) {
|
|
137
192
|
// Copilot's install (install/copilot.mjs) does not wrap the block in
|
|
138
|
-
// begin/end markers
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// used to orphan the block in files >50 KB).
|
|
193
|
+
// begin/end markers. v11.4.1+ writes an explicit end marker after the
|
|
194
|
+
// pipeline section; strip only the start..end span so user content BELOW
|
|
195
|
+
// the section survives. Legacy files without the end marker are bounded
|
|
196
|
+
// at the next top-level heading outside code fences, else EOF.
|
|
143
197
|
if (content.includes("# Multi-Agent Development Pipeline")) {
|
|
144
|
-
// Heuristic: Copilot's generateCopilotInstructions output. Only strip the
|
|
145
|
-
// pipeline section, not the whole file.
|
|
146
198
|
const marker = "# Multi-Agent Development Pipeline";
|
|
147
199
|
const idx = content.indexOf(marker);
|
|
148
200
|
if (idx >= 0) {
|
|
149
201
|
const before = content.slice(0, idx).trimEnd();
|
|
202
|
+
const fromStart = content.slice(idx);
|
|
203
|
+
const endIdx = fromStart.indexOf(COPILOT_END_MARKER);
|
|
204
|
+
const rawTrailing =
|
|
205
|
+
endIdx >= 0
|
|
206
|
+
? fromStart.slice(endIdx + COPILOT_END_MARKER.length)
|
|
207
|
+
: legacyTrailingContent(fromStart);
|
|
208
|
+
const trailing = rawTrailing.replace(/^[\r\n]+/, "").trimEnd();
|
|
209
|
+
let remaining = before;
|
|
210
|
+
if (trailing.length > 0) {
|
|
211
|
+
remaining = before.length > 0 ? before + "\n\n" + trailing : trailing;
|
|
212
|
+
}
|
|
150
213
|
if (dryRun) {
|
|
151
214
|
report("would update", filePath);
|
|
152
|
-
} else if (
|
|
215
|
+
} else if (remaining.length === 0) {
|
|
153
216
|
rmSync(filePath, { force: true });
|
|
154
217
|
console.log(` removed (was pipeline-only): ${filePath}`);
|
|
155
218
|
} else {
|
|
156
|
-
writeFileSync(filePath,
|
|
219
|
+
writeFileSync(filePath, remaining + "\n");
|
|
157
220
|
console.log(` cleaned pipeline section: ${filePath}`);
|
|
158
221
|
}
|
|
159
222
|
return true;
|
|
@@ -200,9 +263,12 @@ function cleanClaudeSettings(settingsPath) {
|
|
|
200
263
|
|
|
201
264
|
if (settings.hooks?.PreToolUse?.length) {
|
|
202
265
|
const before = settings.hooks.PreToolUse.length;
|
|
266
|
+
// Match both the current matcher ("Bash") and the dead pre-v11.4.1 one
|
|
267
|
+
// ("Bash(git commit:*)"); the pre-commit-check.sh command identifies the
|
|
268
|
+
// entry as ours either way.
|
|
203
269
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
204
270
|
(h) =>
|
|
205
|
-
!(h.matcher === "Bash(git commit:*)" &&
|
|
271
|
+
!((h.matcher === "Bash" || h.matcher === "Bash(git commit:*)") &&
|
|
206
272
|
h.hooks?.some((sub) => sub.command?.includes("pre-commit-check.sh"))),
|
|
207
273
|
);
|
|
208
274
|
if (settings.hooks.PreToolUse.length < before) touched = true;
|
|
@@ -239,8 +305,8 @@ async function main() {
|
|
|
239
305
|
if (dryRun) console.log(" [DRY-RUN] Nothing will be modified.");
|
|
240
306
|
console.log("");
|
|
241
307
|
console.log(" Targets:");
|
|
242
|
-
if (forClaude) console.log(" - Claude Code (~/.claude/{commands,
|
|
243
|
-
if (forCopilot) console.log(" - Copilot CLI (~/.copilot/{
|
|
308
|
+
if (forClaude) console.log(" - Claude Code (~/.claude/{commands/multi-agent, scripts, multi-agent-refs, schemas, lib} + pipeline skills + pipeline agent files)");
|
|
309
|
+
if (forCopilot) console.log(" - Copilot CLI (~/.copilot/{scripts, schemas, lib} + pipeline skills + pipeline agent files + copilot-instructions.md section)");
|
|
244
310
|
if (forCursor) console.log(` - Cursor (${adapterTarget}/.cursor/rules/multi-agent-* + .cursorrules)`);
|
|
245
311
|
if (forCopilotChat) console.log(` - GitHub Copilot Chat (${adapterTarget}/.github/copilot-instructions.md block + .github/instructions/multi-agent-*)`);
|
|
246
312
|
if (forAntigravity) console.log(` - Antigravity (${adapterTarget}/.agent/rules + .agent/workflows + AGENTS.md block + .agent/mcp_config.json)`);
|
|
@@ -265,6 +331,10 @@ async function main() {
|
|
|
265
331
|
const CLAUDE = join(HOME, ".claude");
|
|
266
332
|
rmIfExists(join(CLAUDE, "commands", "multi-agent"));
|
|
267
333
|
rmIfExists(join(CLAUDE, "scripts"));
|
|
334
|
+
// Pipeline-managed trees the installer lays down alongside scripts/.
|
|
335
|
+
rmIfExists(join(CLAUDE, "multi-agent-refs"));
|
|
336
|
+
rmIfExists(join(CLAUDE, "schemas"));
|
|
337
|
+
rmIfExists(join(CLAUDE, "lib"));
|
|
268
338
|
// NOTE: ~/.claude/rules/ is USER-OWNED. install lays baseline rules down
|
|
269
339
|
// write-if-missing and NEVER overwrites them (install/claude.mjs
|
|
270
340
|
// installRules, skipExisting) so users can evolve their global rules
|
|
@@ -274,20 +344,14 @@ async function main() {
|
|
|
274
344
|
const skills = join(CLAUDE, "skills");
|
|
275
345
|
let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
|
|
276
346
|
n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
|
|
347
|
+
n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
|
|
277
348
|
if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
|
|
278
349
|
rmIfExists(join(skills, ".skills-index.json"));
|
|
279
350
|
rmIfExists(join(skills, "skills-index.md"));
|
|
280
351
|
rmIfExists(join(skills, "README.md"));
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"explorer.md",
|
|
285
|
-
"ios-architect.md",
|
|
286
|
-
"android-architect.md",
|
|
287
|
-
"backend-architect.md",
|
|
288
|
-
"security-auditor.md",
|
|
289
|
-
].includes(name),
|
|
290
|
-
);
|
|
352
|
+
// Agent definitions are FILES; remove only the pipeline-shipped names so
|
|
353
|
+
// user-authored agents survive.
|
|
354
|
+
rmMatchingFiles(join(CLAUDE, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
|
|
291
355
|
cleanClaudeSettings(join(CLAUDE, "settings.json"));
|
|
292
356
|
}
|
|
293
357
|
|
|
@@ -296,21 +360,20 @@ async function main() {
|
|
|
296
360
|
console.log(" [Copilot CLI] Removing...");
|
|
297
361
|
const COP = join(HOME, ".copilot");
|
|
298
362
|
rmIfExists(join(COP, "scripts"));
|
|
363
|
+
// Pipeline-managed trees the installer lays down alongside scripts/.
|
|
364
|
+
rmIfExists(join(COP, "schemas"));
|
|
365
|
+
rmIfExists(join(COP, "lib"));
|
|
299
366
|
const skills = join(COP, "skills");
|
|
300
367
|
let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
|
|
301
368
|
n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
|
|
369
|
+
n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
|
|
302
370
|
if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
|
|
371
|
+
rmIfExists(join(skills, ".skills-index.json"));
|
|
372
|
+
rmIfExists(join(skills, "skills-index.md"));
|
|
303
373
|
rmIfExists(join(skills, "README.md"));
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"explorer.md",
|
|
308
|
-
"ios-architect.md",
|
|
309
|
-
"android-architect.md",
|
|
310
|
-
"backend-architect.md",
|
|
311
|
-
"security-auditor.md",
|
|
312
|
-
].includes(name),
|
|
313
|
-
);
|
|
374
|
+
// Agent definitions are FILES; remove only the pipeline-shipped names so
|
|
375
|
+
// user-authored agents survive.
|
|
376
|
+
rmMatchingFiles(join(COP, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
|
|
314
377
|
stripManagedBlock(join(COP, "copilot-instructions.md"));
|
|
315
378
|
}
|
|
316
379
|
|
|
@@ -370,7 +433,13 @@ async function main() {
|
|
|
370
433
|
console.log("");
|
|
371
434
|
}
|
|
372
435
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
436
|
+
// Run only when executed directly (node uninstall.mjs). Guard lets tests
|
|
437
|
+
// import the exported constants without triggering the uninstall.
|
|
438
|
+
const isMainModule =
|
|
439
|
+
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
440
|
+
if (isMainModule) {
|
|
441
|
+
main().catch((e) => {
|
|
442
|
+
console.error("uninstall failed:", e.message);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
@@ -29,12 +29,63 @@
|
|
|
29
29
|
|
|
30
30
|
set -euo pipefail
|
|
31
31
|
|
|
32
|
+
# Rewrite the `### Progress` block of the body file ($2) with $1 and print the
|
|
33
|
+
# result to stdout. The replaced region is bounded at the FIRST of:
|
|
34
|
+
# - a line starting with `<!--` (the legend comment, printed and kept), or
|
|
35
|
+
# - the next heading line (^#, printed and kept), or
|
|
36
|
+
# - EOF.
|
|
37
|
+
# Bounding at the next heading matters: bodies without the legend comment used
|
|
38
|
+
# to have everything from the table to EOF silently dropped and persisted.
|
|
39
|
+
rewrite_progress_body() {
|
|
40
|
+
# Block passed via environment: BSD awk rejects literal newlines in -v
|
|
41
|
+
# assignments ("newline in string"), so -v cannot carry the table.
|
|
42
|
+
NEW_BLOCK_ENV="$1" awk '
|
|
43
|
+
BEGIN { new_block = ENVIRON["NEW_BLOCK_ENV"] }
|
|
44
|
+
BEGIN { in_progress = 0 }
|
|
45
|
+
/^### Progress/ {
|
|
46
|
+
print
|
|
47
|
+
print ""
|
|
48
|
+
print new_block
|
|
49
|
+
print ""
|
|
50
|
+
in_progress = 1
|
|
51
|
+
next
|
|
52
|
+
}
|
|
53
|
+
in_progress && /^<!--/ { in_progress = 0; print; next }
|
|
54
|
+
in_progress && /^#/ { in_progress = 0; print; next }
|
|
55
|
+
in_progress { next }
|
|
56
|
+
{ print }
|
|
57
|
+
' "$2"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
build_progress_block() {
|
|
61
|
+
cat <<EOF
|
|
62
|
+
| Flag | State |
|
|
63
|
+
|---|---|
|
|
64
|
+
| Implementation | ${1} |
|
|
65
|
+
| Testing | ${2} |
|
|
66
|
+
| Code Connect | ${3} |
|
|
67
|
+
| Wiki | ${4} |
|
|
68
|
+
EOF
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
TASK_ID="${1:-}"
|
|
33
72
|
if [ -z "$TASK_ID" ]; then
|
|
34
73
|
echo "usage: update-issue-progress.sh <task-id>" >&2
|
|
35
74
|
exit 64
|
|
36
75
|
fi
|
|
37
76
|
|
|
77
|
+
# Internal test seam: `--rewrite <body-file>` runs only the Progress-block
|
|
78
|
+
# rewrite (placeholder glyphs) and prints the result. No state, no gh.
|
|
79
|
+
if [ "$TASK_ID" = "--rewrite" ]; then
|
|
80
|
+
BODY_IN="${2:-}"
|
|
81
|
+
if [ -z "$BODY_IN" ] || [ ! -f "$BODY_IN" ]; then
|
|
82
|
+
echo "usage: update-issue-progress.sh --rewrite <body-file>" >&2
|
|
83
|
+
exit 64
|
|
84
|
+
fi
|
|
85
|
+
rewrite_progress_body "$(build_progress_block "⚪" "⚪" "⚪" "⚪")" "$BODY_IN"
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
38
89
|
# Resolve the project state directory (matches phase-tracker convention).
|
|
39
90
|
STATE_GLOB="$HOME/.claude/projects/*"
|
|
40
91
|
AGENT_STATE=""
|
|
@@ -75,9 +126,9 @@ F_WIKI=$(jq -r '.flags.wiki // false' "$AGENT_STATE")
|
|
|
75
126
|
# Each flag may be: true | false | "yellow" (validated against older impl).
|
|
76
127
|
glyph_for() {
|
|
77
128
|
case "$1" in
|
|
78
|
-
true
|
|
79
|
-
yellow
|
|
80
|
-
*)
|
|
129
|
+
true) echo "🟢" ;;
|
|
130
|
+
yellow) echo "🟡" ;;
|
|
131
|
+
*) echo "⚪" ;;
|
|
81
132
|
esac
|
|
82
133
|
}
|
|
83
134
|
|
|
@@ -87,49 +138,17 @@ G_CC=$(glyph_for "$F_CC")
|
|
|
87
138
|
G_WIKI=$(glyph_for "$F_WIKI")
|
|
88
139
|
|
|
89
140
|
# Fetch current body, replace the Progress table, write back.
|
|
90
|
-
TMP_BODY=$(mktemp /tmp/issue-progress
|
|
141
|
+
TMP_BODY=$(mktemp /tmp/issue-progress-"${TASK_ID}"-XXXXXX.md)
|
|
91
142
|
trap 'rm -f "$TMP_BODY"' EXIT
|
|
92
143
|
|
|
93
144
|
gh issue view "$ISSUE_NUM" --repo "$ORG_REPO" --json body --jq .body > "$TMP_BODY"
|
|
94
145
|
|
|
95
146
|
# Build the new Progress block.
|
|
96
|
-
NEW_BLOCK=$(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| Code Connect | ${G_CC} |
|
|
102
|
-
| Wiki | ${G_WIKI} |
|
|
103
|
-
EOF
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Replace the table between `### Progress` and the next `<!--` HTML comment.
|
|
107
|
-
# Use awk to keep the legend comment in place.
|
|
108
|
-
awk -v new_block="$NEW_BLOCK" '
|
|
109
|
-
BEGIN { in_progress = 0; replaced = 0 }
|
|
110
|
-
/^### Progress/ {
|
|
111
|
-
print
|
|
112
|
-
print ""
|
|
113
|
-
print new_block
|
|
114
|
-
print ""
|
|
115
|
-
in_progress = 1
|
|
116
|
-
replaced = 1
|
|
117
|
-
next
|
|
118
|
-
}
|
|
119
|
-
in_progress && /^<!--/ {
|
|
120
|
-
in_progress = 0
|
|
121
|
-
print
|
|
122
|
-
next
|
|
123
|
-
}
|
|
124
|
-
in_progress { next }
|
|
125
|
-
{ print }
|
|
126
|
-
END {
|
|
127
|
-
if (replaced == 0) {
|
|
128
|
-
# Body had no Progress section; nothing to replace.
|
|
129
|
-
exit 0
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
' "$TMP_BODY" > "${TMP_BODY}.new"
|
|
147
|
+
NEW_BLOCK=$(build_progress_block "$G_IMPL" "$G_TEST" "$G_CC" "$G_WIKI")
|
|
148
|
+
|
|
149
|
+
# Replace the table between `### Progress` and the first terminator (legend
|
|
150
|
+
# comment, next heading, or EOF) - see rewrite_progress_body above.
|
|
151
|
+
rewrite_progress_body "$NEW_BLOCK" "$TMP_BODY" > "${TMP_BODY}.new"
|
|
133
152
|
|
|
134
153
|
# Check if body actually changed (idempotent no-op).
|
|
135
154
|
if cmp -s "$TMP_BODY" "${TMP_BODY}.new"; then
|