@mmerterden/multi-agent-pipeline 13.2.0 → 13.3.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 +50 -0
- package/install/_plugin-skills.mjs +246 -0
- package/install/codex.mjs +46 -6
- package/install/copilot.mjs +28 -7
- package/install/templates/codex-instructions.md +23 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +50 -50
- package/pipeline/multi-agent-refs/cross-cli-contract.md +32 -1
- package/pipeline/scripts/build-stack-plugins.mjs +50 -4
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +50 -50
- package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +37 -37
- package/pipeline/skills/shared/external/ios-coding-standard/references/lint-local.sh +5 -5
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +108 -108
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,56 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [13.3.0] - 2026-07-28
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Copilot CLI and Codex CLI now carry the stack plugin's authored skills.** Claude Code
|
|
24
|
+
loads `multi-agent-plugins` natively; the other two hosts could not, and the contract
|
|
25
|
+
papered over it by claiming Copilot kept standalone `figma-*` copies as a "frozen
|
|
26
|
+
fallback". `install/copilot.mjs` pruned exactly those directories, so Copilot had
|
|
27
|
+
**zero** plugin-authored skills: `create-screen`, `figma-validate`, `figma-review`,
|
|
28
|
+
`component`, `state` and `navigation` were all absent, and a component task on Copilot
|
|
29
|
+
had nothing to dispatch to. New shared `install/_plugin-skills.mjs` delivers the
|
|
30
|
+
`index` / `reference` / `workflow` / `tools` groups to both hosts - as skills on
|
|
31
|
+
Copilot, as reference files on Codex, where the skills block truncates silently.
|
|
32
|
+
- Plugin selection reads `enabledPlugins` from `~/.claude/settings.json` rather than
|
|
33
|
+
copying every stack plugin. A flat copy of all five is last-write-wins, and `fix-bug`
|
|
34
|
+
plus `branch-and-pr` exist in four of them while `component`, `state` and
|
|
35
|
+
`create-component` exist in three - an iOS repo could have ended up running the
|
|
36
|
+
Android `create-component`. The `--platform` flag is now only the fallback for a
|
|
37
|
+
machine with no Claude Code settings to read.
|
|
38
|
+
- `test/stack-content-sync.test.mjs` - three cases locking content propagation,
|
|
39
|
+
content-only version bumps, and no-op idempotence in the stack-plugin generator.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`build-stack-plugins.mjs` never propagated content edits.** It copied a skill only
|
|
44
|
+
when the skill *set* changed, so editing a routed skill in `shared/external` reached
|
|
45
|
+
no plugin while the generator reported "all plugins up to date" - which read as
|
|
46
|
+
confirmation. This falsified the single-authoring-source guarantee the whole
|
|
47
|
+
`shared/external` design rests on. Found by converting banned punctuation in
|
|
48
|
+
`ios-coding-standard` and seeing the plugin copy keep all 188 banned characters with a
|
|
49
|
+
differing `rules.yml`. Fixing it also surfaced 37 stale skill descriptions across four
|
|
50
|
+
plugins, still missing the "Use when ..." routing clause that decides whether a skill
|
|
51
|
+
is matched at all: those plugins had been serving worse descriptions than the source
|
|
52
|
+
for as long as the routing clause existed.
|
|
53
|
+
- `build-stack-plugins.mjs` now accepts `--key=value` as well as `--key value`. The `=`
|
|
54
|
+
form fell through to the default, so a run aimed at a throwaway checkout silently
|
|
55
|
+
retargeted the user's real marketplace and reported success.
|
|
56
|
+
- `smoke-install-layout.sh` asserts plugin-skill parity against a seeded probe plugin,
|
|
57
|
+
so the check cannot pass vacuously in an isolated `HOME` the way it did on first write.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- `cross-cli-contract.md` §1.1 replaced the false "frozen fallback" claim with a
|
|
62
|
+
per-host delivery table: native plugin load on Claude Code, installer-delivered skills
|
|
63
|
+
on Copilot CLI, installer-delivered refs on Codex CLI. Parity is asserted on the
|
|
64
|
+
reference set, not the skill set, because Codex ships one router skill by design.
|
|
65
|
+
- `codex-instructions.md` gained a "the skills are on disk, not in the list" section
|
|
66
|
+
with an `ls` / `grep` / `head` discovery recipe: only 5 of Codex's skill entries render
|
|
67
|
+
descriptions, so the 193 delivered refs are reachable but not description-matched.
|
|
68
|
+
|
|
19
69
|
## [13.2.0] - 2026-07-28
|
|
20
70
|
|
|
21
71
|
The iOS coding standard reaches all three hosts, and works outside the pipeline.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack-plugin skill delivery for the hosts that cannot load a marketplace plugin.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code loads `{owner}/multi-agent-plugins` natively, so it gets all of a
|
|
5
|
+
* stack plugin's skills. The other two hosts do not:
|
|
6
|
+
*
|
|
7
|
+
* - **Copilot CLI** has no plugin loader at all. The contract used to say it kept
|
|
8
|
+
* standalone `figma-*` copies as a "frozen fallback", but `install/copilot.mjs`
|
|
9
|
+
* pruned exactly those directories, so in practice Copilot had **zero**
|
|
10
|
+
* plugin-authored skills - `create-screen`, `figma-validate`, `figma-review`,
|
|
11
|
+
* `component`, `state` and `navigation` were all absent. A component task on
|
|
12
|
+
* Copilot therefore had nothing to dispatch to.
|
|
13
|
+
* - **Codex CLI** can install the plugin, but it lists plugin skills by name only
|
|
14
|
+
* (measured: all 76 plugin-provided skills render with an empty description while
|
|
15
|
+
* the 5 system skills render theirs), and its skills block truncates silently
|
|
16
|
+
* once it overflows. So Codex takes the same skills as **reference files**.
|
|
17
|
+
*
|
|
18
|
+
* What is copied, and what is deliberately not: a stack plugin's `knowledge/` tree
|
|
19
|
+
* is generated from the pipeline's own `skills/shared/external`, which the installer
|
|
20
|
+
* already lays down on Claude Code and Copilot CLI. Copying it again would duplicate
|
|
21
|
+
* ~5 MB the host already has. Only the plugin's **authored** lifecycle skills are
|
|
22
|
+
* copied - `index`, `reference/`, `workflow/`, `tools/` - which is 56 files.
|
|
23
|
+
*
|
|
24
|
+
* @module install/_plugin-skills
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
|
|
30
|
+
import { copyDir, countFiles, ensureDir, ensureRealDir, isDryRun } from "./_common.mjs";
|
|
31
|
+
|
|
32
|
+
/** Subtrees a plugin authors itself. `knowledge/` is generated, so it is excluded. */
|
|
33
|
+
export const AUTHORED_GROUPS = Object.freeze(["index", "reference", "workflow", "tools"]);
|
|
34
|
+
|
|
35
|
+
/** Stack plugins, and the platform each belongs to. */
|
|
36
|
+
export const STACK_PLUGINS = Object.freeze([
|
|
37
|
+
{ name: "ai-ios-engineering-toolkit", platform: "ios" },
|
|
38
|
+
{ name: "ai-android-engineering-toolkit", platform: "android" },
|
|
39
|
+
{ name: "ai-frontend-engineering-toolkit", platform: "all" },
|
|
40
|
+
{ name: "ai-backend-toolkit", platform: "all" },
|
|
41
|
+
{ name: "ai-common-engineering-toolkit", platform: "all" },
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compare two semver-ish directory names, newest last.
|
|
46
|
+
* @param {string} a
|
|
47
|
+
* @param {string} b
|
|
48
|
+
*/
|
|
49
|
+
function semverCompare(a, b) {
|
|
50
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
51
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
52
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
53
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
54
|
+
if (d !== 0) return d;
|
|
55
|
+
}
|
|
56
|
+
return a.localeCompare(b);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find a plugin's `skills/` directory.
|
|
61
|
+
*
|
|
62
|
+
* A local checkout is preferred over the host's plugin cache: the checkout is what
|
|
63
|
+
* `/multi-agent:sync` rebuilds, so it is the newest thing on disk, while the cache
|
|
64
|
+
* only advances when the marketplace is refreshed. Cache dirs are version-named, so
|
|
65
|
+
* the newest is picked rather than whichever `readdir` happened to return first.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} home
|
|
68
|
+
* @param {string} pluginName
|
|
69
|
+
* @returns {{path: string, source: string}|null}
|
|
70
|
+
*/
|
|
71
|
+
export function resolvePluginSkills(home, pluginName) {
|
|
72
|
+
const checkout = join(home, "multi-agent-plugins", "plugins", pluginName, "skills");
|
|
73
|
+
if (existsSync(checkout)) return { path: checkout, source: "local checkout" };
|
|
74
|
+
|
|
75
|
+
const cacheRoot = join(home, ".claude", "plugins", "cache", "multi-agent-plugins", pluginName);
|
|
76
|
+
if (existsSync(cacheRoot)) {
|
|
77
|
+
const versions = readdirSync(cacheRoot)
|
|
78
|
+
.filter((v) => {
|
|
79
|
+
try {
|
|
80
|
+
return statSync(join(cacheRoot, v)).isDirectory();
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.sort(semverCompare);
|
|
86
|
+
for (const v of versions.reverse()) {
|
|
87
|
+
const p = join(cacheRoot, v, "skills");
|
|
88
|
+
if (existsSync(p)) return { path: p, source: `plugin cache ${v}` };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Which plugins to deliver.
|
|
96
|
+
*
|
|
97
|
+
* **Enabled set first, platform second.** Claude Code activates only the plugins a
|
|
98
|
+
* repo enables, so delivering every stack plugin to another host would give it MORE
|
|
99
|
+
* than Claude Code has - and worse, it would collide: `fix-bug` and `branch-and-pr`
|
|
100
|
+
* exist in four stack plugins, `component`, `state` and `create-component` in three.
|
|
101
|
+
* A flat copy of all of them is last-write-wins, so an iOS repo could end up running
|
|
102
|
+
* the Android `create-component`.
|
|
103
|
+
*
|
|
104
|
+
* So the enabled list in Claude Code's settings is the source of truth; the
|
|
105
|
+
* `--platform` flag is only the fallback for a machine that has no Claude Code
|
|
106
|
+
* settings to read.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} home
|
|
109
|
+
* @param {"ios"|"android"|"all"} platformFlag
|
|
110
|
+
* @returns {{names: string[], source: string}}
|
|
111
|
+
*/
|
|
112
|
+
export function pluginsToDeliver(home, platformFlag) {
|
|
113
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
114
|
+
if (existsSync(settingsPath)) {
|
|
115
|
+
try {
|
|
116
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
117
|
+
const enabled = Object.entries(settings.enabledPlugins || {})
|
|
118
|
+
.filter(([, on]) => on === true)
|
|
119
|
+
// keys look like `ai-ios-engineering-toolkit@multi-agent-plugins`
|
|
120
|
+
.map(([k]) => k.split("@")[0])
|
|
121
|
+
.filter((n) => STACK_PLUGINS.some((p) => p.name === n));
|
|
122
|
+
if (enabled.length > 0) {
|
|
123
|
+
// Deterministic order, stack plugin before the shared one: on a name
|
|
124
|
+
// collision the first delivery wins, and the stack-specific version is the
|
|
125
|
+
// one the repo actually wants.
|
|
126
|
+
const ordered = STACK_PLUGINS.map((p) => p.name).filter((n) => enabled.includes(n));
|
|
127
|
+
return { names: ordered, source: "enabledPlugins in ~/.claude/settings.json" };
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
/* unreadable settings fall through to the platform default */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const names = STACK_PLUGINS.filter(
|
|
134
|
+
(p) => p.platform === "all" || platformFlag === "all" || p.platform === platformFlag,
|
|
135
|
+
).map((p) => p.name);
|
|
136
|
+
return { names, source: `--platform=${platformFlag} default (no enabled plugin list found)` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Copy the authored lifecycle skills of every applicable stack plugin into a flat
|
|
141
|
+
* destination, one directory per skill.
|
|
142
|
+
*
|
|
143
|
+
* Flat rather than group-nested because that is how both hosts discover skills:
|
|
144
|
+
* `<dest>/<skill-name>/SKILL.md`. The group is an authoring detail.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} opts
|
|
147
|
+
* @param {string} opts.home
|
|
148
|
+
* @param {string} opts.dest - flat skills directory
|
|
149
|
+
* @param {"ios"|"android"|"all"} opts.platformFlag
|
|
150
|
+
* @param {string} opts.label - host label for log lines
|
|
151
|
+
* @param {Set<string>} [opts.skipNames] - skill names the host already has
|
|
152
|
+
* @returns {{copied: number, skipped: number, plugins: string[], missing: string[]}}
|
|
153
|
+
*/
|
|
154
|
+
export function installAuthoredPluginSkills(opts) {
|
|
155
|
+
const { home, dest, platformFlag, label, skipNames = new Set() } = opts;
|
|
156
|
+
const { names: wanted, source: selectionSource } = pluginsToDeliver(home, platformFlag);
|
|
157
|
+
|
|
158
|
+
let copied = 0;
|
|
159
|
+
let skipped = 0;
|
|
160
|
+
let collided = 0;
|
|
161
|
+
const plugins = [];
|
|
162
|
+
const missing = [];
|
|
163
|
+
// Names delivered in THIS pass, so a later plugin cannot overwrite an earlier
|
|
164
|
+
// one's skill of the same name.
|
|
165
|
+
const delivered = new Set();
|
|
166
|
+
|
|
167
|
+
for (const pluginName of wanted) {
|
|
168
|
+
const resolved = resolvePluginSkills(home, pluginName);
|
|
169
|
+
if (!resolved) {
|
|
170
|
+
missing.push(pluginName);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
let perPlugin = 0;
|
|
174
|
+
for (const group of AUTHORED_GROUPS) {
|
|
175
|
+
const groupDir = join(resolved.path, group);
|
|
176
|
+
if (!existsSync(groupDir)) continue;
|
|
177
|
+
|
|
178
|
+
// `index` is a single skill directory, the others hold one per child.
|
|
179
|
+
const entries =
|
|
180
|
+
group === "index"
|
|
181
|
+
? existsSync(join(groupDir, "SKILL.md"))
|
|
182
|
+
? [{ name: "index", from: groupDir }]
|
|
183
|
+
: []
|
|
184
|
+
: readdirSync(groupDir, { withFileTypes: true })
|
|
185
|
+
.filter((e) => e.isDirectory())
|
|
186
|
+
.map((e) => ({ name: e.name, from: join(groupDir, e.name) }));
|
|
187
|
+
|
|
188
|
+
for (const { name, from } of entries) {
|
|
189
|
+
if (skipNames.has(name)) {
|
|
190
|
+
skipped++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (delivered.has(name)) {
|
|
194
|
+
collided++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
delivered.add(name);
|
|
198
|
+
const to = join(dest, name);
|
|
199
|
+
ensureDir(dest);
|
|
200
|
+
copyDir(from, to, {});
|
|
201
|
+
copied += countFiles(from);
|
|
202
|
+
perPlugin++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (perPlugin > 0) plugins.push(`${pluginName} (${resolved.source}, ${perPlugin} skills)`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isDryRun()) {
|
|
209
|
+
console.log(` [dry-run] would copy authored plugin skills into ${dest}`);
|
|
210
|
+
}
|
|
211
|
+
console.log(` -> ${label}: delivering from ${selectionSource}`);
|
|
212
|
+
for (const p of plugins) console.log(` -> ${label}: ${p}`);
|
|
213
|
+
if (collided > 0) {
|
|
214
|
+
console.log(
|
|
215
|
+
` -> ${label}: ${collided} name collision(s) resolved in favour of the earlier plugin`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (skipped > 0) {
|
|
219
|
+
console.log(` -> ${label}: skipped ${skipped} skill(s) the host already carries`);
|
|
220
|
+
}
|
|
221
|
+
if (missing.length > 0) {
|
|
222
|
+
console.log(
|
|
223
|
+
` -> ${label}: ${missing.length} plugin(s) not on disk (${missing.join(", ")}); ` +
|
|
224
|
+
`clone {owner}/multi-agent-plugins or enable them in Claude Code to deliver their skills`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return { copied, skipped, collided, plugins, missing, selectionSource };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Names already present in a flat skills directory, so a second delivery pass does
|
|
232
|
+
* not overwrite what the pipeline itself installed.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} dir
|
|
235
|
+
* @returns {Set<string>}
|
|
236
|
+
*/
|
|
237
|
+
export function existingSkillNames(dir) {
|
|
238
|
+
if (!existsSync(dir)) return new Set();
|
|
239
|
+
return new Set(
|
|
240
|
+
readdirSync(dir, { withFileTypes: true })
|
|
241
|
+
.filter((e) => e.isDirectory())
|
|
242
|
+
.map((e) => e.name),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { ensureRealDir };
|
package/install/codex.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./_common.mjs";
|
|
34
34
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
35
35
|
import { installCodexAgents } from "./_codex-agents.mjs";
|
|
36
|
+
import { existingSkillNames, installAuthoredPluginSkills } from "./_plugin-skills.mjs";
|
|
36
37
|
import { generateCodexInstructions } from "./_codex-instructions.mjs";
|
|
37
38
|
import { mergeManagedBlock } from "./_managed-block.mjs";
|
|
38
39
|
|
|
@@ -168,12 +169,11 @@ export function toCodexSkill(raw, name) {
|
|
|
168
169
|
* }} ctx
|
|
169
170
|
*/
|
|
170
171
|
export function installCodex(ctx) {
|
|
171
|
-
// `indexOnly`
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
const { home, pipelineSrc, useSymlinks } = ctx;
|
|
172
|
+
// `indexOnly` is intentionally not read: it exists to reduce the skills tree to
|
|
173
|
+
// its index, and this target delivers skills as reference files rather than as
|
|
174
|
+
// block entries, so there is nothing for it to shrink. `platformFlag` IS used -
|
|
175
|
+
// it selects which stack plugin's authored skills are delivered.
|
|
176
|
+
const { home, pipelineSrc, useSymlinks, platformFlag } = ctx;
|
|
177
177
|
|
|
178
178
|
const CODEX_DIR = join(home, ".codex");
|
|
179
179
|
const CODEX_SKILLS = join(CODEX_DIR, "skills");
|
|
@@ -184,6 +184,7 @@ export function installCodex(ctx) {
|
|
|
184
184
|
const CODEX_LIB = join(CODEX_DIR, "lib");
|
|
185
185
|
const CODEX_SCHEMAS = join(CODEX_DIR, "schemas");
|
|
186
186
|
const CODEX_RULES = join(CODEX_DIR, "rules");
|
|
187
|
+
const CODEX_SKILL_REFS = join(CODEX_MA_REFS, "skills");
|
|
187
188
|
|
|
188
189
|
console.log(" [Codex CLI] Installing pipeline orchestrator...");
|
|
189
190
|
ensureDir(CODEX_DIR);
|
|
@@ -192,6 +193,7 @@ export function installCodex(ctx) {
|
|
|
192
193
|
installRefs(pipelineSrc, CODEX_MA_REFS);
|
|
193
194
|
installPersonas(pipelineSrc, CODEX_AGENTS);
|
|
194
195
|
installPrompt(CODEX_PROMPTS);
|
|
196
|
+
installSkillRefs(pipelineSrc, CODEX_SKILL_REFS, home, platformFlag);
|
|
195
197
|
installScripts(pipelineSrc, CODEX_SCRIPTS, useSymlinks);
|
|
196
198
|
installTree("lib", pipelineSrc, CODEX_LIB, useSymlinks);
|
|
197
199
|
installTree("schemas", pipelineSrc, CODEX_SCHEMAS, useSymlinks);
|
|
@@ -357,6 +359,44 @@ function installPrompt(promptsDir) {
|
|
|
357
359
|
console.log(` -> /multi-agent prompt written to ${promptsDir}`);
|
|
358
360
|
}
|
|
359
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Deliver every skill Claude Code and Copilot CLI get as a **reference file** under
|
|
364
|
+
* `~/.codex/multi-agent-refs/skills/`, at zero skills-block cost.
|
|
365
|
+
*
|
|
366
|
+
* Codex cannot hold the list. Measured on 0.145: one plugin declaring 142 skills
|
|
367
|
+
* surfaced 75 of them and evicted an unrelated user-scope skill, and every
|
|
368
|
+
* plugin-provided skill renders with an empty description, so the block buys nothing
|
|
369
|
+
* even when it fits. Parity on this host therefore means **reachability**, not an
|
|
370
|
+
* identical listing: the router reads a skill by path the moment it needs one, the
|
|
371
|
+
* same way it reads a phase spec.
|
|
372
|
+
*
|
|
373
|
+
* Two sources, so the set matches the other hosts exactly:
|
|
374
|
+
* - the pipeline's own `skills/shared/external` (what `~/.claude/skills` receives)
|
|
375
|
+
* - the enabled stack plugin's authored skills (what the marketplace provides)
|
|
376
|
+
*/
|
|
377
|
+
function installSkillRefs(pipelineSrc, dest, home, platformFlag) {
|
|
378
|
+
const externalSrc = join(pipelineSrc, "skills", "shared", "external");
|
|
379
|
+
|
|
380
|
+
ensureRealDir(dest);
|
|
381
|
+
ensureDir(dest);
|
|
382
|
+
wipeDir(dest);
|
|
383
|
+
|
|
384
|
+
let count = 0;
|
|
385
|
+
if (existsSync(externalSrc)) count += copyTreeRewritten(externalSrc, dest);
|
|
386
|
+
|
|
387
|
+
const res = installAuthoredPluginSkills({
|
|
388
|
+
home,
|
|
389
|
+
dest,
|
|
390
|
+
platformFlag,
|
|
391
|
+
label: "Codex CLI",
|
|
392
|
+
skipNames: existingSkillNames(dest),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
console.log(
|
|
396
|
+
` -> ${count + res.copied} skill file(s) copied to ${dest} as refs (0 skills-block cost)`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
360
400
|
/**
|
|
361
401
|
* Copy `pipeline/scripts` minus the maintainer-only set.
|
|
362
402
|
*
|
package/install/copilot.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { copyExternalSkillsFiltered } from "./_platform-filter.mjs";
|
|
|
25
25
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
26
26
|
import { generateCopilotInstructions } from "./_copilot-instructions.mjs";
|
|
27
27
|
import { legacyTrailingContent, mergeManagedBlock } from "./_managed-block.mjs";
|
|
28
|
+
import { existingSkillNames, installAuthoredPluginSkills } from "./_plugin-skills.mjs";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* @param {{
|
|
@@ -54,7 +55,7 @@ export function installCopilot(ctx) {
|
|
|
54
55
|
installAgents(pipelineSrc, COPILOT_AGENTS, useSymlinks);
|
|
55
56
|
installSchemas(pipelineSrc, COPILOT_SCHEMAS, useSymlinks);
|
|
56
57
|
installLib(pipelineSrc, COPILOT_LIB, useSymlinks);
|
|
57
|
-
installSkills({ pipelineSrc, dest: COPILOT_SKILLS, indexOnly, useSymlinks, platformFlag });
|
|
58
|
+
installSkills({ home, pipelineSrc, dest: COPILOT_SKILLS, indexOnly, useSymlinks, platformFlag });
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/** Start of the pipeline-managed span in copilot-instructions.md. */
|
|
@@ -160,7 +161,7 @@ function installLib(pipelineSrc, dest, useSymlinks) {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
function installSkills(opts) {
|
|
163
|
-
const { pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
|
|
164
|
+
const { home, pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
|
|
164
165
|
console.log(" [Copilot CLI] Installing skills...");
|
|
165
166
|
|
|
166
167
|
// Same symlink guard as the other owned trees: never prune or copy through
|
|
@@ -237,24 +238,44 @@ function installSkills(opts) {
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// pre-migration install so the installed tree stays consistent.
|
|
241
|
+
// The pre-migration figma-* skill copies are gone; the plugin owns component work
|
|
242
|
+
// now. Prune leftovers from an old install so the tree stays consistent.
|
|
243
243
|
for (const sub of ["figma-ios", "figma-android", "figma-common", "figma-to-component"]) {
|
|
244
244
|
const stale = join(dest, sub);
|
|
245
245
|
if (!existsSync(stale)) continue;
|
|
246
246
|
if (isDryRun()) {
|
|
247
|
-
console.log(` [dry-run] would prune stale ${stale} (
|
|
247
|
+
console.log(` [dry-run] would prune stale ${stale} (superseded by the plugin set)`);
|
|
248
248
|
continue;
|
|
249
249
|
}
|
|
250
250
|
try {
|
|
251
251
|
rmSync(stale, { recursive: true, force: true });
|
|
252
|
-
console.log(` -> pruned stale ${sub}/ (
|
|
252
|
+
console.log(` -> pruned stale ${sub}/ (superseded by the plugin set)`);
|
|
253
253
|
} catch {
|
|
254
254
|
/* non-fatal */
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// Deliver the stack plugin's authored skills. Copilot CLI has no marketplace
|
|
259
|
+
// loader, so without this it has none of them: create-screen, figma-validate,
|
|
260
|
+
// figma-review, component, state and navigation were all absent, which left a
|
|
261
|
+
// component task on Copilot with nothing to dispatch to. The contract used to call
|
|
262
|
+
// the pruned figma-* copies a "frozen fallback"; they were pruned, so the fallback
|
|
263
|
+
// did not exist.
|
|
264
|
+
//
|
|
265
|
+
// `knowledge/` is skipped: it is generated from this pipeline's own
|
|
266
|
+
// skills/shared/external, which the step above already installed. Copying it again
|
|
267
|
+
// would duplicate ~5 MB the host already has.
|
|
268
|
+
const pluginResult = installAuthoredPluginSkills({
|
|
269
|
+
home,
|
|
270
|
+
dest,
|
|
271
|
+
platformFlag,
|
|
272
|
+
label: "Copilot CLI",
|
|
273
|
+
skipNames: existingSkillNames(dest),
|
|
274
|
+
});
|
|
275
|
+
if (pluginResult.copied > 0) {
|
|
276
|
+
console.log(` -> ${pluginResult.copied} plugin skill file(s) delivered to ${dest}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
258
279
|
console.log(` -> ${copilotSkillCount} skill files installed to ${dest}`);
|
|
259
280
|
|
|
260
281
|
const sharedReadmeSrc = join(pipelineSrc, "skills", "shared", "README.md");
|
|
@@ -35,6 +35,29 @@ on real fan-out, so **delegation is authorized whenever a `/multi-agent` or
|
|
|
35
35
|
3. Outside an active pipeline run, the default applies: do not delegate unless
|
|
36
36
|
asked.
|
|
37
37
|
|
|
38
|
+
## The skills are on disk, not in the list
|
|
39
|
+
|
|
40
|
+
`$HOME/.codex/multi-agent-refs/skills/<name>/SKILL.md` holds every skill Claude Code
|
|
41
|
+
and Copilot CLI get: the pipeline's shared knowledge set plus the enabled stack
|
|
42
|
+
plugin's lifecycle skills (`create-screen`, `create-component`, `figma-validate`,
|
|
43
|
+
`figma-review`, `component`, `state`, `navigation`, `ios-coding-standard`, and the
|
|
44
|
+
rest). They are reference files rather than block entries, so they cost nothing until
|
|
45
|
+
read.
|
|
46
|
+
|
|
47
|
+
That means they will not be suggested to you. Look for one when the work calls for it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ls "$HOME/.codex/multi-agent-refs/skills" # what exists
|
|
51
|
+
grep -rl "<topic>" "$HOME/.codex/multi-agent-refs/skills"/*/SKILL.md | head # by content
|
|
52
|
+
head -4 "$HOME/.codex/multi-agent-refs/skills/<name>/SKILL.md" # its description
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Read the SKILL.md before implementing in its area, exactly as you would read a phase
|
|
56
|
+
spec. A component or screen task should reach `figma-validate` and `create-screen`; a
|
|
57
|
+
Swift review should reach `ios-coding-standard` and cite rule IDs from
|
|
58
|
+
`references/rules.yml`. Guessing a convention that one of these files states is the
|
|
59
|
+
failure this layout exists to prevent.
|
|
60
|
+
|
|
38
61
|
## Skills-block budget
|
|
39
62
|
|
|
40
63
|
Codex assembles every discovered skill's name + description into one prompt block
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.3.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex 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",
|