@mmerterden/multi-agent-pipeline 11.3.0 → 11.3.1

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 CHANGED
@@ -14,6 +14,37 @@ Internal file-layout changes that don't affect the slash-command surface are sti
14
14
 
15
15
  ---
16
16
 
17
+ ## [11.3.1] - 2026-07-10
18
+
19
+ Self-audit patch (found by running `/multi-agent:refactor` on the pipeline itself): a data-loss fix, a latent gate regression, supply-chain hardening, and a few correctness fixes.
20
+
21
+ - **Fix (data-loss): uninstall no longer deletes `~/.claude/rules/`.** Install
22
+ treats `rules/` as user-owned (write-if-missing, never overwritten), but the
23
+ uninstaller recursively removed the whole tree, destroying personal rules
24
+ (including the git-attribution rule). Uninstall now preserves `rules/`, and
25
+ lists it in the PRESERVED summary.
26
+ - **Fix (gate regression): `lint-skills` scoped to `pipeline/skills`.** The
27
+ v11.3.0 command-layout migration made command files match `find pipeline -name
28
+ SKILL.md`; slash commands have no `name:` frontmatter, so the skill linter
29
+ started erroring on all 37 of them (latent because CI is billing-paused and
30
+ releases were published manually). The linter now only checks actual skills.
31
+ - **Fix: `.skills-index.json` is idempotent** - dropped the `generatedAt`
32
+ timestamp so regenerating an unchanged skill set produces no diff.
33
+ - **Fix: uninstall strips the Copilot pipeline block regardless of file size** -
34
+ removed the 50 KB fallback cap that orphaned the block in large
35
+ `copilot-instructions.md` files.
36
+ - **Fix: install "dev-only excluded" count** is now the real file count (the
37
+ `fixtures/` directory was under-counted as one entry).
38
+ - **Security (supply-chain):** `release.yml` publishes with `npm publish
39
+ --provenance` (`id-token: write`); all GitHub Actions pinned to commit SHAs;
40
+ added `.github/dependabot.yml` (actions + npm dev deps); `credential-store.sh`
41
+ escapes single quotes in the Windows PowerShell paths.
42
+ - **New: `eval-mine-corpus.mjs`** turns recorded triage decisions
43
+ (`triage-corpus.jsonl`) into review-gated candidate triage-eval fixtures.
44
+ - **Tests:** added node unit tests for the Phase 4 diff-risk gates
45
+ (`test/phase4-gates.test.mjs`) so they run in the release lane, not only the
46
+ bash smoke; `lint-skills` now warns (non-fatal) on terse skill descriptions.
47
+
17
48
  ## [11.3.0] - 2026-07-10
18
49
 
19
50
  Single-source skill management, an upgraded `refactor` command, and two new Phase 4 diff-risk gates.
@@ -105,9 +105,13 @@ function installScripts(pipelineSrc, dest, useSymlinks) {
105
105
  // removed upstream should not linger.
106
106
  wipeDir(dest);
107
107
  copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
108
- const scriptCount = countFiles(scriptsSrc) - DEV_ONLY_SCRIPTS.length;
108
+ // Count files actually excluded, not the array length: DEV_ONLY_SCRIPTS mixes
109
+ // individual .sh files with the `fixtures` directory (many files), so the
110
+ // array length under-counts the real exclusion.
111
+ const excludedCount = DEV_ONLY_SCRIPTS.reduce((n, name) => n + countFiles(join(scriptsSrc, name)), 0);
112
+ const scriptCount = countFiles(scriptsSrc) - excludedCount;
109
113
  console.log(
110
- ` -> ${scriptCount} files copied to ${dest} (${DEV_ONLY_SCRIPTS.length} dev-only excluded)`,
114
+ ` -> ${scriptCount} files copied to ${dest} (${excludedCount} dev-only excluded)`,
111
115
  );
112
116
  }
113
117
 
@@ -119,9 +119,13 @@ function installScripts(pipelineSrc, dest, useSymlinks) {
119
119
  if (!existsSync(scriptsSrc)) return;
120
120
  wipeDir(dest);
121
121
  copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
122
- const scriptCount = countFiles(scriptsSrc) - DEV_ONLY_SCRIPTS.length;
122
+ // Count files actually excluded, not the array length: DEV_ONLY_SCRIPTS mixes
123
+ // individual .sh files with the `fixtures` directory (many files), so the
124
+ // array length under-counts the real exclusion.
125
+ const excludedCount = DEV_ONLY_SCRIPTS.reduce((n, name) => n + countFiles(join(scriptsSrc, name)), 0);
126
+ const scriptCount = countFiles(scriptsSrc) - excludedCount;
123
127
  console.log(
124
- ` -> ${scriptCount} files copied to ${dest} (${DEV_ONLY_SCRIPTS.length} dev-only excluded)`,
128
+ ` -> ${scriptCount} files copied to ${dest} (${excludedCount} dev-only excluded)`,
125
129
  );
126
130
  }
127
131
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "11.3.0",
3
+ "version": "11.3.1",
4
4
  "description": "8-phase AI development pipeline with full orchestration on Claude Code and Copilot CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -105,9 +105,11 @@ do_get() {
105
105
  val=$(secret-tool lookup service "$key" 2>/dev/null || true)
106
106
  ;;
107
107
  windows)
108
+ # Escape single quotes for PowerShell single-quoted strings ('' = literal ').
109
+ local key_esc="${key//\'/\'\'}"
108
110
  val=$(ps_run "
109
111
  \$ErrorActionPreference='SilentlyContinue'
110
- \$c = Get-StoredCredential -Target '$key'
112
+ \$c = Get-StoredCredential -Target '$key_esc'
111
113
  if (\$c) { \$c.GetNetworkCredential().Password }
112
114
  " 2>/dev/null | tr -d '\r' || true)
113
115
  ;;
@@ -137,11 +139,13 @@ do_set() {
137
139
  printf '%s' "$val" | secret-tool store --label="$key" service "$key" >/dev/null
138
140
  ;;
139
141
  windows)
142
+ # Escape single quotes for PowerShell single-quoted strings ('' = literal ').
143
+ local key_esc="${key//\'/\'\'}" val_esc="${val//\'/\'\'}"
140
144
  # CredentialManager module: New-StoredCredential
141
145
  ps_run "
142
146
  \$ErrorActionPreference='Stop'
143
- \$pwd = ConvertTo-SecureString '$val' -AsPlainText -Force
144
- New-StoredCredential -Target '$key' -UserName '${USER:-${USERNAME:-claude}}' -SecurePassword \$pwd -Persist LocalMachine | Out-Null
147
+ \$pwd = ConvertTo-SecureString '$val_esc' -AsPlainText -Force
148
+ New-StoredCredential -Target '$key_esc' -UserName '${USER:-${USERNAME:-claude}}' -SecurePassword \$pwd -Persist LocalMachine | Out-Null
145
149
  " >/dev/null
146
150
  ;;
147
151
  *)
@@ -159,7 +163,7 @@ do_delete() {
159
163
  case "$PLATFORM" in
160
164
  macos) security delete-generic-password -s "$key" >/dev/null 2>&1 || true ;;
161
165
  linux) secret-tool clear service "$key" >/dev/null 2>&1 || true ;;
162
- windows) ps_run "Remove-StoredCredential -Target '$key' -ErrorAction SilentlyContinue" >/dev/null 2>&1 || true ;;
166
+ windows) local key_esc="${key//\'/\'\'}"; ps_run "Remove-StoredCredential -Target '$key_esc' -ErrorAction SilentlyContinue" >/dev/null 2>&1 || true ;;
163
167
  esac
164
168
  }
165
169
 
@@ -13,8 +13,7 @@
13
13
  // name - frontmatter `name:` (fallback: directory name)
14
14
  // description - frontmatter `description:` (first 300 chars)
15
15
  // platform - frontmatter `platform:` if set (ios / android / generic)
16
- // group - derived from path: core / figma-ios / figma-android /
17
- // figma-common / external
16
+ // group - derived from path: core / external
18
17
  // triggerKeywords - frontmatter `trigger-keywords:` (comma-separated
19
18
  // values lowercased) - authors can add these to tighten
20
19
  // match score. Missing → empty list.
@@ -66,9 +65,6 @@ function parseFrontmatter(raw) {
66
65
  function groupFromPath(rel) {
67
66
  if (rel.startsWith("shared/core/")) return "core";
68
67
  if (rel.startsWith("shared/external/")) return "external";
69
- if (rel.startsWith("figma-ios/")) return "figma-ios";
70
- if (rel.startsWith("figma-android/")) return "figma-android";
71
- if (rel.startsWith("figma-common/")) return "figma-common";
72
68
  return "other";
73
69
  }
74
70
 
@@ -107,7 +103,8 @@ entries.sort((a, b) => a.name.localeCompare(b.name));
107
103
 
108
104
  const json = {
109
105
  schemaVersion: "1.0.0",
110
- generatedAt: new Date().toISOString(),
106
+ // No generatedAt timestamp: the index is a committed build artifact and must
107
+ // be idempotent, so regenerating on an unchanged skill set produces no diff.
111
108
  skillCount: entries.length,
112
109
  entries,
113
110
  };
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // eval-mine-corpus.mjs - grow the triage eval set from real production runs.
3
+ //
4
+ // The pipeline already records every Phase 4 triage decision to
5
+ // `~/.claude/logs/multi-agent/triage-corpus.jsonl` (one line per finding:
6
+ // severity/file/line/issue/fix/reviewer + the classification the run gave it,
7
+ // plus an optional human `reason`/`reviewer`). Hand-authoring eval fixtures is
8
+ // the bottleneck; this closes the loop by turning those real decisions into
9
+ // CANDIDATE regression fixtures in the exact shape `eval-triage.mjs` consumes.
10
+ //
11
+ // It is review-gated on purpose: candidates land in
12
+ // `pipeline/eval/triage-candidates/<task_id>/` (which eval-triage does NOT
13
+ // walk), each validated against the triage schema. A maintainer reviews them,
14
+ // fills scope/diffSummary, and moves the good ones into
15
+ // `pipeline/eval/triage/<NN>-<name>/` to adopt them.
16
+ //
17
+ // Highest-signal cases (a human left a `reason` or `reviewer`) are emitted
18
+ // first and marked, since those encode a deliberate triage judgement.
19
+ //
20
+ // Usage:
21
+ // node pipeline/scripts/eval-mine-corpus.mjs # default corpus path
22
+ // node pipeline/scripts/eval-mine-corpus.mjs --corpus <path>
23
+ // node pipeline/scripts/eval-mine-corpus.mjs --limit 20 # cap candidates
24
+ // node pipeline/scripts/eval-mine-corpus.mjs --dry-run # report, write nothing
25
+ //
26
+ // Exit codes: 0 ok (including "nothing to mine"), 2 usage/setup error.
27
+
28
+ import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
29
+ import { dirname, join, resolve } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { homedir } from "node:os";
32
+ import { spawnSync } from "node:child_process";
33
+
34
+ const here = dirname(fileURLToPath(import.meta.url));
35
+ const root = resolve(here, "..", "..");
36
+ const evalTriageDir = join(root, "pipeline", "eval", "triage");
37
+ const candidatesDir = join(root, "pipeline", "eval", "triage-candidates");
38
+ const validator = join(root, "pipeline", "scripts", "validate-triage.mjs");
39
+
40
+ const args = process.argv.slice(2);
41
+ const opts = { corpus: null, limit: Infinity, dryRun: false };
42
+ for (let i = 0; i < args.length; i++) {
43
+ if (args[i] === "--corpus") opts.corpus = args[++i];
44
+ else if (args[i] === "--limit") opts.limit = Number(args[++i]) || Infinity;
45
+ else if (args[i] === "--dry-run") opts.dryRun = true;
46
+ else if (args[i] === "-h" || args[i] === "--help") {
47
+ console.log("Usage: eval-mine-corpus.mjs [--corpus <path>] [--limit N] [--dry-run]");
48
+ process.exit(0);
49
+ }
50
+ }
51
+
52
+ const corpusPath =
53
+ opts.corpus || join(homedir(), ".claude", "logs", "multi-agent", "triage-corpus.jsonl");
54
+
55
+ if (!existsSync(corpusPath)) {
56
+ console.log(`eval-mine-corpus: no corpus at ${corpusPath} - nothing to mine.`);
57
+ process.exit(0);
58
+ }
59
+
60
+ // Parse the JSONL corpus, skipping blank/malformed lines (never crash on data).
61
+ const rows = [];
62
+ for (const line of readFileSync(corpusPath, "utf-8").split("\n")) {
63
+ const s = line.trim();
64
+ if (!s) continue;
65
+ try {
66
+ const r = JSON.parse(s);
67
+ if (r && r.task_id && r.issue && r.classification) rows.push(r);
68
+ } catch {
69
+ /* skip malformed line */
70
+ }
71
+ }
72
+
73
+ if (rows.length === 0) {
74
+ console.log("eval-mine-corpus: corpus has no usable rows - nothing to mine.");
75
+ process.exit(0);
76
+ }
77
+
78
+ // task_ids that already have an adopted fixture: skip so we only propose new cases.
79
+ const adopted = new Set();
80
+ if (existsSync(evalTriageDir)) {
81
+ for (const name of readdirSync(evalTriageDir)) {
82
+ const inputPath = join(evalTriageDir, name, "input.json");
83
+ if (existsSync(inputPath)) {
84
+ try {
85
+ const meta = JSON.parse(readFileSync(join(evalTriageDir, name, "meta.json"), "utf-8"));
86
+ if (meta.task_id) adopted.add(meta.task_id);
87
+ } catch {
88
+ /* no meta.json -> can't dedup by task_id, that's fine */
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // Group corpus rows by task_id.
95
+ const byTask = new Map();
96
+ for (const r of rows) {
97
+ if (!byTask.has(r.task_id)) byTask.set(r.task_id, []);
98
+ byTask.get(r.task_id).push(r);
99
+ }
100
+
101
+ const toFinding = (r) => ({
102
+ severity: r.severity,
103
+ file: r.file,
104
+ // Triage contract requires an integer line >= 0; corpus may carry null for a
105
+ // file-level finding, so coerce to 0 (the schema's file-level sentinel).
106
+ line: Number.isInteger(r.line) ? r.line : 0,
107
+ issue: r.issue,
108
+ fix: r.fix ?? null,
109
+ reviewer: r.reviewer ?? null,
110
+ });
111
+
112
+ // Build a candidate per task. Highest-signal first (has a human reason/reviewer).
113
+ const candidates = [];
114
+ for (const [taskId, findings] of byTask) {
115
+ if (adopted.has(taskId)) continue;
116
+ const rawFindings = findings.map(toFinding);
117
+ const accepted = [];
118
+ const deferred = [];
119
+ const rejected = [];
120
+ for (const r of findings) {
121
+ if (r.classification === "accepted") accepted.push(toFinding(r));
122
+ else if (r.classification === "deferred")
123
+ deferred.push({ finding: toFinding(r), reason: r.reason || "TODO: reason (from corpus)" });
124
+ else if (r.classification === "rejected")
125
+ rejected.push({ finding: toFinding(r), reason: r.reason || "TODO: reason (from corpus)" });
126
+ }
127
+ const hasHumanSignal = findings.some((r) => r.reason || r.reviewer);
128
+ const title = findings.find((f) => f.task_title)?.task_title || taskId;
129
+ candidates.push({
130
+ taskId,
131
+ hasHumanSignal,
132
+ input: {
133
+ rawFindings,
134
+ // Corpus does not record scope/diff; a maintainer fills these before adopting.
135
+ scope: `TODO: scope for ${title}`,
136
+ diffSummary: "TODO: diff summary",
137
+ },
138
+ expected: {
139
+ accepted,
140
+ deferred,
141
+ rejected,
142
+ // Blockers loop back to dev, so a run with an accepted blocker is not approved.
143
+ approved: accepted.every((f) => f.severity !== "blocking"),
144
+ },
145
+ });
146
+ }
147
+
148
+ candidates.sort((a, b) => Number(b.hasHumanSignal) - Number(a.hasHumanSignal));
149
+ const selected = candidates.slice(0, opts.limit);
150
+
151
+ if (selected.length === 0) {
152
+ console.log("eval-mine-corpus: no new task_ids to propose (all already adopted).");
153
+ process.exit(0);
154
+ }
155
+
156
+ let written = 0;
157
+ let invalid = 0;
158
+ for (const c of selected) {
159
+ const safe = c.taskId.replace(/[^A-Za-z0-9._-]/g, "_");
160
+ const dir = join(candidatesDir, safe);
161
+ const tag = c.hasHumanSignal ? " [human-signal]" : "";
162
+ if (opts.dryRun) {
163
+ console.log(` would emit candidate: ${safe} (${c.input.rawFindings.length} findings)${tag}`);
164
+ continue;
165
+ }
166
+ mkdirSync(dir, { recursive: true });
167
+ writeFileSync(join(dir, "input.json"), JSON.stringify(c.input, null, 2) + "\n");
168
+ writeFileSync(join(dir, "expected.json"), JSON.stringify(c.expected, null, 2) + "\n");
169
+ writeFileSync(
170
+ join(dir, "meta.json"),
171
+ JSON.stringify({ task_id: c.taskId, source: "eval-mine-corpus", hasHumanSignal: c.hasHumanSignal }, null, 2) + "\n",
172
+ );
173
+ // Validate the emitted expected.json against the triage contract; flag if bad.
174
+ const res = spawnSync("node", [validator, join(dir, "expected.json")], { encoding: "utf-8" });
175
+ const ok = res.status === 0;
176
+ if (!ok) invalid++;
177
+ console.log(` emitted: triage-candidates/${safe}${tag} ${ok ? "(schema OK)" : "(SCHEMA FAIL - review)"}`);
178
+ written++;
179
+ }
180
+
181
+ if (!opts.dryRun) {
182
+ console.log("");
183
+ console.log(`eval-mine-corpus: ${written} candidate(s) written to pipeline/eval/triage-candidates/`);
184
+ if (invalid > 0) console.log(` ${invalid} failed schema validation - fix before adopting.`);
185
+ console.log(" Review each, fill scope/diffSummary, then move good ones into");
186
+ console.log(" pipeline/eval/triage/<NN>-<name>/ to add them to the eval suite.");
187
+ }
@@ -6,12 +6,12 @@
6
6
  .claude/multi-agent-refs 50
7
7
  .claude/rules 12
8
8
  .claude/schemas 23
9
- .claude/scripts 177
9
+ .claude/scripts 178
10
10
  .claude/settings.json 1
11
11
  .claude/skills 428
12
12
  .copilot/agents 8
13
13
  .copilot/copilot-instructions.md 1
14
14
  .copilot/lib 24
15
15
  .copilot/schemas 23
16
- .copilot/scripts 177
16
+ .copilot/scripts 178
17
17
  .copilot/skills 465
@@ -21,7 +21,11 @@ import { dirname, join } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
22
 
23
23
  const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
24
- const files = execSync("find pipeline -name SKILL.md", { cwd: repoRoot, encoding: "utf-8" })
24
+ // Scope to pipeline/skills only. Slash commands under pipeline/commands also use
25
+ // the <name>/SKILL.md layout now, but they are command definitions
26
+ // (description + allowed-tools, name derived from the dir), NOT skills, so the
27
+ // skill frontmatter contract (required `name:`) does not apply to them.
28
+ const files = execSync("find pipeline/skills -name SKILL.md", { cwd: repoRoot, encoding: "utf-8" })
25
29
  .trim()
26
30
  .split("\n")
27
31
  .filter(Boolean)
@@ -29,6 +33,10 @@ const files = execSync("find pipeline -name SKILL.md", { cwd: repoRoot, encoding
29
33
 
30
34
  const errors = [];
31
35
  const fail = (file, msg) => errors.push(`${file}: ${msg}`);
36
+ // Non-fatal quality signals (a terse description routes poorly). Warnings never
37
+ // fail the gate; they surface skills whose frontmatter could trigger better.
38
+ const warnings = [];
39
+ const warn = (file, msg) => warnings.push(`${file}: ${msg}`);
32
40
 
33
41
  for (const file of files) {
34
42
  const raw = readFileSync(join(repoRoot, file), "utf-8");
@@ -61,6 +69,18 @@ for (const file of files) {
61
69
  fail(file, "description is empty");
62
70
  } else if (fields.description.length > 1024) {
63
71
  fail(file, `description exceeds 1024 chars (${fields.description.length})`);
72
+ } else {
73
+ // Routing quality: a description of a few words rarely says what the skill
74
+ // does AND when to use it, so the model cannot decide to trigger it.
75
+ // Skip YAML block scalars (`description: |` / `>`): this line-based parser
76
+ // only sees the indicator, not the real multi-line text, so a word count
77
+ // would be a false positive.
78
+ const desc = fields.description;
79
+ const isBlockScalar = /^[|>][+-]?\d*$/.test(desc);
80
+ const words = desc.split(/\s+/).filter(Boolean).length;
81
+ if (!isBlockScalar && words < 6) {
82
+ warn(file, `description is terse (${words} words) - say what it does AND when to use it`);
83
+ }
64
84
  }
65
85
 
66
86
  if ("user-invocable" in fields && !["true", "false"].includes(fields["user-invocable"])) {
@@ -72,6 +92,11 @@ for (const file of files) {
72
92
  }
73
93
  }
74
94
 
95
+ if (warnings.length > 0) {
96
+ console.warn(`lint-skills: ${warnings.length} warning(s) (non-fatal):`);
97
+ for (const w of warnings) console.warn(" " + w);
98
+ }
99
+
75
100
  if (errors.length > 0) {
76
101
  console.error(`lint-skills: ${errors.length} error(s) across ${files.length} SKILL.md files`);
77
102
  for (const e of errors) console.error(" " + e);
@@ -134,8 +134,13 @@ function stripManagedBlock(filePath) {
134
134
  const re =
135
135
  /\s*<!-- multi-agent-pipeline:begin -->[\s\S]*?<!-- multi-agent-pipeline:end -->\s*/m;
136
136
  if (!re.test(content)) {
137
- // Legacy: maybe the whole file is ours (Copilot wrote it before markers)
138
- if (content.includes("# Multi-Agent Development Pipeline") && content.length < 50000) {
137
+ // Copilot's install (install/copilot.mjs) does not wrap the block in
138
+ // begin/end markers; it always writes the pipeline section LAST, from the
139
+ // `# Multi-Agent Development Pipeline` heading to EOF, keeping any user
140
+ // content ABOVE it. So slicing from that heading to EOF reliably removes
141
+ // only our section regardless of file size (no arbitrary size cap - a cap
142
+ // used to orphan the block in files >50 KB).
143
+ if (content.includes("# Multi-Agent Development Pipeline")) {
139
144
  // Heuristic: Copilot's generateCopilotInstructions output. Only strip the
140
145
  // pipeline section, not the whole file.
141
146
  const marker = "# Multi-Agent Development Pipeline";
@@ -244,8 +249,9 @@ async function main() {
244
249
  console.log(" PRESERVED (never touched):");
245
250
  console.log(" - Personal access tokens in OS credential store (Keychain / Credential Manager / libsecret)");
246
251
  console.log(" - ~/.claude/CLAUDE.md (your customizations)");
252
+ console.log(" - ~/.claude/rules/ (user-owned; installed write-if-missing, never overwritten)");
247
253
  console.log(" - ~/.claude/multi-agent-preferences.json (your settings)");
248
- console.log(" - User content outside <!-- multi-agent-pipeline:begin/end --> markers");
254
+ console.log(" - Your own content in copilot-instructions.md above the pipeline section");
249
255
  console.log("");
250
256
 
251
257
  if (!(await confirm(" Proceed? (y/N) "))) {
@@ -259,7 +265,12 @@ async function main() {
259
265
  const CLAUDE = join(HOME, ".claude");
260
266
  rmIfExists(join(CLAUDE, "commands", "multi-agent"));
261
267
  rmIfExists(join(CLAUDE, "scripts"));
262
- rmIfExists(join(CLAUDE, "rules"));
268
+ // NOTE: ~/.claude/rules/ is USER-OWNED. install lays baseline rules down
269
+ // write-if-missing and NEVER overwrites them (install/claude.mjs
270
+ // installRules, skipExisting) so users can evolve their global rules
271
+ // locally. Deleting the tree here would destroy those personal edits
272
+ // (e.g. the git-attribution rule) -- asymmetric data loss. Leave rules
273
+ // in place on uninstall, exactly like CLAUDE.md and preferences.
263
274
  const skills = join(CLAUDE, "skills");
264
275
  let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
265
276
  n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-07-09T19:01:45.304Z",
4
3
  "skillCount": 187,
5
4
  "entries": [
6
5
  {
@@ -968,7 +967,7 @@
968
967
  },
969
968
  {
970
969
  "name": "multi-agent-refactor",
971
- "description": "Analyse the project, score it, draft an improvement plan, take approval, develop, then ask whether to sync.",
970
+ "description": "Analyse the project: extract adapted best-practices, hunt real bugs + improvement areas, check upstream drift of derived skills, draft one plan, take approval, develop, then ask whether to sync.",
972
971
  "platform": null,
973
972
  "group": "core",
974
973
  "triggerKeywords": [],
@@ -114,7 +114,7 @@
114
114
  | core | `multi-agent-manual-test` | - | Switch to the active task's branch and prepare it for manual testing in Xcode. Phase 5 standalone (the UI Bug Hunter lives at multi-agent-te |
115
115
  | core | `multi-agent-prune-logs` | - | Delete per-task project logs under ~/.claude/logs/multi-agent (filter by age/project/task). Audit trail + metrics are preserved. Dry-run fir |
116
116
  | core | `multi-agent-purge` | - | ⚠️ Wipes every worktree, branch, log, and state file. Irreversible; asks for double confirmation. |
117
- | core | `multi-agent-refactor` | - | Analyse the project, score it, draft an improvement plan, take approval, develop, then ask whether to sync. |
117
+ | core | `multi-agent-refactor` | - | Analyse the project: extract adapted best-practices, hunt real bugs + improvement areas, check upstream drift of derived skills, draft one p |
118
118
  | core | `multi-agent-resume` | - | Resume a stopped or failed task from the phase where it left off. |
119
119
  | core | `multi-agent-review` | - | Run parallel review on the current branch's diff: 2 models on Claude Code (Fable + Sonnet), 3 models on Copilot CLI (GPT + Opus + Sonnet). R |
120
120
  | core | `multi-agent-scan` | - | Skill security scan: walks local skill directories against a tiered pattern catalog. |