@mmerterden/multi-agent-pipeline 20.17.2 → 20.18.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 +13 -0
- package/docs/facts.json +2 -2
- package/install/_mcp-register.mjs +1 -1
- package/install/_xcode-skills.mjs +263 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +23 -0
- package/install/codex.mjs +11 -0
- package/install/copilot.mjs +9 -0
- package/manifest.json +22 -20
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/stack/SKILL.md +2 -0
- package/pipeline/lib/xcode-plugin.mjs +131 -0
- package/pipeline/multi-agent-refs/features/doctor.md +10 -0
- package/pipeline/scripts/doctor.mjs +38 -0
- package/pipeline/scripts/uninstall.mjs +57 -1
- package/pipeline/skills/.skill-manifest.json +2 -2
- package/pipeline/skills/shared/external/ios-iphone-duo/SKILL.md +9 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/evals/evals.json +15 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +59 -2
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,19 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [20.18.0] - 2026-10-09
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Apple's Xcode-provided skills are linked from the machine's own Xcode 27.1 or later (`xcrun agent plugin path`), never shipped in this package. The installer keeps a local directory marketplace (`~/.claude/xcode-apple-marketplace`, plugin `xcode-integration@xcode-apple`) at the installed Xcode build for Claude Code, and copies the same skills beside the stack plugin skills for Copilot CLI and as refs for Codex CLI when the iOS toolkit is delivered, with a manifest for refresh and uninstall. No Xcode, an older one or a failing `xcrun` links nothing and fails nothing. `MULTI_AGENT_SKIP_XCODE_SKILLS=1` (and the existing `MULTI_AGENT_SKIP_MCP_REGISTER=1`) skip the link for hermetic installs.
|
|
24
|
+
- `/multi-agent:stack` turns `xcode-integration@xcode-apple` on with `ios` and off without it, once the link exists.
|
|
25
|
+
- doctor check `xcode-skills`: SKIP without the Xcode plugin, INFO when available but not linked, WARN when the link is older than the installed Xcode build.
|
|
26
|
+
- `ios-iphone-duo` routes the code scan to Apple's `app-resizability` first, and covers the scene-based life cycle (required from iOS 27 for apps built with the latest SDK), legacy layout guides, `keyboardLayoutGuide` and stored insets for machines without it.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Uninstall removes the Claude Code link (plugin, marketplace and directory) and the delivered copies, since nothing refreshes them without the pipeline.
|
|
31
|
+
|
|
19
32
|
## [20.17.2] - 2026-10-08
|
|
20
33
|
|
|
21
34
|
### Added
|
package/docs/facts.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "Generated by pipeline/scripts/gen-facts.mjs. Do not hand-edit: the site reads this, and a number edited here instead of at its source is the drift this file removes.",
|
|
3
|
-
"generatedAt": "2026-10-
|
|
4
|
-
"version": "20.
|
|
3
|
+
"generatedAt": "2026-10-09",
|
|
4
|
+
"version": "20.18.0",
|
|
5
5
|
"phaseSchema": 2,
|
|
6
6
|
"phases": [
|
|
7
7
|
{
|
|
@@ -119,7 +119,7 @@ function hostConfig(host) {
|
|
|
119
119
|
* @param {"claude"|"codex"|"copilot"} host
|
|
120
120
|
* @returns {string}
|
|
121
121
|
*/
|
|
122
|
-
function resolveCli(host) {
|
|
122
|
+
export function resolveCli(host) {
|
|
123
123
|
const { candidates } = hostConfig(host);
|
|
124
124
|
for (const c of candidates) {
|
|
125
125
|
if (c.includes("/") && existsSync(c)) return c;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple's Xcode-provided skills, linked from the machine's own Xcode.
|
|
3
|
+
*
|
|
4
|
+
* Xcode 27.1 and later materialize an agent plugin (`xcode-integration`: Apple's
|
|
5
|
+
* skills for app resizability and iPhone Duo, SwiftUI, App Intents, accessibility
|
|
6
|
+
* audits, String Catalog translation, test modernization and more, plus Xcode's
|
|
7
|
+
* MCP bridge) and print its path with `xcrun agent plugin path`. These skills are
|
|
8
|
+
* the reference for the topics they cover, so iOS work routes to them first.
|
|
9
|
+
*
|
|
10
|
+
* Nothing of Apple's is shipped here. Each host gets the content its own Xcode
|
|
11
|
+
* produced, refreshed whenever the Xcode build changes:
|
|
12
|
+
*
|
|
13
|
+
* - **Claude Code** loads it as a plugin from a local directory marketplace
|
|
14
|
+
* (`xcode-apple`). A directory marketplace is read in place, so a refresh is a
|
|
15
|
+
* re-copy into that directory; the next session sees it.
|
|
16
|
+
* - **Copilot CLI** has no plugin loader, so the skills are copied into its flat
|
|
17
|
+
* skills directory, the same delivery the stack plugins get.
|
|
18
|
+
* - **Codex CLI** takes Xcode's Codex-format skills as reference files beside the
|
|
19
|
+
* stack plugin skills: it lists plugin skills without their descriptions and
|
|
20
|
+
* truncates its skills block once it overflows.
|
|
21
|
+
*
|
|
22
|
+
* No developer tools, an Xcode older than 27.1, a failing `xcrun` or a failed
|
|
23
|
+
* copy means nothing is linked and the pipeline's own skills cover the topics.
|
|
24
|
+
* The install never fails over it: every outcome is returned, never thrown.
|
|
25
|
+
*
|
|
26
|
+
* @module install/_xcode-skills
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
|
|
32
|
+
import { copyDir, ensureDir, isDryRun, wipeDir } from "./_common.mjs";
|
|
33
|
+
import { resolveCli } from "./_mcp-register.mjs";
|
|
34
|
+
import {
|
|
35
|
+
BUILD_STAMP,
|
|
36
|
+
CLAUDE_MARKETPLACE_DIR,
|
|
37
|
+
XCODE_MARKETPLACE,
|
|
38
|
+
XCODE_PLUGIN,
|
|
39
|
+
XCODE_PLUGIN_ID,
|
|
40
|
+
XCODE_SKILLS_MANIFEST,
|
|
41
|
+
claudeLinkedBuild,
|
|
42
|
+
defaultRun,
|
|
43
|
+
resolveXcodePlugin,
|
|
44
|
+
xcodeSkillsStatus,
|
|
45
|
+
} from "../pipeline/lib/xcode-plugin.mjs";
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
CLAUDE_MARKETPLACE_DIR,
|
|
49
|
+
XCODE_MARKETPLACE,
|
|
50
|
+
XCODE_PLUGIN,
|
|
51
|
+
XCODE_PLUGIN_ID,
|
|
52
|
+
XCODE_SKILLS_MANIFEST,
|
|
53
|
+
resolveXcodePlugin,
|
|
54
|
+
xcodeSkillsStatus,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hermetic installs skip the link: it shells out to `xcrun` and to the host CLI,
|
|
59
|
+
* so the result would depend on the machine. The layout smokes already set the
|
|
60
|
+
* MCP opt-out, and Apple's plugin carries an MCP server of its own.
|
|
61
|
+
*/
|
|
62
|
+
export function xcodeLinkDisabled(env = process.env) {
|
|
63
|
+
return env.MULTI_AGENT_SKIP_XCODE_SKILLS === "1" || env.MULTI_AGENT_SKIP_MCP_REGISTER === "1";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const firstLine = (e) =>
|
|
67
|
+
(
|
|
68
|
+
`${e?.stderr ?? ""}${e?.stdout ?? ""}${e?.message ?? ""}`.split("\n").find((l) => l.trim()) ||
|
|
69
|
+
"unknown error"
|
|
70
|
+
).trim();
|
|
71
|
+
|
|
72
|
+
function marketplaceJson(build) {
|
|
73
|
+
return {
|
|
74
|
+
name: XCODE_MARKETPLACE,
|
|
75
|
+
owner: { name: "Xcode on this Mac" },
|
|
76
|
+
metadata: {
|
|
77
|
+
description: `Apple's Xcode-provided skills, copied from this Mac's Xcode (build ${build}).`,
|
|
78
|
+
},
|
|
79
|
+
plugins: [
|
|
80
|
+
{
|
|
81
|
+
name: XCODE_PLUGIN,
|
|
82
|
+
source: `./${XCODE_PLUGIN}`,
|
|
83
|
+
description: "Apple's skills and Xcode MCP bridge, as Xcode ships them.",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Bring Claude Code's `xcode-apple` marketplace to the installed Xcode build and
|
|
91
|
+
* make sure the plugin is installed at user scope.
|
|
92
|
+
*
|
|
93
|
+
* User scope enables it in every repo, as the stack plugins are when installed
|
|
94
|
+
* that way; `/multi-agent:stack` turns it off for a repo whose stack is not iOS.
|
|
95
|
+
*
|
|
96
|
+
* Idempotent: the copy is skipped when the stamp names this build, and both CLI
|
|
97
|
+
* calls succeed on an existing marketplace or install. The stamp is written only
|
|
98
|
+
* after both succeed, so a failed link never reads as current.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} home
|
|
101
|
+
* @param {{run?: (cmd: string, args: string[]) => string, cli?: string, env?: object}} [opts]
|
|
102
|
+
* @returns {{status: "linked"|"refreshed"|"current"|"unavailable"|"skipped"|"failed", build?: string, skills?: number, reason?: string}}
|
|
103
|
+
*/
|
|
104
|
+
export function linkClaudeXcodePlugin(home, { run = defaultRun, cli, env = process.env } = {}) {
|
|
105
|
+
if (xcodeLinkDisabled(env)) return { status: "skipped", reason: "opted out" };
|
|
106
|
+
const plugin = resolveXcodePlugin("claude", { run, env });
|
|
107
|
+
if (!plugin) return { status: "unavailable", reason: "no Xcode agent plugin on this Mac" };
|
|
108
|
+
|
|
109
|
+
const mpDir = join(home, CLAUDE_MARKETPLACE_DIR);
|
|
110
|
+
const target = join(mpDir, XCODE_PLUGIN);
|
|
111
|
+
const previous = claudeLinkedBuild(home);
|
|
112
|
+
const fresh = previous === plugin.build && existsSync(join(target, ".claude-plugin"));
|
|
113
|
+
|
|
114
|
+
if (isDryRun()) {
|
|
115
|
+
console.log(` [dry-run] would link ${XCODE_PLUGIN_ID} from Xcode ${plugin.build}`);
|
|
116
|
+
return { status: "skipped", reason: "dry-run", build: plugin.build };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (!fresh) {
|
|
121
|
+
wipeDir(join(mpDir, BUILD_STAMP));
|
|
122
|
+
ensureDir(join(mpDir, ".claude-plugin"));
|
|
123
|
+
wipeDir(target);
|
|
124
|
+
copyDir(plugin.path, target);
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(mpDir, ".claude-plugin", "marketplace.json"),
|
|
127
|
+
JSON.stringify(marketplaceJson(plugin.build), null, 2) + "\n",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return { status: "failed", reason: `copy failed: ${firstLine(e)}`, build: plugin.build };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const claude = cli || resolveCli("claude");
|
|
135
|
+
try {
|
|
136
|
+
run(claude, ["plugin", "marketplace", "add", mpDir]);
|
|
137
|
+
run(claude, ["plugin", "install", XCODE_PLUGIN_ID, "--scope", "user"]);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
const reason = e?.code === "ENOENT" ? "claude not on PATH" : firstLine(e);
|
|
140
|
+
return { status: "failed", reason, build: plugin.build };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (!fresh) writeFileSync(join(mpDir, BUILD_STAMP), plugin.build + "\n");
|
|
145
|
+
} catch (e) {
|
|
146
|
+
return { status: "failed", reason: `stamp not written: ${firstLine(e)}`, build: plugin.build };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const status = fresh ? "current" : previous ? "refreshed" : "linked";
|
|
150
|
+
return { status, build: plugin.build, skills: plugin.skills.length };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readManifest(dest) {
|
|
154
|
+
try {
|
|
155
|
+
const names = JSON.parse(readFileSync(join(dest, XCODE_SKILLS_MANIFEST), "utf8"));
|
|
156
|
+
return Array.isArray(names) ? names : [];
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function existingDirs(dest) {
|
|
163
|
+
try {
|
|
164
|
+
return readdirSync(dest, { withFileTypes: true })
|
|
165
|
+
.filter((e) => e.isDirectory())
|
|
166
|
+
.map((e) => e.name);
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Copy Apple's skills into a flat skills directory (Copilot) or refs directory
|
|
174
|
+
* (Codex), one directory per skill, replacing what an earlier pass delivered.
|
|
175
|
+
*
|
|
176
|
+
* A name already present in `dest` that an earlier pass did not deliver - a
|
|
177
|
+
* pipeline skill, a stack plugin skill, or one the user wrote - is never
|
|
178
|
+
* overwritten: Apple's skill goes in as `xcode-<name>` instead, and is left out
|
|
179
|
+
* if that name is taken too.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} opts
|
|
182
|
+
* @param {string} opts.dest
|
|
183
|
+
* @param {string} opts.label - host label for log lines
|
|
184
|
+
* @param {"claude"|"codex"} [opts.format] - which of Xcode's plugin formats to copy
|
|
185
|
+
* @param {Set<string>} [opts.taken] - names this install delivers into `dest`
|
|
186
|
+
* @param {boolean} [opts.enabled] - false removes an earlier delivery (the repo is not iOS)
|
|
187
|
+
* @param {(from: string, to: string) => void} [opts.copy]
|
|
188
|
+
* @param {(cmd: string, args: string[]) => string} [opts.run]
|
|
189
|
+
* @param {object} [opts.env]
|
|
190
|
+
* @returns {{status: "delivered"|"unavailable"|"skipped"|"failed", build?: string, delivered: string[], reason?: string}}
|
|
191
|
+
*/
|
|
192
|
+
export function deliverXcodeSkills(opts) {
|
|
193
|
+
const {
|
|
194
|
+
dest,
|
|
195
|
+
label,
|
|
196
|
+
format = "claude",
|
|
197
|
+
taken: takenByInstall = new Set(),
|
|
198
|
+
enabled = true,
|
|
199
|
+
copy = (from, to) => copyDir(from, to),
|
|
200
|
+
run = defaultRun,
|
|
201
|
+
env = process.env,
|
|
202
|
+
} = opts;
|
|
203
|
+
const previous = readManifest(dest);
|
|
204
|
+
const ours = new Set(previous);
|
|
205
|
+
const taken = new Set([
|
|
206
|
+
...takenByInstall,
|
|
207
|
+
...existingDirs(dest).filter((name) => !ours.has(name)),
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const clear = () => {
|
|
212
|
+
for (const name of previous) if (!taken.has(name)) wipeDir(join(dest, name));
|
|
213
|
+
if (!isDryRun() && previous.length) writeFileSync(join(dest, XCODE_SKILLS_MANIFEST), "[]\n");
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (!enabled) {
|
|
217
|
+
clear();
|
|
218
|
+
return { status: "skipped", delivered: [] };
|
|
219
|
+
}
|
|
220
|
+
if (xcodeLinkDisabled(env)) return { status: "skipped", delivered: [] };
|
|
221
|
+
const plugin = resolveXcodePlugin(format, { run, env });
|
|
222
|
+
if (!plugin) {
|
|
223
|
+
clear();
|
|
224
|
+
console.log(` -> ${label}: no Xcode agent plugin on this Mac; Apple's skills not linked`);
|
|
225
|
+
return { status: "unavailable", delivered: [] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const name of previous) if (!taken.has(name)) wipeDir(join(dest, name));
|
|
229
|
+
const delivered = [];
|
|
230
|
+
let skipped = 0;
|
|
231
|
+
for (const name of plugin.skills) {
|
|
232
|
+
const deliveredName = !taken.has(name)
|
|
233
|
+
? name
|
|
234
|
+
: !taken.has(`xcode-${name}`)
|
|
235
|
+
? `xcode-${name}`
|
|
236
|
+
: null;
|
|
237
|
+
if (!deliveredName) {
|
|
238
|
+
skipped++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const to = join(dest, deliveredName);
|
|
242
|
+
ensureDir(dest);
|
|
243
|
+
wipeDir(to);
|
|
244
|
+
if (!isDryRun()) copy(join(plugin.path, "skills", name), to);
|
|
245
|
+
delivered.push(deliveredName);
|
|
246
|
+
}
|
|
247
|
+
if (isDryRun()) {
|
|
248
|
+
console.log(
|
|
249
|
+
` [dry-run] would link ${delivered.length} Apple skill(s) from Xcode ${plugin.build} into ${dest}`,
|
|
250
|
+
);
|
|
251
|
+
return { status: "skipped", build: plugin.build, delivered };
|
|
252
|
+
}
|
|
253
|
+
writeFileSync(join(dest, XCODE_SKILLS_MANIFEST), JSON.stringify(delivered, null, 2) + "\n");
|
|
254
|
+
console.log(
|
|
255
|
+
` -> ${label}: ${delivered.length} Apple skill(s) linked from Xcode ${plugin.build}` +
|
|
256
|
+
(skipped ? ` (${skipped} left out: both names taken)` : ""),
|
|
257
|
+
);
|
|
258
|
+
return { status: "delivered", build: plugin.build, delivered };
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.log(` -> ${label}: Apple's skills not linked: ${firstLine(e)}`);
|
|
261
|
+
return { status: "failed", delivered: [], reason: firstLine(e) };
|
|
262
|
+
}
|
|
263
|
+
}
|