@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
|
@@ -5,17 +5,17 @@ argument-hint: "[module name or path]"
|
|
|
5
5
|
allowed-tools: Skill, Bash, Read, Edit, Write, AskUserQuestion
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# multi-agent ios-coding-standard
|
|
8
|
+
# multi-agent ios-coding-standard - Module audit → plan → dev handoff
|
|
9
9
|
|
|
10
|
-
**Input**: $ARGUMENTS
|
|
10
|
+
**Input**: $ARGUMENTS - optionally a module name or path. When absent, Phase 1 discovers and asks.
|
|
11
11
|
|
|
12
12
|
This routine is the **procedure**. The rules live in the `ios-coding-standard` skill, whose registry is
|
|
13
13
|
`references/rules.yml` and whose teaching doc is `references/STANDARD.md`. Load that
|
|
14
14
|
skill first; it is installed on every host and carried by the iOS stack plugin, so
|
|
15
|
-
there is one registry rather than a copy per CLI. Never restate a rule here
|
|
15
|
+
there is one registry rather than a copy per CLI. Never restate a rule here - cite its ID. A rule that is not in the
|
|
16
16
|
registry is not a rule; if the audit needs one, propose it as `status: proposed` and say so.
|
|
17
17
|
|
|
18
|
-
Read-only + planning. This routine never edits source
|
|
18
|
+
Read-only + planning. This routine never edits source - the dev pipeline does.
|
|
19
19
|
|
|
20
20
|
**Goal.** A developer new to the repo can open any file in the module and understand it without a
|
|
21
21
|
guided tour, and nothing sensitive leaks on the way. Rank every finding by "does fixing this
|
|
@@ -26,26 +26,26 @@ dev task) stay English.
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
## Phase 1
|
|
29
|
+
## Phase 1 - Discover and pick the module
|
|
30
30
|
|
|
31
31
|
1. Repo root via `git rev-parse --show-toplevel`.
|
|
32
|
-
2. **Discover module roots**
|
|
32
|
+
2. **Discover module roots** - any directory with a `Package.swift`, a `Sources/` subtree or an
|
|
33
33
|
`.xcodeproj`. Sweep the container dirs that exist (`Domains/`, `Packages/`, `Modules/`,
|
|
34
34
|
`Features/`, `Core/`, `Common/`, top-level module dirs) one level deep, then two if empty.
|
|
35
35
|
3. Record per module: name · path · Swift files · lines · governance docs present · SPM targets.
|
|
36
|
-
Keep this **module registry** for the whole run
|
|
36
|
+
Keep this **module registry** for the whole run - `MOD-*` greps every import against it.
|
|
37
37
|
Classify each module's role, because the role decides which dependency edges are legal:
|
|
38
38
|
**feature** · **core/common** · **seam** (cross-module contracts/bridges/navigation) ·
|
|
39
39
|
**composition root** (legitimately knows every module).
|
|
40
40
|
4. If `$ARGUMENTS` resolves to a module, skip the prompt. Otherwise `AskUserQuestion`
|
|
41
41
|
(single-select, `outputLanguage`), documented modules first, each option showing file/line
|
|
42
42
|
counts and `docs: yes/no`. If the list exceeds the question limit, group the smallest into an
|
|
43
|
-
"other" option and ask again
|
|
43
|
+
"other" option and ask again - never truncate silently.
|
|
44
44
|
5. Scope = the whole module's sources across all its targets, unless the user narrows it.
|
|
45
45
|
|
|
46
|
-
## Phase 2
|
|
46
|
+
## Phase 2 - Resolve what applies to THIS module
|
|
47
47
|
|
|
48
|
-
### 2a. The registry is the standard
|
|
48
|
+
### 2a. The registry is the standard - in-module prose docs are NOT consulted
|
|
49
49
|
|
|
50
50
|
The skill's `references/rules.yml` (+ a project `modules/<Module>.yml` overlay) is the single source of truth. **Do not read, cite, or
|
|
51
51
|
derive rules from a module's own `*-CLAUDE.md`, `docs/` set or any other in-repo prose.** They are
|
|
@@ -64,7 +64,7 @@ the specs the generator reads, and the module registry from Phase 1.
|
|
|
64
64
|
be `blocking`.
|
|
65
65
|
*Why prohibitions need to be written down:* inference reads dominant patterns, and a rule
|
|
66
66
|
obeyed everywhere has no counter-example to infer from. A ban at 100% compliance is invisible
|
|
67
|
-
to a scan
|
|
67
|
+
to a scan - it survives only if the overlay states it.
|
|
68
68
|
2. **Repo-level rules** (root `CLAUDE.md`, contributing docs) apply to every module.
|
|
69
69
|
3. **The module's own dominant pattern is its de-facto convention.** Count variants per dimension;
|
|
70
70
|
the minority instances are the finding. Internal consistency beats conformity to a sibling.
|
|
@@ -74,14 +74,14 @@ Findings from step 3 are tagged `inferred` and capped at **suggestion**. Only re
|
|
|
74
74
|
overlay-backed rules may be `blocking`.
|
|
75
75
|
|
|
76
76
|
**When the overlay is missing**, generate a draft from code evidence, print it for confirmation,
|
|
77
|
-
and mark every slot it could not derive
|
|
77
|
+
and mark every slot it could not derive - a frozen UI target, a name lock, a carve-out and an
|
|
78
78
|
operational constraint all look like ordinary code from the outside. Do not guess them; list them
|
|
79
79
|
as Open Questions.
|
|
80
80
|
|
|
81
81
|
### 2c. Vocabulary
|
|
82
82
|
|
|
83
83
|
Bind each slot to a real symbol by grepping the module, then the reference module. **An unbound
|
|
84
|
-
slot disables its rules**
|
|
84
|
+
slot disables its rules** - never invent a name, never import a sibling's.
|
|
85
85
|
|
|
86
86
|
`HandlerName` · `EventParam` · `CoordinatorType` · `ResultType` / `ErrorType` / `ErrorFactory` ·
|
|
87
87
|
`DIResolver` / `DIConfigurator` · `RequestModelSuffix` / `ResponseModelSuffix` / `TransportSuffix` ·
|
|
@@ -94,14 +94,14 @@ slot disables its rules** — never invent a name, never import a sibling's.
|
|
|
94
94
|
|
|
95
95
|
Print the resolved table before scanning so a wrong binding is caught early.
|
|
96
96
|
|
|
97
|
-
### 2d. Sensitive-data inventory
|
|
97
|
+
### 2d. Sensitive-data inventory - required before any SEC rule runs
|
|
98
98
|
|
|
99
99
|
The `SEC-*` rules are written against the **data classes** in `references/rules.yml →
|
|
100
100
|
sensitive_data_classes`, never against one module's field names. Resolve the module's concrete
|
|
101
101
|
instances of each class: grep entities, request/response models, storage calls and analytics
|
|
102
102
|
events for the values the module actually handles, and map each to its class.
|
|
103
103
|
|
|
104
|
-
Produce the inventory as a table
|
|
104
|
+
Produce the inventory as a table - **class · concrete symbols · where it enters · required
|
|
105
105
|
lifetime (transient / survives-restart / survives-reinstall) · where it is actually stored ·
|
|
106
106
|
where it is logged · where it leaves (network, analytics, pasteboard, another module)**.
|
|
107
107
|
|
|
@@ -117,13 +117,13 @@ whose required lifetime nobody can state, is an Open Question, not a silent pass
|
|
|
117
117
|
|
|
118
118
|
This table is a deliverable in its own right, independent of whether any violation is found.
|
|
119
119
|
|
|
120
|
-
## Phase 3
|
|
120
|
+
## Phase 3 - Scan
|
|
121
121
|
|
|
122
122
|
Apply the rules in `references/rules.yml` that survived Phase 2 binding, plus the module's own `validation`
|
|
123
123
|
gate. Tag every finding: **rule ID · severity · source (`registry` / `doc` / `inferred`) ·
|
|
124
124
|
onboarding impact (high/medium/low)**.
|
|
125
125
|
|
|
126
|
-
### Coverage gate
|
|
126
|
+
### Coverage gate - on disk, not in memory
|
|
127
127
|
|
|
128
128
|
Before scanning, write `.<module>-audit-coverage.tsv` to the scratch dir: one row per target
|
|
129
129
|
(every screen under `ScreenRoot`, every shared component, every entity, every file in every
|
|
@@ -131,25 +131,25 @@ target) with columns `path · status · rules-applied`. Mark rows as you go.
|
|
|
131
131
|
|
|
132
132
|
- Each target is **audited** or **explicitly skipped with a reason** (generated, fixture,
|
|
133
133
|
vendored). No third state.
|
|
134
|
-
- **The run is incomplete while any row is unmarked**
|
|
134
|
+
- **The run is incomplete while any row is unmarked** - and the file, not a claim, is the proof.
|
|
135
135
|
Chunk the module if it is too large for one pass; the checklist survives a context reset.
|
|
136
136
|
- No sampling. A partial audit reported as complete certifies the unscanned screens as clean.
|
|
137
137
|
|
|
138
138
|
### Grep hygiene
|
|
139
139
|
|
|
140
140
|
Registry `mechanism` patterns are line-based and match doc comments. Filter
|
|
141
|
-
`^[[:space:]]*//`
|
|
142
|
-
through
|
|
141
|
+
`^[[:space:]]*//` - BSD grep does not understand `\s`, so a `\s*//` filter silently lets `/// ...`
|
|
142
|
+
through - then open each surviving hit and confirm it is real code. Report pre-filter and
|
|
143
143
|
post-verification counts separately; a table padded with comment matches destroys trust.
|
|
144
144
|
|
|
145
145
|
### Judgement rules need evidence, not assertion
|
|
146
146
|
|
|
147
147
|
For any rule marked `enforcement: judgement`, a finding must carry the measurement its `check`
|
|
148
|
-
names
|
|
148
|
+
names - a reference count, a call-site count, a consumer count, a file list. **No count, no
|
|
149
149
|
finding.** This is what keeps a subjective rule from becoming an opinion.
|
|
150
150
|
|
|
151
151
|
Every judgement rule has a worked ✗/✓ pair in `EXAMPLES.md`, keyed by ID. Cite it in the finding's
|
|
152
|
-
proposed fix rather than re-describing the shape
|
|
152
|
+
proposed fix rather than re-describing the shape - the developer applying the fix should be
|
|
153
153
|
reading the same picture the audit used.
|
|
154
154
|
|
|
155
155
|
### Module-specific overrides
|
|
@@ -158,79 +158,79 @@ The module's own docs win over the registry, including where they contradict a s
|
|
|
158
158
|
Never apply one module's dialect to another. List every override honoured, so the reader sees why
|
|
159
159
|
a registry rule was not raised.
|
|
160
160
|
|
|
161
|
-
## Phase 4
|
|
161
|
+
## Phase 4 - Produce the plan
|
|
162
162
|
|
|
163
163
|
No findings → report compliant and stop. Otherwise write
|
|
164
164
|
`<ModulePath>/ios-coding-standard-plan-<module>.md` (or the repo's scratch/docs dir if the module
|
|
165
165
|
tree must stay clean), containing, in order:
|
|
166
166
|
|
|
167
|
-
1. **Coverage ledger**
|
|
167
|
+
1. **Coverage ledger** - total targets · audited · skipped with reasons. Not 100% accounted for
|
|
168
168
|
means the plan is not deliverable.
|
|
169
|
-
2. **Summary**
|
|
170
|
-
3. **Sensitive-data inventory** (Phase 2d)
|
|
171
|
-
4. **Screen × role matrix** `[STRUCT-02]`
|
|
169
|
+
2. **Summary** - counts by severity and source, resolved vocabulary, branch to create.
|
|
170
|
+
3. **Sensitive-data inventory** (Phase 2d) - first, because it is the highest-risk content.
|
|
171
|
+
4. **Screen × role matrix** `[STRUCT-02]` - screens as rows, manifest roles as columns, cells
|
|
172
172
|
`✓ / missing / misplaced`. The structural map a newcomer reads first.
|
|
173
|
-
5. **Type placement table** `[STRUCT-05]`
|
|
173
|
+
5. **Type placement table** `[STRUCT-05]` - type · location · consumer count · correct tier ·
|
|
174
174
|
move required. Paired with the nested-type list `[STRUCT-01]`: owner · nested type · kind ·
|
|
175
175
|
reference count · verdict.
|
|
176
|
-
6. **Visibility report** `[VIS-01, VIS-02]`
|
|
176
|
+
6. **Visibility report** `[VIS-01, VIS-02]` - over-exposed declarations per file, non-final
|
|
177
177
|
classes with no subclass.
|
|
178
|
-
7. **Boundary report** `[MOD-*]`
|
|
178
|
+
7. **Boundary report** `[MOD-*]` - import graph (allowed vs forbidden, with coupling depth) ·
|
|
179
179
|
manifest graph (declared vs legal for the role, plus dead edges) · **removability delta**
|
|
180
|
-
("removing X touches N files:
|
|
181
|
-
8. **Testability seam report** `[TEST-*]`
|
|
180
|
+
("removing X touches N files: ...") · inbound test (tests compile with no sibling present).
|
|
181
|
+
8. **Testability seam report** `[TEST-*]` - types reaching for the environment, logic requiring a
|
|
182
182
|
view to execute, doubles whose signatures have drifted.
|
|
183
|
-
9. **Change-cost probe** `[FLEX-*]`
|
|
183
|
+
9. **Change-cost probe** `[FLEX-*]` - pick two plausible upcoming changes (a new variant of an
|
|
184
184
|
existing screen; a new field on a shared entity) and state what each costs in files today.
|
|
185
|
-
10. **Findings table**
|
|
185
|
+
10. **Findings table** - rule ID · `file:line` · current state · proposed fix · severity · source ·
|
|
186
186
|
onboarding impact.
|
|
187
|
-
11. **Per-file work**
|
|
187
|
+
11. **Per-file work** - grouped per file, blocking → important → suggestion; within a severity,
|
|
188
188
|
highest onboarding impact first.
|
|
189
|
-
12. **Behaviour-preservation guard**
|
|
189
|
+
12. **Behaviour-preservation guard** - splitting files, adding MARKs, extracting extensions and
|
|
190
190
|
moving types must not change behaviour. Anything requiring a behaviour change (a missing
|
|
191
191
|
analytics event, a wrong result type, a timezone fix, a storage migration) goes in a
|
|
192
192
|
**separate group**, reviewed and tested on its own, never inside a mechanical split commit.
|
|
193
|
-
13. **Tooling delegation & the ratchet**
|
|
193
|
+
13. **Tooling delegation & the ratchet** - the section that decides whether any of this survives.
|
|
194
194
|
- Classify each applied rule by its registry `enforcement`. For every `lint` / `format` /
|
|
195
195
|
`scan` rule, state the concrete mechanism and whether that toolchain exists in the repo
|
|
196
196
|
(`LintToolchain`). **When it does not exist, saying so is a finding**, ranked above most
|
|
197
197
|
individual violations it would have caught.
|
|
198
198
|
- Recommend **baseline-and-ratchet, never big-bang**: grandfather existing violations, surface
|
|
199
|
-
only new ones, drain the backlog behind a green build. Order the plan the same way
|
|
199
|
+
only new ones, drain the backlog behind a green build. Order the plan the same way - stop
|
|
200
200
|
the bleeding, then clean up.
|
|
201
201
|
- Custom lint rule identifiers **are** the registry IDs (`sec_01_no_plaintext_persistence`,
|
|
202
202
|
`mod_01_no_sibling_import`) so a violation points straight at the rule entry.
|
|
203
|
-
- **Linting is local-only by decision**
|
|
203
|
+
- **Linting is local-only by decision** - the skill's `references/lint-local.sh` with `references/swiftlint.draft.yml`, config
|
|
204
204
|
and baseline kept outside the repository; no committed config, no build phase, no CI job.
|
|
205
205
|
State this honestly in the plan rather than describing a gate that does not exist: a
|
|
206
206
|
pre-PR habit catches less than a wall, so the residual risk is real and belongs in the
|
|
207
|
-
report. Adopting it project-wide remains an open decision
|
|
207
|
+
report. Adopting it project-wide remains an open decision - record it as one so it stays
|
|
208
208
|
visible instead of quietly lapsing.
|
|
209
209
|
- Never imply the audit substitutes for the linter, or the linter for a gate. The audit finds
|
|
210
210
|
the debt once; the linter keeps a developer honest; only a gate stops it returning.
|
|
211
|
-
14. **Numeric exit criteria**
|
|
211
|
+
14. **Numeric exit criteria** - target and today's measured value side by side: lint violations
|
|
212
212
|
per 1k lines (<1) · public-surface ratio, public declarations ÷ externally consumed symbols
|
|
213
213
|
(≈1) · forbidden cross-module imports (0) · removability delta · files over threshold and
|
|
214
214
|
files >120 lines with no sectioning (0) · screens missing a manifest role (0) · escape-hatch
|
|
215
215
|
count `[CONC-03]` (flat or falling) · exception-marker count (flat or falling) · dead code (0)
|
|
216
216
|
· screens with logic and no mirrored tests (0) · unclassified sensitive symbols (0).
|
|
217
|
-
15. **Exception ledger**
|
|
217
|
+
15. **Exception ledger** - every `// standard:exception(<ID>)` in the module: rule · reason ·
|
|
218
218
|
expiry · expired yes/no.
|
|
219
|
-
16. **Open Questions**
|
|
220
|
-
17. **Suggested commit slicing**
|
|
221
|
-
18. **Scope guard**
|
|
219
|
+
16. **Open Questions** - Phase 2b dimensions with no majority, and unclassifiable symbols.
|
|
220
|
+
17. **Suggested commit slicing** - one commit per file-group / concern.
|
|
221
|
+
18. **Scope guard** - only this module is touched; definition of done is the module's own
|
|
222
222
|
verification path.
|
|
223
223
|
|
|
224
224
|
Then write a **separate one-page onboarding summary** beside the plan, aimed at a developer
|
|
225
225
|
joining next week rather than at whoever does the remediation: the ten always/never lines
|
|
226
226
|
specialised to this module, the screen file manifest, the placement ladder, the sensitive-data
|
|
227
|
-
classes in play, and the verification command. Nobody learns a codebase from a 40-finding audit
|
|
227
|
+
classes in play, and the verification command. Nobody learns a codebase from a 40-finding audit -
|
|
228
228
|
this page is the actual deliverable, and the audit is how you earn the right to write it
|
|
229
|
-
accurately. Derive it from `references/STANDARD.md
|
|
229
|
+
accurately. Derive it from `references/STANDARD.md section 0`, do not re-invent it.
|
|
230
230
|
|
|
231
231
|
Show a concise version of the plan to the user too.
|
|
232
232
|
|
|
233
|
-
## Phase 5
|
|
233
|
+
## Phase 5 - Hand off
|
|
234
234
|
|
|
235
235
|
`AskUserQuestion` (single-select, `outputLanguage`):
|
|
236
236
|
|
|
@@ -245,10 +245,10 @@ verifies, and commits only when asked. This routine ends at the handoff.
|
|
|
245
245
|
|
|
246
246
|
## Notes
|
|
247
247
|
|
|
248
|
-
- **Verification reality is per-module
|
|
248
|
+
- **Verification reality is per-module - establish it, do not assume it.** Some targets cannot be
|
|
249
249
|
compiled from the CLI (asset symbol generation, UIKit-dependent packages, a dependency failing
|
|
250
250
|
earlier in the graph); there, verification is Xcode and a manifest dump validates the manifest
|
|
251
|
-
only. Never trust a piped build's exit code
|
|
251
|
+
only. Never trust a piped build's exit code - a pipe reports the tail's status. Record the
|
|
252
252
|
module's actual verification command in the plan.
|
|
253
253
|
- One module per run. Several modules means several runs and several plans, never one
|
|
254
254
|
cross-module churn commit.
|
|
@@ -34,7 +34,38 @@ Categories:
|
|
|
34
34
|
|
|
35
35
|
Component work is **no longer a bundled pipeline skill set**. Claude Code dispatches `taskType === "component"` to the enabled `ai-<platform>-engineering-toolkit` **marketplace plugin** (`create-component`, fallback `create-ui-component`) via the Skill tool - full contract in `$HOME/.claude/multi-agent-refs/component-dispatch.md`. The pipeline no longer ships `pipeline/skills/figma-ios|figma-android|figma-common`; the pipeline-unique component skills (iterate loops, performance harness, validate/review, commit, adapters, wiki) were absorbed into the `ai-ios-engineering-toolkit` plugin so component skills live in one place.
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**How each host receives the plugin's skills.** Only Claude Code loads the marketplace
|
|
38
|
+
plugin natively. The other two are served by the installer, so all three end up with the
|
|
39
|
+
same active skill set:
|
|
40
|
+
|
|
41
|
+
| Host | Delivery | Discovery |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Claude Code | the marketplace plugin, loaded natively | matched on description |
|
|
44
|
+
| Copilot CLI | `install/copilot.mjs` copies the **enabled** stack plugin's authored skills (`index`, `reference/`, `workflow/`, `tools/`) into `~/.copilot/skills/` | matched on description |
|
|
45
|
+
| Codex CLI | `install/codex.mjs` copies the same set **plus** `skills/shared/external` into `~/.codex/multi-agent-refs/skills/` as reference files | located by path; the AGENTS.md block carries the `ls` / `grep` recipe |
|
|
46
|
+
|
|
47
|
+
Three rules make that work:
|
|
48
|
+
|
|
49
|
+
1. **`knowledge/` is never copied to Copilot.** A stack plugin's `knowledge/` tree is
|
|
50
|
+
generated from this pipeline's `skills/shared/external`, which the installer already
|
|
51
|
+
lays down there. Copying it again would duplicate ~5 MB the host has.
|
|
52
|
+
2. **Only the ENABLED plugins are delivered.** `fix-bug` and `branch-and-pr` exist in
|
|
53
|
+
four stack plugins, `component`, `state` and `create-component` in three, and the
|
|
54
|
+
destination is flat - delivering every stack would be last-write-wins, so an iOS
|
|
55
|
+
repo could end up running the Android `create-component`. The enabled list in
|
|
56
|
+
`~/.claude/settings.json` is the source of truth, exactly as on Claude Code;
|
|
57
|
+
`--platform` is the fallback when no such list exists.
|
|
58
|
+
3. **Codex takes refs, not list entries.** Measured on 0.145: one plugin declaring 142
|
|
59
|
+
skills surfaced 75 and evicted an unrelated user skill, and every plugin-provided
|
|
60
|
+
skill renders with an empty description, so a block entry buys nothing even when it
|
|
61
|
+
fits. Parity on that host is **reachability**, not an identical listing.
|
|
62
|
+
|
|
63
|
+
> This section previously claimed Copilot "retains its own standalone
|
|
64
|
+
> `~/.copilot/skills/figma-*` copies as a frozen fallback". `install/copilot.mjs` pruned
|
|
65
|
+
> exactly those directories, so the fallback did not exist: `create-screen`,
|
|
66
|
+
> `figma-validate`, `figma-review`, `component`, `state` and `navigation` were all
|
|
67
|
+
> absent, and a component task on Copilot had nothing to dispatch to. The contract
|
|
68
|
+
> documented a safety net the installer deleted.
|
|
38
69
|
|
|
39
70
|
What still MUST match across CLIs for component tasks: the `taskType === "component"` classification, the `state.phases["3"].subphases[]` shape the dispatch layer writes, and `--dev` elision semantics. Resolution/routing is Claude-plugin vs Copilot-local-copy by design.
|
|
40
71
|
|
|
@@ -35,9 +35,14 @@ import {
|
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
|
|
37
37
|
const args = process.argv.slice(2);
|
|
38
|
+
// Accepts both `--key value` and `--key=value`. The `=` form used to fall through to
|
|
39
|
+
// the default, which meant a run meant for a throwaway checkout silently retargeted
|
|
40
|
+
// the user's real marketplace instead - a wrong-target write, reported as success.
|
|
38
41
|
const getArg = (k, d) => {
|
|
42
|
+
const eq = args.find((a) => a.startsWith(`${k}=`));
|
|
43
|
+
if (eq) return eq.slice(k.length + 1);
|
|
39
44
|
const i = args.indexOf(k);
|
|
40
|
-
return i >= 0 ? args[i + 1] : d;
|
|
45
|
+
return i >= 0 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : d;
|
|
41
46
|
};
|
|
42
47
|
const HOME = process.env.HOME;
|
|
43
48
|
const PLUGINS_REPO = getArg("--plugins-repo", join(HOME, "multi-agent-plugins"));
|
|
@@ -102,6 +107,29 @@ const isDir = (p) => {
|
|
|
102
107
|
};
|
|
103
108
|
const listDirs = (p) => (existsSync(p) ? readdirSync(p).filter((n) => isDir(join(p, n))) : []);
|
|
104
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Cheap content fingerprint for a skill directory: every file's relative path and
|
|
112
|
+
* size. Enough to notice an edit without hashing megabytes on every run, and it is
|
|
113
|
+
* only used to decide whether a version bump is warranted.
|
|
114
|
+
*/
|
|
115
|
+
function treeFingerprint(dir) {
|
|
116
|
+
const parts = [];
|
|
117
|
+
const walk = (d, rel) => {
|
|
118
|
+
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
119
|
+
const full = join(d, e.name);
|
|
120
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
121
|
+
if (e.isDirectory()) walk(full, r);
|
|
122
|
+
else if (e.isFile()) parts.push(`${r}:${statSync(full).size}`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
walk(dir, "");
|
|
127
|
+
} catch {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
return parts.join("|");
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
// --- 1. enumerate authoring source ---------------------------------------
|
|
106
134
|
if (!existsSync(EXTERNAL)) {
|
|
107
135
|
console.error(`source not found: ${EXTERNAL}`);
|
|
@@ -180,13 +208,31 @@ for (const [plugin, want] of Object.entries(desired)) {
|
|
|
180
208
|
const have = new Set(listDirs(kdir));
|
|
181
209
|
const toAdd = [...want].filter((s) => !have.has(s)).sort();
|
|
182
210
|
const toRemove = [...have].filter((s) => !want.has(s)).sort();
|
|
183
|
-
const
|
|
211
|
+
const setChanged = toAdd.length > 0 || toRemove.length > 0;
|
|
184
212
|
|
|
185
|
-
|
|
213
|
+
// Re-copy EVERY wanted skill, not just the newly added ones.
|
|
214
|
+
//
|
|
215
|
+
// This used to copy only on a set change, which quietly broke the single-source
|
|
216
|
+
// claim: editing a skill in shared/external left the plugin serving the old text
|
|
217
|
+
// forever, because the set had not changed. Found by editing a promoted skill and
|
|
218
|
+
// seeing the plugin copy stay stale while the generator reported "all plugins up
|
|
219
|
+
// to date".
|
|
220
|
+
//
|
|
221
|
+
// cpSync overwrites, so a full re-copy is idempotent and cheap relative to a
|
|
222
|
+
// publish. Removals still need the explicit delete.
|
|
223
|
+
let contentChanged = false;
|
|
224
|
+
if (!DRY) {
|
|
186
225
|
if (!existsSync(kdir)) mkdirSync(kdir, { recursive: true });
|
|
187
|
-
for (const s of
|
|
226
|
+
for (const s of want) {
|
|
227
|
+
const from = join(EXTERNAL, s);
|
|
228
|
+
const to = join(kdir, s);
|
|
229
|
+
const before = existsSync(to) ? treeFingerprint(to) : "";
|
|
230
|
+
cpSync(from, to, { recursive: true });
|
|
231
|
+
if (treeFingerprint(to) !== before) contentChanged = true;
|
|
232
|
+
}
|
|
188
233
|
for (const s of toRemove) rmSync(join(kdir, s), { recursive: true, force: true });
|
|
189
234
|
}
|
|
235
|
+
const changed = setChanged || contentChanged;
|
|
190
236
|
|
|
191
237
|
// regenerate plugin.json skills[]: keep non-knowledge entries, rebuild knowledge set
|
|
192
238
|
const pj = JSON.parse(readFileSync(pjPath, "utf8"));
|