@mmerterden/multi-agent-pipeline 13.1.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 CHANGED
@@ -16,6 +16,108 @@ 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
+
69
+ ## [13.2.0] - 2026-07-28
70
+
71
+ The iOS coding standard reaches all three hosts, and works outside the pipeline.
72
+
73
+ The 95-rule registry lived as a pipeline-local routine. Two consequences, both
74
+ measured: only Claude Code could reach it, because `local-only` commands are
75
+ deliberately never synced (verified absent on Copilot CLI and Codex CLI), and it
76
+ applied only when the routine was invoked explicitly - nothing pulled the rules in
77
+ while Swift was being written. The plugin's own `reference/code-style` skill is 154
78
+ lines of prose with zero rule IDs, so the registry was not reachable that way either.
79
+
80
+ ### Added
81
+
82
+ - **`ios-coding-standard` skill**, authored once in `pipeline/skills/shared/external`
83
+ and routed into the iOS stack plugin's `knowledge/` by the existing generator. That
84
+ one source now lands as a skill on Claude Code and Copilot CLI (description-matched,
85
+ so the rules are present while code is being written) and ships inside the plugin for
86
+ Codex. `references/` carries the registry, the teaching doc, a SwiftLint config for
87
+ the mechanically-enforceable subset, and a module-scoped lint runner with a baseline
88
+ mode.
89
+ - **`/multi-agent:ios-coding-standard`** promoted out of `local-only` into the command
90
+ inventory (43 → 44), with its Copilot twin, so the module-wide audit procedure exists
91
+ on every host. It reads its rules from the skill rather than carrying a copy.
92
+
93
+ ### Changed
94
+
95
+ - The registry was generalised before promotion: company hostnames, an internal URL
96
+ scheme, internal model and endpoint names, and repo-specific module paths were
97
+ replaced. `smoke-personal-data.sh` is clean across all six promoted files. Rule
98
+ bodies describe shapes (a mapper doing arithmetic, a logger interpolating a token)
99
+ rather than named modules, so the registry applies unchanged to any SwiftUI codebase;
100
+ a project layers its own vocabulary through a `modules/<Module>.yml` overlay.
101
+ - Four over-long skill descriptions trimmed (`ios-simulator` 839→578,
102
+ `swift-api-design-guidelines` 738→468, `swiftlint` 725→473, and the three new ones).
103
+ The always-on description surface was at 78,079 of 78,000 bytes before this change;
104
+ adding a 44th command and a new skill needed 859 bytes it did not have. Each trim
105
+ removed duplication between a description's "covers" enumeration and its trigger
106
+ clause, which is where the routing value actually is. Surface is now 77,734.
107
+
108
+ ### Notes
109
+
110
+ - **Codex lists plugin skills by name only.** Measured: all 76 plugin-provided skills
111
+ render with an empty description in Codex's skills block, while the 5 system skills
112
+ render theirs. So on Codex a plugin skill is invoked explicitly rather than matched
113
+ on description. That is the reason the registry is authored in `shared/external`
114
+ instead of hand-placed in the plugin: the pipeline install path gives Claude Code and
115
+ Copilot CLI a description-matched skill, and the plugin copy covers Codex.
116
+ - The iOS plugin now declares 143 skills and Codex surfaces about half. That overflow
117
+ predates this change and `tools/validate.py` warns on it; routing `knowledge/` behind
118
+ the plugin's `index` skill is the fix and is separate work.
119
+
120
+
19
121
  ## [13.1.0] - 2026-07-27
20
122
 
21
123
  Six gates, from one branch that spent half its commits on rework.
@@ -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` and `platformFlag` are intentionally not read. Both exist to
172
- // shrink the 428-file external skills tree, which this target never installs:
173
- // Codex gets one router skill, so there is nothing for either flag to reduce.
174
- // Documented rather than silently dropped, so a future reader does not "fix"
175
- // it by wiring flags that would have no effect.
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
  *
@@ -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
- // Figma component skills moved to the ai-<platform>-engineering-toolkit marketplace
241
- // plugin and are no longer bundled here. Prune any stale copies left by a
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} (moved to plugin)`);
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}/ (moved to marketplace plugin)`);
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.1.0",
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",