@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 +31 -0
- package/install/claude.mjs +6 -2
- package/install/copilot.mjs +6 -2
- package/package.json +1 -1
- package/pipeline/lib/credential-store.sh +8 -4
- package/pipeline/scripts/build-skills-index.mjs +3 -6
- package/pipeline/scripts/eval-mine-corpus.mjs +187 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +2 -2
- package/pipeline/scripts/lint-skills.mjs +26 -1
- package/pipeline/scripts/uninstall.mjs +15 -4
- package/pipeline/skills/.skills-index.json +1 -2
- package/pipeline/skills/skills-index.md +1 -1
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.
|
package/install/claude.mjs
CHANGED
|
@@ -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
|
-
|
|
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} (${
|
|
114
|
+
` -> ${scriptCount} files copied to ${dest} (${excludedCount} dev-only excluded)`,
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
|
package/install/copilot.mjs
CHANGED
|
@@ -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
|
-
|
|
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} (${
|
|
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.
|
|
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 '$
|
|
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 '$
|
|
144
|
-
New-StoredCredential -Target '$
|
|
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 '$
|
|
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 /
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
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(" -
|
|
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
|
-
|
|
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,
|
|
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
|
|
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. |
|