@mmerterden/multi-agent-pipeline 13.2.0 → 13.4.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,131 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [13.4.0] - 2026-07-28
20
+
21
+ ### Added
22
+
23
+ - **`credential-inventory.sh`** - one command that answers "what can I reach right now?"
24
+ A run asked the user to paste a Crashlytics stack trace by hand, offering the manual
25
+ path as the fastest route, while a valid Firebase service-account JSON sat in the
26
+ Keychain mapped as `firebase` and resolving fine. Nothing had failed: the pipeline
27
+ simply never asked itself whether it already held a credential that answered the
28
+ question. The tool reports `present` / `mapped-but-missing` / `unmapped` per logical
29
+ key plus what each one unlocks, and `keychain.md` Rule 2 now binds every question that
30
+ requests external data to its output - ask for the pointer (the issue URL), never for
31
+ the payload. Values are never printed.
32
+ - `--probe` verifies that configured credentials actually answer, because "the key is in
33
+ the Keychain" and "the service replies" are different claims that the user fixes in
34
+ different ways. The verdicts stay separate on purpose: `auth-rejected` (refresh the
35
+ token), `unreachable` (on a corporate host, almost always the VPN),
36
+ `no-host-configured` (a token with nowhere to point), `well-formed` (valid shape,
37
+ liveness needs more input), `not-probeable`. Collapsing them into one "failed" bucket
38
+ is what led the user to three different wrong actions. Only configured credentials are
39
+ probed - an unmapped key is a capability the user chose not to enable.
40
+ - `smoke-credential-awareness.sh` (32 assertions) and `test/phase0-exit-gate.test.mjs`
41
+ (20 cases) lock all of the above, including two secret-leak checks and the rule that
42
+ the credential reaches curl through a stdin config rather than argv, where any `ps`
43
+ could read it.
44
+ - `smoke-install-layout.sh --update-fixture` regenerates the layout fixture from the
45
+ tree the gate itself installs. The documented hand-rolled recipe omitted the seeded
46
+ probe plugin, so following it baked a three-file shortfall into the fixture and the
47
+ next run failed on a drift that was really a bad regen.
48
+
49
+ ### Fixed
50
+
51
+ - **The Phase 0 exit gate now asserts that the pickers ran.** It checked `taskType` and
52
+ the Figma access tier only, so a run that skipped the project and branch pickers and
53
+ developed straight on the local checkout passed cleanly - which is exactly what a
54
+ reported run did with a Jira ID. The gate now also requires `baseBranch`,
55
+ a `baseFetchStatus` from the known set, and a worktree path distinct from the project
56
+ root. Branch selection is marked non-skippable in `phase-0-init.md`, in every mode:
57
+ `--dev` skips the LLM phases, not Phase 0's pickers.
58
+ - **An unreachable external source is announced instead of absorbed.** Exit code `3`
59
+ used to mean "mark it failed and continue", so an expired token and a VPN-off remote
60
+ both reached the analysis phase as *no data*, indistinguishable from a ticket that
61
+ referenced nothing. The run then planned from a partial picture and reported success.
62
+ `external-context-injection.md` now classifies the stderr and surfaces a decision -
63
+ refresh the credential, connect the VPN and retry, supply a current URL, or continue
64
+ without - records `userDecision` in state, and requires autopilot to report what it
65
+ skipped rather than hide it. Retry is a real branch, not a label.
66
+ - The base-branch and fetch-failure pickers were still written as ASCII numbered menus
67
+ in `phase-0-init.md`, against the picker contract's native-widget rule. Both are now
68
+ expressed as picker options, and the fetch-failure question names the corporate host
69
+ when the remote points at one - a host that will not resolve is almost always the VPN,
70
+ and saying so is the difference between a five-second fix and a run built on a stale ref.
71
+ - `credential-inventory.sh --json` returned empty arrays while the table mode looked
72
+ correct: `python3 - <<EOF` takes its script from stdin, so the piped payload never
73
+ reached `sys.stdin`. Caught by the new gate on its first run.
74
+ - The probe read hosts from `global.<service>Host`; the real path is
75
+ `global.hosts.<service>`, so every self-hosted service reported
76
+ `no-host-configured`. Found by running the probe against the real preferences instead
77
+ of trusting the field name.
78
+ - `figma_mcp` is an OAuth token for the MCP server, not a REST PAT: probing it against
79
+ `api.figma.com` returned 403 for a healthy token and would have sent the user to
80
+ regenerate something that worked. It is now reported as not-probeable, with liveness
81
+ left to `figma-mcp-refresh.sh`, which owns the grant.
82
+ - A failed `curl` wrote `000` through `write-out` *and* triggered the `|| echo "000"`
83
+ fallback, producing `000000` and a bogus verdict for what was simply a closed VPN.
84
+ Any non-three-digit status now reads as `probe-error` rather than being dressed up as
85
+ a service verdict.
86
+ - `multi-agent-refs/rules.md` pointed at `~/.claude/scripts/vercel-deploy.sh`; the
87
+ wrapper installs to `lib/` with the other shell libraries. It had been reported as a
88
+ dangling reference for several releases.
89
+ - `/multi-agent:setup` no longer describes an unreachable discovery source as skipped
90
+ "silently". Setup is where the user is configuring things, so a source that could not
91
+ be reached is precisely what they need told, with the classification that decides
92
+ their next move.
93
+
94
+ ## [13.3.0] - 2026-07-28
95
+
96
+ ### Added
97
+
98
+ - **Copilot CLI and Codex CLI now carry the stack plugin's authored skills.** Claude Code
99
+ loads `multi-agent-plugins` natively; the other two hosts could not, and the contract
100
+ papered over it by claiming Copilot kept standalone `figma-*` copies as a "frozen
101
+ fallback". `install/copilot.mjs` pruned exactly those directories, so Copilot had
102
+ **zero** plugin-authored skills: `create-screen`, `figma-validate`, `figma-review`,
103
+ `component`, `state` and `navigation` were all absent, and a component task on Copilot
104
+ had nothing to dispatch to. New shared `install/_plugin-skills.mjs` delivers the
105
+ `index` / `reference` / `workflow` / `tools` groups to both hosts - as skills on
106
+ Copilot, as reference files on Codex, where the skills block truncates silently.
107
+ - Plugin selection reads `enabledPlugins` from `~/.claude/settings.json` rather than
108
+ copying every stack plugin. A flat copy of all five is last-write-wins, and `fix-bug`
109
+ plus `branch-and-pr` exist in four of them while `component`, `state` and
110
+ `create-component` exist in three - an iOS repo could have ended up running the
111
+ Android `create-component`. The `--platform` flag is now only the fallback for a
112
+ machine with no Claude Code settings to read.
113
+ - `test/stack-content-sync.test.mjs` - three cases locking content propagation,
114
+ content-only version bumps, and no-op idempotence in the stack-plugin generator.
115
+
116
+ ### Fixed
117
+
118
+ - **`build-stack-plugins.mjs` never propagated content edits.** It copied a skill only
119
+ when the skill *set* changed, so editing a routed skill in `shared/external` reached
120
+ no plugin while the generator reported "all plugins up to date" - which read as
121
+ confirmation. This falsified the single-authoring-source guarantee the whole
122
+ `shared/external` design rests on. Found by converting banned punctuation in
123
+ `ios-coding-standard` and seeing the plugin copy keep all 188 banned characters with a
124
+ differing `rules.yml`. Fixing it also surfaced 37 stale skill descriptions across four
125
+ plugins, still missing the "Use when ..." routing clause that decides whether a skill
126
+ is matched at all: those plugins had been serving worse descriptions than the source
127
+ for as long as the routing clause existed.
128
+ - `build-stack-plugins.mjs` now accepts `--key=value` as well as `--key value`. The `=`
129
+ form fell through to the default, so a run aimed at a throwaway checkout silently
130
+ retargeted the user's real marketplace and reported success.
131
+ - `smoke-install-layout.sh` asserts plugin-skill parity against a seeded probe plugin,
132
+ so the check cannot pass vacuously in an isolated `HOME` the way it did on first write.
133
+
134
+ ### Changed
135
+
136
+ - `cross-cli-contract.md` §1.1 replaced the false "frozen fallback" claim with a
137
+ per-host delivery table: native plugin load on Claude Code, installer-delivered skills
138
+ on Copilot CLI, installer-delivered refs on Codex CLI. Parity is asserted on the
139
+ reference set, not the skill set, because Codex ships one router skill by design.
140
+ - `codex-instructions.md` gained a "the skills are on disk, not in the list" section
141
+ with an `ls` / `grep` / `head` discovery recipe: only 5 of Codex's skill entries render
142
+ descriptions, so the 193 delivered refs are reachable but not description-matched.
143
+
19
144
  ## [13.2.0] - 2026-07-28
20
145
 
21
146
  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` 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.2.0",
3
+ "version": "13.4.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",