@mmerterden/multi-agent-pipeline 15.3.1 → 15.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 +24 -0
- package/install/codex.mjs +12 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -67
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -49
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -4
- package/pipeline/lib/extract-conventions.sh +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +9 -3
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +3 -1
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/refactor/dev-toolkit-research.md +72 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +54 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/preferences-template.json +1 -0
- package/pipeline/schemas/prefs.schema.json +5 -0
- package/pipeline/schemas/token-budget.json +1 -1
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +1 -67
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +79 -9
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +21 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +21 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +120 -6
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +66 -4
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,30 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [15.4.0] - 2026-08-18
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Per-project `testPolicy`** (`tdd` | `tests-after` | `none`): Phase 0 resolves it after project selection for every input type, asking once via a native picker when absent (autopilot defaults to `tdd` and notes it). Phase 3 gates the TDD cycle on it; Phase 5 skips the gap scan under `none` and runs only pre-existing targets. Prefs schema + template carry the field.
|
|
23
|
+
- **Ordered stack-toolkit routing candidates**: the ios/android rows resolve the corporate `-engineering-` variant before its public derivation, and a full probe miss records every candidate tried. The conventions census learns a `CrossDomains` root; screen-creation tasks bind the routed toolkit's `create-screen` workflow.
|
|
24
|
+
- **ios-module-structure 0.3.0 / ios-coding-standard 1.2.0** (authoring source): the settled architecture becomes the standard - UnitDialect slot, unit vocabulary roles, STRUCT-18..21 with checker support, UNIT-01..03 / SAFE-03 / MOD-08; the pre-conversion spellings are named residue.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- The test-policy step resolves for Jira-ID and URL inputs, not only free-text; STRUCT-18 reports an unbound contracts root as disabled coverage instead of passing silently.
|
|
28
|
+
|
|
29
|
+
## [15.3.2] - 2026-08-17
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- `local` and `local-autopilot` mode docs pointed at the retired `commands/multi-agent.md` dispatcher spelling; they now reference `commands/multi-agent/SKILL.md`, and the Codex installer rewrites the directory-layout spelling to the router skill (dedicated rewrite rule, smoke guard for the new corruption shape, unit tests).
|
|
34
|
+
- The sync spec copies (command tree + Copilot shared-core mirror) disagreed on the Claude source list and file mapping; both now carry `multi-agent-refs/` and `lib/` and the same mapping rows.
|
|
35
|
+
- The dispatcher entry doc now declares the TaskCreate ordering rule and is scanned by `smoke-tasklist-ordering.sh`, closing the one mode (full interactive) the inventory missed.
|
|
36
|
+
- `check-md-links.mjs` resolves `$HOME/.claude/commands/` links against the repo source, so a future layout move cannot leave dangling dispatcher references silently.
|
|
37
|
+
- Install-layout fingerprint fixture regenerated for `usage-report.mjs` and the relocated reference files.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- refactor Step 0c (dev-toolkit research) and setup Step 3b (App Store Connect onboarding) moved to on-demand references under `multi-agent-refs/`, keeping both skills inside their token budgets.
|
|
42
|
+
|
|
19
43
|
## [15.3.1] - 2026-08-15
|
|
20
44
|
|
|
21
45
|
### Changed
|
package/install/codex.mjs
CHANGED
|
@@ -82,6 +82,18 @@ export const CODEX_PATH_REWRITES = Object.freeze([
|
|
|
82
82
|
to: "$HOME/.codex/skills/multi-agent/SKILL.md",
|
|
83
83
|
},
|
|
84
84
|
{ from: /~\/\.claude\/commands\/multi-agent\.md/g, to: "~/.codex/skills/multi-agent/SKILL.md" },
|
|
85
|
+
// Current dispatcher spelling (directory layout). MUST also precede the
|
|
86
|
+
// sub-command rule: `commands/multi-agent/SKILL.md` would otherwise become
|
|
87
|
+
// `multi-agent-refs/commands/SKILL.md`, which installRefs deliberately never
|
|
88
|
+
// writes (the top-level SKILL.md ships as the router skill instead).
|
|
89
|
+
{
|
|
90
|
+
from: /\$HOME\/\.claude\/commands\/multi-agent\/SKILL\.md/g,
|
|
91
|
+
to: "$HOME/.codex/skills/multi-agent/SKILL.md",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
from: /~\/\.claude\/commands\/multi-agent\/SKILL\.md/g,
|
|
95
|
+
to: "~/.codex/skills/multi-agent/SKILL.md",
|
|
96
|
+
},
|
|
85
97
|
// Sub-command specs become reference files under the Codex refs tree.
|
|
86
98
|
{
|
|
87
99
|
from: /\$HOME\/\.claude\/commands\/multi-agent\b/g,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.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",
|
|
@@ -285,6 +285,10 @@ Quick pointers:
|
|
|
285
285
|
|
|
286
286
|
Load `$HOME/.claude/multi-agent-refs/rules.md` for the full contract (Swift-specific rules, reviewer policy, secret scan, subagent contract).
|
|
287
287
|
|
|
288
|
+
### TaskCreate ordering (strict)
|
|
289
|
+
|
|
290
|
+
**All TaskCreate calls fire in strict phase-number order BEFORE any TaskUpdate is applied** - this binds the full interactive `/multi-agent` run (all 8 phases) exactly like every mode alias. The native widget renders tiles by creation order, not by phase number; out-of-order calls produce visually scrambled tile stacks. Register every tile in order, then flip status via TaskUpdate when a phase actually starts or short-circuits. Full contract: `$HOME/.claude/multi-agent-refs/tracker-contract.md` section "TaskCreate ordering (strict)".
|
|
291
|
+
|
|
288
292
|
---
|
|
289
293
|
|
|
290
294
|
## Status Display
|
|
@@ -40,7 +40,7 @@ Phase 7: Report → Jira / Wiki / Confluence + log + knowledge/memory
|
|
|
40
40
|
|
|
41
41
|
This command routes to the orchestrator with the `--local` flag set. The Phase 0-7 contract from `$HOME/.claude/multi-agent-refs/phases/phase-0-init.md` and the later phase docs applies as-is - only the worktree step is skipped, and `state.projects[*].worktreePath` stays `null`.
|
|
42
42
|
|
|
43
|
-
Read the routing table in `$HOME/.claude/commands/multi-agent.md` and apply Phase 0 Step 8 in local mode (no worktree: continue on the current branch, state file under `.claude/logs/multi-agent/{project}/{taskId}/`).
|
|
43
|
+
Read the routing table in `$HOME/.claude/commands/multi-agent/SKILL.md` and apply Phase 0 Step 8 in local mode (no worktree: continue on the current branch, state file under `.claude/logs/multi-agent/{project}/{taskId}/`).
|
|
44
44
|
|
|
45
45
|
## Examples
|
|
46
46
|
|
|
@@ -69,7 +69,7 @@ Run the full pipeline **without a worktree** and **with every confirmation skipp
|
|
|
69
69
|
|
|
70
70
|
## Delegation
|
|
71
71
|
|
|
72
|
-
Orchestrator routing: the routing table in `$HOME/.claude/commands/multi-agent.md` resolves `local-autopilot` as the union of the `dev-local` + `autopilot` mode mixins. Contract details: `$HOME/.claude/multi-agent-refs/phases/phase-0-init.md` Step 8 (local branch) + `$HOME/.claude/multi-agent-refs/phases/phase-2-planning.md` Step 5 (autopilot gate skip + safety classifier).
|
|
72
|
+
Orchestrator routing: the routing table in `$HOME/.claude/commands/multi-agent/SKILL.md` resolves `local-autopilot` as the union of the `dev-local` + `autopilot` mode mixins. Contract details: `$HOME/.claude/multi-agent-refs/phases/phase-0-init.md` Step 8 (local branch) + `$HOME/.claude/multi-agent-refs/phases/phase-2-planning.md` Step 5 (autopilot gate skip + safety classifier).
|
|
73
73
|
## Required: outward-facing payload contracts
|
|
74
74
|
|
|
75
75
|
Before writing anything outward-facing - PR body, Jira comment, Confluence page, closing report - load `$HOME/.claude/multi-agent-refs/payload-contracts.md`. It names the canonical section set for each payload, the markup dialect per surface (PR body is Markdown, Jira is wiki markup - mixing them is a defect), and the token/duration numbers the closing report must carry. Improvising a payload shape from memory is the most common failure of the short modes.
|
|
@@ -112,73 +112,7 @@ Procedure:
|
|
|
112
112
|
|
|
113
113
|
The pipeline's hands on devices and browsers are MCP tools served by a companion repo (`dev-toolkit-mcp`): Phase 5 test, `manual-test`, `design-check` and `apple-archive-compliance` all call them, and several pipeline skills declare a minimum toolkit version (see `cross-cli-contract.md`). That repo therefore has to track the MCP field, not just its own README. This step researches what current practice is and audits the toolkit against it.
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
1. `prefs.global.devToolkit` in `~/.claude/multi-agent-preferences.json`:
|
|
118
|
-
|
|
119
|
-
```jsonc
|
|
120
|
-
{
|
|
121
|
-
"enabled": true,
|
|
122
|
-
"label": "<human name>",
|
|
123
|
-
"localPath": "$HOME/<repo-dir>", // the companion repo working copy
|
|
124
|
-
"mcpServerName": "<registered MCP server name>",
|
|
125
|
-
"packageName": "@<scope>/<package>",
|
|
126
|
-
"registry": "github-packages", // github-packages | npmjs | none
|
|
127
|
-
"repoUrl": "https://github.com/<owner>/<repo>"
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
2. If unset, auto-detect from the MCP registration: read `mcpServers` in `~/.claude.json` (including each `projects[*].mcpServers`) and in `~/.claude/settings.json`; for a stdio entry whose command is `node`, take `dirname(args[0])`. Accept it only when that directory is a git repo whose `package.json` depends on `@modelcontextprotocol/sdk`.
|
|
132
|
-
3. If neither resolves, skip this step and report "no dev-toolkit configured". Never guess a path, never clone.
|
|
133
|
-
4. `enabled: false` skips the step.
|
|
134
|
-
|
|
135
|
-
**Research axes** - a finding without a source link is not a finding:
|
|
136
|
-
|
|
137
|
-
| # | Axis | Where to look | What to extract |
|
|
138
|
-
|---|------|---------------|-----------------|
|
|
139
|
-
| 1 | MCP protocol | spec revisions + `@modelcontextprotocol/sdk` releases | protocol features released since the pinned SDK range that the server does not use yet: tool annotations (`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`), `outputSchema` + structured content, resource links in results, progress + cancellation, `tools/list_changed`, pagination, elicitation |
|
|
140
|
-
| 2 | Host clients | Claude Code / Copilot CLI / Cursor / Antigravity docs + release notes | per-tool description budget, tool-count ceilings, naming conventions, image and output size limits, permission / allowlist ergonomics |
|
|
141
|
-
| 3 | Peer servers | GitHub search on the same domain terms + `stars:>50` | tool surfaces we lack, conventions peers converged on, and what to discard as out of scope |
|
|
142
|
-
| 4 | Wrapped tooling | `xcrun simctl help`, `idb`, `adb`, `xcodebuild`, Playwright release notes, Apple ITMS + App Store Review Guidelines | deprecated flags still in use, new capabilities worth a tool, audit rules that changed |
|
|
143
|
-
| 5 | Field practice | X / Twitter, Reddit, MCP community threads | what server authors actually changed recently (transport choice, output-token diets, sandboxing, error shape) |
|
|
144
|
-
|
|
145
|
-
**Audit the toolkit against the findings** - run the checks, do not assume:
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
DT="<resolved localPath>"
|
|
149
|
-
node --check "$DT/index.js"
|
|
150
|
-
find "$DT/tools" -name "*.js" -type f -exec node --check {} \;
|
|
151
|
-
|
|
152
|
-
# stdout carries the JSON-RPC frames: a stray stdout write corrupts the stream
|
|
153
|
-
grep -rn "console\.log(" "$DT/index.js" "$DT/tools" || echo "stdout clean"
|
|
154
|
-
|
|
155
|
-
# advertised tool counts vs reality (README header + package.json description)
|
|
156
|
-
grep -nE "[0-9]+ tools" "$DT/README.md" "$DT/package.json"
|
|
157
|
-
|
|
158
|
-
# packaging: every runtime directory must be inside files[]
|
|
159
|
-
node -p "require('$DT/package.json').files.join('\n')"
|
|
160
|
-
ls -d "$DT"/tools/*/
|
|
161
|
-
|
|
162
|
-
cd "$DT" && npm outdated; npm audit --omit=dev 2>/dev/null | tail -20
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Also check: every tool carries a description and an `inputSchema`; token-heavy results (screenshots, UI trees, logs) are truncated or written to a file path instead of inlined; failures return an error result with an actionable message instead of throwing; `engines.node` matches what the SDK needs; `CHANGELOG.md`, a CI workflow and a test harness exist.
|
|
166
|
-
|
|
167
|
-
Output (plan band E):
|
|
168
|
-
|
|
169
|
-
```
|
|
170
|
-
| # | Axis | Finding | Source | Adaptation in the toolkit (file) | Effort | Impact | In plan? |
|
|
171
|
-
|---|------|---------|--------|----------------------------------|--------|--------|----------|
|
|
172
|
-
| 1 | Protocol | read-only tools carry no annotations | <spec link> | add `annotations` to the read-only tools in index.js | Low | Medium | Yes (P1) |
|
|
173
|
-
| 2 | Wrapped tooling | uses a simctl flag removed in Xcode <v> | <release notes> | switch tools/<family>/<file>.js to <new flag> | Low | High | Yes (P0) |
|
|
174
|
-
| 3 | Peer servers | peer exposes <surface> | <repo link> | does not fit: outside the pipeline's phases | - | - | No |
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Rules for this band:
|
|
178
|
-
|
|
179
|
-
- Band-E work lands in the toolkit repo, never mirrored into this one. Shipping it is `/multi-agent:sync` Step 3d.
|
|
180
|
-
- A finding that changes the tool surface (new / renamed / removed tool) pairs with a pipeline-side item: bump the minimum toolkit version wherever a pipeline skill declares one.
|
|
181
|
-
- If the current working directory IS the toolkit repo, skip band E and let bands A/B/C cover it - never report the same finding twice.
|
|
115
|
+
Full procedure - resolution (configuration first, never a hardcoded path; skip when nothing resolves or `enabled` is false), the 5 research axes, the audit command block, and the band-E output table + rules - lives in `$HOME/.claude/multi-agent-refs/refactor/dev-toolkit-research.md`. Read it before running this step.
|
|
182
116
|
|
|
183
117
|
## Step 0d: RUN-ERRORS - what the pipeline actually failed on
|
|
184
118
|
|
|
@@ -402,55 +402,7 @@ This builds `platformIdentityRouting` incrementally - no separate Step 7 neede
|
|
|
402
402
|
|
|
403
403
|
### Step 3b - App Store Connect onboarding (iOS only, optional)
|
|
404
404
|
|
|
405
|
-
Runs inside Step 3 alongside the other missing credentials,
|
|
406
|
-
a user who already has an App Store Connect credential in their keychain gets it
|
|
407
|
-
mapped by Step 1 discovery like any other token, and only the genuinely missing
|
|
408
|
-
pieces reach this flow.
|
|
409
|
-
|
|
410
|
-
Three of the four entries do not go through the normal Token Save Flow, because
|
|
411
|
-
what they hold is not a pasteable secret:
|
|
412
|
-
|
|
413
|
-
| Entry | Holds | Flow |
|
|
414
|
-
|---|---|---|
|
|
415
|
-
| `appstore_connect_key_id` | an identifier | plain value, not a secret; still mapped so it is read through the mapping layer |
|
|
416
|
-
| `appstore_connect_issuer_id` | an identifier | same |
|
|
417
|
-
| `appstore_connect_apple_id` | an email address | same |
|
|
418
|
-
| `appstore_connect_password_item` | a keychain ITEM NAME | the password lives in Apple's own keychain item, referenced as `-p @keychain:<item>` and never read by the pipeline |
|
|
419
|
-
|
|
420
|
-
Ask which tier to configure (picker): **API key** / **Apple ID + app-specific
|
|
421
|
-
password** / **Skip**. Lead with the second when the user says they cannot create
|
|
422
|
-
an API key.
|
|
423
|
-
|
|
424
|
-
**API key.** The private key is a FILE and is never copied into the credential
|
|
425
|
-
store. It must sit in a directory `altool` already searches:
|
|
426
|
-
|
|
427
|
-
```bash
|
|
428
|
-
ls ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null \
|
|
429
|
-
|| echo "MISSING: put AuthKey_<keyId>.p8 in ~/.appstoreconnect/private_keys/"
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
**Apple ID + app-specific password.** Use Apple's own keychain helper. The secret
|
|
433
|
-
never enters chat and never becomes a shell argument, per the Token Save Flow rule:
|
|
434
|
-
|
|
435
|
-
```bash
|
|
436
|
-
# the user exports AC_PASSWORD_ONCE in their own shell, for this one command
|
|
437
|
-
xcrun altool --store-password-in-keychain-item "<item-name>" \
|
|
438
|
-
-u "<apple-id>" -p @env:AC_PASSWORD_ONCE
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
Then map only `<item-name>` as `appstore_connect_password_item`.
|
|
442
|
-
|
|
443
|
-
**Multi-provider accounts.** A corporate Apple ID often belongs to several
|
|
444
|
-
providers, and `altool` fails opaquely without one. Resolve it once with
|
|
445
|
-
`ios_testflight_validate({list_providers: true, <credentials just configured>})`
|
|
446
|
-
and store the answer under
|
|
447
|
-
`prefs.projects[<key>].appStoreConnect.providerPublicId` - per-project, since a
|
|
448
|
-
user can ship for more than one team.
|
|
449
|
-
|
|
450
|
-
**Verify + expiry.** Re-run the `list_providers` probe and report the resolved
|
|
451
|
-
tier. A credential that resolves but is rejected (401/403) follows the
|
|
452
|
-
Expired-token decision in `refs/keychain.md` Rule 1 - Regenerate / Use a
|
|
453
|
-
different token / Skip and continue - never a silent drop.
|
|
405
|
+
Runs inside Step 3 alongside the other missing credentials. The four `appstore_connect_*` entries, their non-secret handling (three hold identifiers / a keychain item name, not pasteable secrets), the API-key vs Apple-ID tier picker, and the verification probes live in `$HOME/.claude/multi-agent-refs/setup/app-store-connect.md`. Read that file when the user opts into App Store Connect onboarding; on Skip, nothing is written.
|
|
454
406
|
|
|
455
407
|
### Step 3.5 - Host Prompt (embedded in Token Save Flow)
|
|
456
408
|
|
|
@@ -25,7 +25,7 @@ When invoked, it synchronizes all targets in order. It detects what changed, upd
|
|
|
25
25
|
|
|
26
26
|
| # | Target | Path | Direction |
|
|
27
27
|
|---|-------|-----|-----|
|
|
28
|
-
| 1 | Claude Code (source of truth) | `~/.claude/commands/multi-agent
|
|
28
|
+
| 1 | Claude Code (source of truth) | `~/.claude/commands/multi-agent/` + `~/.claude/multi-agent-refs/` + `~/.claude/agents/` + `~/.claude/scripts/` + `~/.claude/lib/` | source |
|
|
29
29
|
| 2 | Copilot CLI | `~/.copilot/copilot-instructions.md` + `~/.copilot/skills/` | <- from Claude |
|
|
30
30
|
| 2b | Codex CLI | `~/.codex/AGENTS.md` + `~/.codex/skills/multi-agent/` + `~/.codex/multi-agent-refs/` + `~/.codex/agents/*.toml` | <- from Claude (path-rewritten) |
|
|
31
31
|
| 3 | multi-agent-pipeline repo | `~/multi-agent-pipeline/pipeline/` | <- from Claude (genericized) |
|
|
@@ -87,7 +87,7 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
87
87
|
|
|
88
88
|
1. **Read first** - understand every target's current state before making any change
|
|
89
89
|
2. **Preserve file structure** - each target has its own format:
|
|
90
|
-
- `multi-agent
|
|
90
|
+
- `commands/multi-agent/` SKILL tree + `multi-agent-refs/phases/`: detailed pipeline spec (frontmatter, phases, routing)
|
|
91
91
|
- `copilot-instructions.md`: general development instructions + pipeline summary section
|
|
92
92
|
- `multi-agent-pipeline/pipeline/`: generic open-source version (NO personal data)
|
|
93
93
|
3. **Sync the shared sections** (Claude ↔ Copilot):
|
|
@@ -105,7 +105,7 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
105
105
|
|
|
106
106
|
## Pipeline Repo Sync Rules (CRITICAL)
|
|
107
107
|
|
|
108
|
-
1. **Source**: `~/.claude/commands/multi-agent
|
|
108
|
+
1. **Source**: `~/.claude/commands/multi-agent/` + `~/.claude/multi-agent-refs/` + `~/.claude/agents/` + `~/.claude/lib/`
|
|
109
109
|
2. **Target**: `~/multi-agent-pipeline/pipeline/`
|
|
110
110
|
3. **Genericization** (must be applied BEFORE writing to the target):
|
|
111
111
|
- Jira project key (`prefs.global.defaultJiraKey`) -> `{JIRA_KEY}`, `PROJ` in examples
|
|
@@ -116,7 +116,6 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
116
116
|
- **Verify**: `grep` scan for personal/corporate data inside pipeline/ -> 0 results
|
|
117
117
|
4. **File mapping**:
|
|
118
118
|
```
|
|
119
|
-
~/.claude/commands/multi-agent.md -> pipeline/commands/multi-agent.md
|
|
120
119
|
~/.claude/commands/multi-agent/ -> pipeline/commands/multi-agent/
|
|
121
120
|
~/.claude/multi-agent-refs/ -> pipeline/multi-agent-refs/ (refs + pickers, non-command)
|
|
122
121
|
~/.claude/commands/sim-test.md -> pipeline/commands/sim-test.md
|
|
@@ -20,12 +20,18 @@ Phase 3 pre-flight, before any code is written, for **every** `taskType`. Compon
|
|
|
20
20
|
|
|
21
21
|
Platform comes from the same mapping component dispatch uses, so the two cannot disagree:
|
|
22
22
|
|
|
23
|
-
| `state.platform` / detected stack | Toolkit |
|
|
23
|
+
| `state.platform` / detected stack | Toolkit candidates, first enabled wins |
|
|
24
24
|
|---|---|
|
|
25
|
-
| ios, swift | `ai-ios-toolkit` |
|
|
26
|
-
| android, kotlin | `ai-android-toolkit` |
|
|
25
|
+
| ios, swift | `ai-ios-engineering-toolkit`, then `ai-ios-toolkit` |
|
|
26
|
+
| android, kotlin | `ai-android-engineering-toolkit`, then `ai-android-toolkit` |
|
|
27
27
|
| anything else | no toolkit - step is a recorded no-op |
|
|
28
28
|
|
|
29
|
+
A row is an ORDERED candidate list, not one name: the corporate toolkit (where one exists)
|
|
30
|
+
carries the project-specific doctrine and outranks its public derivation, so it resolves first.
|
|
31
|
+
Probe the candidates in order against the enabled plugins and take the first hit. When none is
|
|
32
|
+
enabled, the recorded no-op names every candidate probed - a repo that enabled a corporate
|
|
33
|
+
toolkit the table did not know about must show up as a probe miss, never as silence.
|
|
34
|
+
|
|
29
35
|
The toolkit is enabled per repo (`.claude/settings.local.json` / `~/.claude/settings.json` `enabledPlugins`). **Not enabled is not an error here**, unlike component dispatch: a backend or web repo legitimately has no toolkit, and halting would make the pipeline unusable outside mobile. Record the no-op and continue.
|
|
30
36
|
|
|
31
37
|
Two marketplaces may ship the same toolkit name (a public one and a corporate one). Resolve whichever is enabled and record its **name and version** in the ledger entry, because the routing table and the skill set differ between versions - a finding that cites a skill has to be traceable to the version that defined it.
|
|
@@ -35,7 +35,7 @@ Read preferences: `PREFS_FILE="$HOME/.claude/multi-agent-preferences.json"` (if
|
|
|
35
35
|
OUTPUT_LANG=$(jq -r '.global.outputLanguage // "en"' "$PREFS_FILE" 2>/dev/null || echo en)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
From this point on, everything the user reads renders in `$OUTPUT_LANG`: conversational lines, `AskUserQuestion` `question`/`description`, and external payload bodies (PR/Jira/Confluence). English stays only on `label`/`header`, commit messages, branch names, PR titles, identifiers. Full matrix: `rules.md` "Language Application".
|
|
38
|
+
From this point on, everything the user reads renders in `$OUTPUT_LANG`: conversational lines, `AskUserQuestion` `question`/`description`, and external payload bodies (PR/Jira/Confluence). English stays only on `label`/`header`, commit messages, branch names, PR titles, identifiers. Full matrix: `rules.md` "Language Application".
|
|
39
39
|
|
|
40
40
|
**Model fallback date gate** (same step, once per run): read `prefs.global.modelFallback`. If `premiumTierUntil` is set and in the past, apply the date-gate trigger from `$HOME/.claude/multi-agent-refs/features/model-fallback.md` - `preferredModel` personas dispatch on `fallbackModel` for this run, with the one-line WARN. Dispatch-error and budget triggers in that contract apply per-dispatch later; nothing else to do here.
|
|
41
41
|
|
|
@@ -238,6 +238,8 @@ Scan `$HOME` (maxdepth 2) for project markers (`.xcodeproj`, `Package.swift`, `b
|
|
|
238
238
|
- After selection, if the chosen set matches an existing `recentGroups` entry, bump `count` + `lastUsed`. If it's a new combination of ≥2 repos, ask with a native `AskUserQuestion` picker (`question`: "Save this repo combo as a reusable multi-repo group?" in `outputLanguage`; `header`: "Save"; `options`: `{ label: "Save", ... }`, `{ label: "Skip", ... }`) - on **Save**, prepend to `recentGroups` (LRU cap 10).
|
|
239
239
|
- Single-repo selection (1 repo) → legacy single-repo path; `state.projects[]` is omitted, scalars are populated as before.
|
|
240
240
|
|
|
241
|
+
**Test policy (once per project).** Resolve `prefs.projects[{slug}].testPolicy` → `state.testPolicy`; missing → native picker "Should development write tests here?" (`header`: "Tests"): `tdd` (recommended) / `tests-after` / `none`; persist. Autopilot without a record: `tdd`, noted.
|
|
242
|
+
|
|
241
243
|
#### Step 3 - Remote Detection + Branch Selection
|
|
242
244
|
|
|
243
245
|
1. **Check preferences first**: If `prefs.projects[{project}].remoteType` exists, use cached value.
|
|
@@ -38,7 +38,7 @@ Pre-flight steps (run in order, abort on failure).
|
|
|
38
38
|
|
|
39
39
|
`targetFiles` is required - without it a skill applied to the wrong files still reads as "applied". Append at the moment of consultation, not at the end of the phase. Phase 4 Step 1.78 treats this as self-report only and resolves criteria independently; it is the one signal separating "applied to the wrong files" from "never opened".
|
|
40
40
|
|
|
41
|
-
9. **Stack skill routing (every `taskType`, when a stack toolkit plugin is enabled)**: ask the
|
|
41
|
+
9. **Stack skill routing (every `taskType`, when a stack toolkit plugin is enabled)**: ask the routed stack toolkit's own `index` skill which skills govern this task, load them BEFORE writing code, and record each into `state.telemetry.skillCalls[]` with `routedBy: "<toolkit>:index@<version>"`. The routing table stays in the plugin - a copy here would be the stale one. A screen-creation task loads the routed toolkit's `workflow/create-screen` when one exists. No toolkit, or none enabled, is a recorded no-op, not a halt. Contract: [`features/stack-skill-routing.md`]($HOME/.claude/multi-agent-refs/features/stack-skill-routing.md).
|
|
42
42
|
|
|
43
43
|
The analysis document is the SOLE design source in Phase 3. Variant choices, padding values, color tokens, copy strings, accessibility identifiers, and test method names all come from the rendered Pass B cells. If something is missing in the analysis doc, the fix is to re-run `/multi-agent:analysis`, not to fetch from Figma.
|
|
44
44
|
|
|
@@ -112,7 +112,7 @@ For each task (respecting dependency order):
|
|
|
112
112
|
- New types/tokens in the **common** submodule
|
|
113
113
|
- The component itself in the **uicomponents** submodule
|
|
114
114
|
- Work in BOTH submodules in the same task - this is normal and expected
|
|
115
|
-
3. **TDD cycle** (Launch Agent with `model: "sonnet"`)
|
|
115
|
+
3. **TDD cycle** (Launch Agent with `model: "sonnet"`) - gated by `state.testPolicy`: `tdd` = the loop below; `tests-after` = skip RED, author the same tests once green; `none` = author no tests (existing ones never weakened), report "tests not written - project policy" rather than a gap.
|
|
116
116
|
|
|
117
117
|
**RED - Write ONE failing test first:**
|
|
118
118
|
- **Rework re-entry**: if `state.reviewIterations[-1].verifyByTest.redTests[]` exists (Phase 4 Step 3.7 ran), those failing repro tests ARE the RED step for their findings - make them green, do not write a duplicate failing test and do not delete or weaken them. See `$HOME/.claude/multi-agent-refs/features/verify-by-test.md`.
|
|
@@ -7,7 +7,7 @@ Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md` -
|
|
|
7
7
|
|
|
8
8
|
#### Step 0 - Test Gap Report (advisory)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
`state.testPolicy: none` → skip the gap scan (the gap IS the recorded policy) and run only pre-existing test targets; none → recorded no-op. Otherwise, before the local-checkout prompt, run the static test-gap detector. Heuristic, deterministic, no LLM, sub-second. The report ends up in `agent-log.md` under "Test Scenarios" and surfaces public symbols added in this branch that have no paired test.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
STACK=$(jq -r '.analysis.stack.primary // "unknown"' "$STATE_FILE")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Dev-Toolkit Research (refactor Step 0c)
|
|
2
|
+
|
|
3
|
+
Loaded on demand by `/multi-agent:refactor` Step 0c. The SKILL.md carries the step intro; this file is the full procedure.
|
|
4
|
+
|
|
5
|
+
**Resolution** - configuration first, never a hardcoded path:
|
|
6
|
+
|
|
7
|
+
1. `prefs.global.devToolkit` in `~/.claude/multi-agent-preferences.json`:
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
{
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"label": "<human name>",
|
|
13
|
+
"localPath": "$HOME/<repo-dir>", // the companion repo working copy
|
|
14
|
+
"mcpServerName": "<registered MCP server name>",
|
|
15
|
+
"packageName": "@<scope>/<package>",
|
|
16
|
+
"registry": "github-packages", // github-packages | npmjs | none
|
|
17
|
+
"repoUrl": "https://github.com/<owner>/<repo>"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. If unset, auto-detect from the MCP registration: read `mcpServers` in `~/.claude.json` (including each `projects[*].mcpServers`) and in `~/.claude/settings.json`; for a stdio entry whose command is `node`, take `dirname(args[0])`. Accept it only when that directory is a git repo whose `package.json` depends on `@modelcontextprotocol/sdk`.
|
|
22
|
+
3. If neither resolves, skip this step and report "no dev-toolkit configured". Never guess a path, never clone.
|
|
23
|
+
4. `enabled: false` skips the step.
|
|
24
|
+
|
|
25
|
+
**Research axes** - a finding without a source link is not a finding:
|
|
26
|
+
|
|
27
|
+
| # | Axis | Where to look | What to extract |
|
|
28
|
+
|---|------|---------------|-----------------|
|
|
29
|
+
| 1 | MCP protocol | spec revisions + `@modelcontextprotocol/sdk` releases | protocol features released since the pinned SDK range that the server does not use yet: tool annotations (`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`), `outputSchema` + structured content, resource links in results, progress + cancellation, `tools/list_changed`, pagination, elicitation |
|
|
30
|
+
| 2 | Host clients | Claude Code / Copilot CLI / Cursor / Antigravity docs + release notes | per-tool description budget, tool-count ceilings, naming conventions, image and output size limits, permission / allowlist ergonomics |
|
|
31
|
+
| 3 | Peer servers | GitHub search on the same domain terms + `stars:>50` | tool surfaces we lack, conventions peers converged on, and what to discard as out of scope |
|
|
32
|
+
| 4 | Wrapped tooling | `xcrun simctl help`, `idb`, `adb`, `xcodebuild`, Playwright release notes, Apple ITMS + App Store Review Guidelines | deprecated flags still in use, new capabilities worth a tool, audit rules that changed |
|
|
33
|
+
| 5 | Field practice | X / Twitter, Reddit, MCP community threads | what server authors actually changed recently (transport choice, output-token diets, sandboxing, error shape) |
|
|
34
|
+
|
|
35
|
+
**Audit the toolkit against the findings** - run the checks, do not assume:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
DT="<resolved localPath>"
|
|
39
|
+
node --check "$DT/index.js"
|
|
40
|
+
find "$DT/tools" -name "*.js" -type f -exec node --check {} \;
|
|
41
|
+
|
|
42
|
+
# stdout carries the JSON-RPC frames: a stray stdout write corrupts the stream
|
|
43
|
+
grep -rn "console\.log(" "$DT/index.js" "$DT/tools" || echo "stdout clean"
|
|
44
|
+
|
|
45
|
+
# advertised tool counts vs reality (README header + package.json description)
|
|
46
|
+
grep -nE "[0-9]+ tools" "$DT/README.md" "$DT/package.json"
|
|
47
|
+
|
|
48
|
+
# packaging: every runtime directory must be inside files[]
|
|
49
|
+
node -p "require('$DT/package.json').files.join('\n')"
|
|
50
|
+
ls -d "$DT"/tools/*/
|
|
51
|
+
|
|
52
|
+
cd "$DT" && npm outdated; npm audit --omit=dev 2>/dev/null | tail -20
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Also check: every tool carries a description and an `inputSchema`; token-heavy results (screenshots, UI trees, logs) are truncated or written to a file path instead of inlined; failures return an error result with an actionable message instead of throwing; `engines.node` matches what the SDK needs; `CHANGELOG.md`, a CI workflow and a test harness exist.
|
|
56
|
+
|
|
57
|
+
Output (plan band E):
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
| # | Axis | Finding | Source | Adaptation in the toolkit (file) | Effort | Impact | In plan? |
|
|
61
|
+
|---|------|---------|--------|----------------------------------|--------|--------|----------|
|
|
62
|
+
| 1 | Protocol | read-only tools carry no annotations | <spec link> | add `annotations` to the read-only tools in index.js | Low | Medium | Yes (P1) |
|
|
63
|
+
| 2 | Wrapped tooling | uses a simctl flag removed in Xcode <v> | <release notes> | switch tools/<family>/<file>.js to <new flag> | Low | High | Yes (P0) |
|
|
64
|
+
| 3 | Peer servers | peer exposes <surface> | <repo link> | does not fit: outside the pipeline's phases | - | - | No |
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Rules for this band:
|
|
68
|
+
|
|
69
|
+
- Band-E work lands in the toolkit repo, never mirrored into this one. Shipping it is `/multi-agent:sync` Step 3d.
|
|
70
|
+
- A finding that changes the tool surface (new / renamed / removed tool) pairs with a pipeline-side item: bump the minimum toolkit version wherever a pipeline skill declares one.
|
|
71
|
+
- If the current working directory IS the toolkit repo, skip band E and let bands A/B/C cover it - never report the same finding twice.
|
|
72
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# App Store Connect Onboarding (setup Step 3b)
|
|
2
|
+
|
|
3
|
+
Loaded on demand by `/multi-agent:setup` Step 3b (iOS only, optional). The SKILL.md carries the step intro; this file is the full flow.
|
|
4
|
+
|
|
5
|
+
Runs inside Step 3 alongside the other missing credentials, not as a late add-on:
|
|
6
|
+
a user who already has an App Store Connect credential in their keychain gets it
|
|
7
|
+
mapped by Step 1 discovery like any other token, and only the genuinely missing
|
|
8
|
+
pieces reach this flow.
|
|
9
|
+
|
|
10
|
+
Three of the four entries do not go through the normal Token Save Flow, because
|
|
11
|
+
what they hold is not a pasteable secret:
|
|
12
|
+
|
|
13
|
+
| Entry | Holds | Flow |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `appstore_connect_key_id` | an identifier | plain value, not a secret; still mapped so it is read through the mapping layer |
|
|
16
|
+
| `appstore_connect_issuer_id` | an identifier | same |
|
|
17
|
+
| `appstore_connect_apple_id` | an email address | same |
|
|
18
|
+
| `appstore_connect_password_item` | a keychain ITEM NAME | the password lives in Apple's own keychain item, referenced as `-p @keychain:<item>` and never read by the pipeline |
|
|
19
|
+
|
|
20
|
+
Ask which tier to configure (picker): **API key** / **Apple ID + app-specific
|
|
21
|
+
password** / **Skip**. Lead with the second when the user says they cannot create
|
|
22
|
+
an API key.
|
|
23
|
+
|
|
24
|
+
**API key.** The private key is a FILE and is never copied into the credential
|
|
25
|
+
store. It must sit in a directory `altool` already searches:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ls ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null \
|
|
29
|
+
|| echo "MISSING: put AuthKey_<keyId>.p8 in ~/.appstoreconnect/private_keys/"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Apple ID + app-specific password.** Use Apple's own keychain helper. The secret
|
|
33
|
+
never enters chat and never becomes a shell argument, per the Token Save Flow rule:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# the user exports AC_PASSWORD_ONCE in their own shell, for this one command
|
|
37
|
+
xcrun altool --store-password-in-keychain-item "<item-name>" \
|
|
38
|
+
-u "<apple-id>" -p @env:AC_PASSWORD_ONCE
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then map only `<item-name>` as `appstore_connect_password_item`.
|
|
42
|
+
|
|
43
|
+
**Multi-provider accounts.** A corporate Apple ID often belongs to several
|
|
44
|
+
providers, and `altool` fails opaquely without one. Resolve it once with
|
|
45
|
+
`ios_testflight_validate({list_providers: true, <credentials just configured>})`
|
|
46
|
+
and store the answer under
|
|
47
|
+
`prefs.projects[<key>].appStoreConnect.providerPublicId` - per-project, since a
|
|
48
|
+
user can ship for more than one team.
|
|
49
|
+
|
|
50
|
+
**Verify + expiry.** Re-run the `list_providers` probe and report the resolved
|
|
51
|
+
tier. A credential that resolves but is rejected (401/403) follows the
|
|
52
|
+
Expired-token decision in `refs/keychain.md` Rule 1 - Regenerate / Use a
|
|
53
|
+
different token / Skip and continue - never a silent drop.
|
|
54
|
+
|
|
@@ -142,7 +142,7 @@ Mode-specific phase sets:
|
|
|
142
142
|
|
|
143
143
|
Note: in `--dev` modes the omitted phases (1, 2 - plus 5 in the autopilot/local variants) **do not get TaskCreate calls at all** - they're not part of the mode's phase set. Phase 4 is in every mode's set as of v14.0.0, so it is always registered. The "[SKIPPED]" pattern above only applies if a phase that IS in the set is conditionally skipped at runtime. The authoritative per-mode set is the `for p in ...` init block in each mode's own entry doc, generated by `gen-mode-dispatch.mjs`; this table mirrors those blocks.
|
|
144
144
|
|
|
145
|
-
**Enforcement**: `smoke-tasklist-ordering.sh` scans
|
|
145
|
+
**Enforcement**: `smoke-tasklist-ordering.sh` scans the dispatcher (`commands/multi-agent/SKILL.md`) and every mode entry point doc (`commands/multi-agent/{dev,autopilot,local,local-autopilot,dev-autopilot,dev-local,dev-local-autopilot,resume-local}/SKILL.md` + the Copilot full-inline orchestrator mirror) for the explicit "in phase-number order" rule. Inventory drift fails the smoke.
|
|
146
146
|
|
|
147
147
|
### Other CLIs - call render after every state change
|
|
148
148
|
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
"projects": {},
|
|
97
|
+
"_projectsComment": "Per-project overrides by slug. Notable: testPolicy (tdd | tests-after | none) - how the dev phase authors tests; asked once by Phase 0 when absent.",
|
|
97
98
|
"_figmaConfigTemplate": {
|
|
98
99
|
"_comment": "Copy this to ~/.claude/projects/{slug}/figma-config.json and fill in values. See pipeline/schemas/figma-project-config.schema.json for full schema.",
|
|
99
100
|
"schemaVersion": "1.0.0",
|
|
@@ -1545,6 +1545,11 @@
|
|
|
1545
1545
|
"type": "object",
|
|
1546
1546
|
"additionalProperties": false,
|
|
1547
1547
|
"properties": {
|
|
1548
|
+
"testPolicy": {
|
|
1549
|
+
"type": "string",
|
|
1550
|
+
"enum": ["tdd", "tests-after", "none"],
|
|
1551
|
+
"description": "How Phase 3 authors tests in this project: tdd (default; failing test first), tests-after (implementation first, tests authored at the end), none (no unit/UI test authoring; existing tests are kept and run). Asked once by Phase 0 when absent; autopilot defaults to tdd and notes it."
|
|
1552
|
+
},
|
|
1548
1553
|
"defaultReviewers": {
|
|
1549
1554
|
"type": "array",
|
|
1550
1555
|
"items": {
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"warn_tokens": 5600
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
-
"total_max_tokens":
|
|
39
|
+
"total_max_tokens": 53350,
|
|
40
40
|
"note": "Token estimate = ceil(chars / 4). Per-phase budget rule: warn = current+10% (rounded to nearest 50), max = current+25%. Gives ~6 edit cycles of headroom before warn trips - intentionally quiet under normal maintenance, loud when a phase grows unusually. Only the active phase is loaded (lazy). Recalibrated at v10.0.0 after the validator/consistency/simplifier/lesson gate contracts landed in phases 1-4. Recalibrated again at v10.9.0 after the verify-by-test (Phase 4 Step 3.7), update-check (Phase 0 Step 0.6), immutable-test (Phase 3 GREEN) and redTests re-entry contracts landed - Step 3.7 prose was compressed to a pointer into refs/features/verify-by-test.md before the recalibration. Total bumped 50000 -> 51000 at v12.5.0 after the worktree residue/traversal-prune contract (Phase 0 + Phase 5 heal) and the Reflexion causal-diagnosis contract (Phase 4 lesson memory) landed; the prose was compressed first (161 tokens reclaimed) and every per-phase max still passes - only the aggregate needed room. Recalibrated again at v13.6.0 after the install-relative path correction: an instruction that names `pipeline/scripts/x` resolves only from a repo checkout, and a run happens in the user's worktree, so 157 references across these docs moved to `$HOME/.claude/...` at +5 bytes each - 196 tokens of pure correctness cost. Same discipline as before: prose was compressed FIRST (149 tokens reclaimed, by pointing Phase 1's Figma tier table at the Phase 0 probe that already resolved it and Phase 4's Codex constraints at the always-loaded AGENTS.md block), and only then were the budgets moved. Five warn lines had been permanently amber, which makes the amber tier useless as a signal, so every warn was reset to the documented current+10% and the four maxes that the new warn would have collided with were reset to current+25%. Aggregate 51000 -> 51500. Total bumped 51500 -> 52200 at v14.0.0 after Phase 4 Review entered the four --dev mode phase sets and the criteria-resolution contract (Step 1.78) landed. Same discipline as every prior bump: prose was compressed FIRST, 820 tokens reclaimed, before the number moved. Two of those compressions are structural rather than cosmetic - the hardcoded SwiftUI interaction list in Step 1.5 and the SwiftUI convention paragraph in Step 2.8 were transcriptions of rules that now live in a scoped registry, so keeping them here would have re-created the drift this release exists to remove, and the third moved the Step 1.78 full contract into refs/features/skill-conformance.md leaving a pointer. What remains is contract text that cannot be inferred: the manifest's four consumer-visible parts, the conformance checklist the reviewers must return, and the fail-closed semantics. Every per-phase max still passes (phase-4 12405/14750); only the aggregate needed room. Total bumped 52200 -> 52700 at v14.1.0 after two more contracts landed: stack skill routing (Phase 3 pre-flight step 9) and worktree finalize (Phase 6 step 9). Compression came first, as always, and twice: 224 tokens out of Phase 3 by pointing its criteria-ledger and routing steps at their feature files instead of restating them, and 190 out of Phase 6 by moving the finalize contract into refs/features/worktree-finalize.md and leaving the invocation plus the exit-3 semantics. Both new contracts follow the pattern the earlier ones set: the phase doc carries the call and the decision, the feature file carries the reasoning, and the feature files are outside this budget because it loops only the eight phase-N-* keys. Every per-phase max still passes (phase-3 7677/8950, phase-6 5223/6150 and both under warn); only the aggregate needed room. Total bumped 52700 -> 52750 for the Phase 0 Step 3 branch-persistence correction: the step wrote the legacy `projects[].branches` while the TTL filter two sections below read `global.recentBranches`, and both spots named a `{name, lastUsed}` shape the schema rejects (`branch` required, `additionalProperties: false`), so the recent-branch picker option could never populate and a literal implementation would have failed prefs validation. Naming the right target, the right key and the legacy field to avoid costs 41 tokens over the one line it replaces. Compression came first and was applied three times to the replacement text itself, from 120 tokens down to 66, by moving the rationale out of the phase doc entirely: the reasoning now lives where it is enforced, in the migrate-prefs carry-forward comment and the smoke-pref-migration f7 block, leaving the phase doc with only the instruction. 50 was the smallest step that clears it; phase-0-init sits at 10893/12400, far under its own max, so this is purely an aggregate ceiling. v15.0.0: total 52750 -> 53100, the stack-skill tables in phase-1/2/4 now carry plugin-namespaced names (ai-<stack>-toolkit:<skill>) - functional prefixes, ~170 tokens."
|
|
41
41
|
}
|
|
@@ -28,7 +28,7 @@ If user gives wrong format for the project, warn:
|
|
|
28
28
|
|
|
29
29
|
## Input Parsing
|
|
30
30
|
|
|
31
|
-
Classify user input into one of 5 types (matches the
|
|
31
|
+
Classify user input into one of 5 types (matches the `commands/multi-agent/SKILL.md` dispatcher contract; cross-CLI parity is enforced by `smoke-cross-cli-behavior.sh`):
|
|
32
32
|
|
|
33
33
|
| # | Pattern | Type | Action |
|
|
34
34
|
|---|---------|------|--------|
|
|
@@ -114,73 +114,7 @@ Procedure:
|
|
|
114
114
|
|
|
115
115
|
The pipeline's hands on devices and browsers are MCP tools served by a companion repo (`dev-toolkit-mcp`): Phase 5 test, `manual-test`, `design-check` and `apple-archive-compliance` all call them, and several pipeline skills declare a minimum toolkit version (see `cross-cli-contract.md`). That repo therefore has to track the MCP field, not just its own README. This step researches what current practice is and audits the toolkit against it.
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
1. `prefs.global.devToolkit` in `~/.claude/multi-agent-preferences.json`:
|
|
120
|
-
|
|
121
|
-
```jsonc
|
|
122
|
-
{
|
|
123
|
-
"enabled": true,
|
|
124
|
-
"label": "<human name>",
|
|
125
|
-
"localPath": "$HOME/<repo-dir>", // the companion repo working copy
|
|
126
|
-
"mcpServerName": "<registered MCP server name>",
|
|
127
|
-
"packageName": "@<scope>/<package>",
|
|
128
|
-
"registry": "github-packages", // github-packages | npmjs | none
|
|
129
|
-
"repoUrl": "https://github.com/<owner>/<repo>"
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
2. If unset, auto-detect from the MCP registration: read `mcpServers` in `~/.claude.json` (including each `projects[*].mcpServers`) and in `~/.claude/settings.json`; for a stdio entry whose command is `node`, take `dirname(args[0])`. Accept it only when that directory is a git repo whose `package.json` depends on `@modelcontextprotocol/sdk`.
|
|
134
|
-
3. If neither resolves, skip this step and report "no dev-toolkit configured". Never guess a path, never clone.
|
|
135
|
-
4. `enabled: false` skips the step.
|
|
136
|
-
|
|
137
|
-
**Research axes** - a finding without a source link is not a finding:
|
|
138
|
-
|
|
139
|
-
| # | Axis | Where to look | What to extract |
|
|
140
|
-
|---|------|---------------|-----------------|
|
|
141
|
-
| 1 | MCP protocol | spec revisions + `@modelcontextprotocol/sdk` releases | protocol features released since the pinned SDK range that the server does not use yet: tool annotations (`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`), `outputSchema` + structured content, resource links in results, progress + cancellation, `tools/list_changed`, pagination, elicitation |
|
|
142
|
-
| 2 | Host clients | Claude Code / Copilot CLI / Cursor / Antigravity docs + release notes | per-tool description budget, tool-count ceilings, naming conventions, image and output size limits, permission / allowlist ergonomics |
|
|
143
|
-
| 3 | Peer servers | GitHub search on the same domain terms + `stars:>50` | tool surfaces we lack, conventions peers converged on, and what to discard as out of scope |
|
|
144
|
-
| 4 | Wrapped tooling | `xcrun simctl help`, `idb`, `adb`, `xcodebuild`, Playwright release notes, Apple ITMS + App Store Review Guidelines | deprecated flags still in use, new capabilities worth a tool, audit rules that changed |
|
|
145
|
-
| 5 | Field practice | X / Twitter, Reddit, MCP community threads | what server authors actually changed recently (transport choice, output-token diets, sandboxing, error shape) |
|
|
146
|
-
|
|
147
|
-
**Audit the toolkit against the findings** - run the checks, do not assume:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
DT="<resolved localPath>"
|
|
151
|
-
node --check "$DT/index.js"
|
|
152
|
-
find "$DT/tools" -name "*.js" -type f -exec node --check {} \;
|
|
153
|
-
|
|
154
|
-
# stdout carries the JSON-RPC frames: a stray stdout write corrupts the stream
|
|
155
|
-
grep -rn "console\.log(" "$DT/index.js" "$DT/tools" || echo "stdout clean"
|
|
156
|
-
|
|
157
|
-
# advertised tool counts vs reality (README header + package.json description)
|
|
158
|
-
grep -nE "[0-9]+ tools" "$DT/README.md" "$DT/package.json"
|
|
159
|
-
|
|
160
|
-
# packaging: every runtime directory must be inside files[]
|
|
161
|
-
node -p "require('$DT/package.json').files.join('\n')"
|
|
162
|
-
ls -d "$DT"/tools/*/
|
|
163
|
-
|
|
164
|
-
cd "$DT" && npm outdated; npm audit --omit=dev 2>/dev/null | tail -20
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Also check: every tool carries a description and an `inputSchema`; token-heavy results (screenshots, UI trees, logs) are truncated or written to a file path instead of inlined; failures return an error result with an actionable message instead of throwing; `engines.node` matches what the SDK needs; `CHANGELOG.md`, a CI workflow and a test harness exist.
|
|
168
|
-
|
|
169
|
-
Output (plan band E):
|
|
170
|
-
|
|
171
|
-
```
|
|
172
|
-
| # | Axis | Finding | Source | Adaptation in the toolkit (file) | Effort | Impact | In plan? |
|
|
173
|
-
|---|------|---------|--------|----------------------------------|--------|--------|----------|
|
|
174
|
-
| 1 | Protocol | read-only tools carry no annotations | <spec link> | add `annotations` to the read-only tools in index.js | Low | Medium | Yes (P1) |
|
|
175
|
-
| 2 | Wrapped tooling | uses a simctl flag removed in Xcode <v> | <release notes> | switch tools/<family>/<file>.js to <new flag> | Low | High | Yes (P0) |
|
|
176
|
-
| 3 | Peer servers | peer exposes <surface> | <repo link> | does not fit: outside the pipeline's phases | - | - | No |
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Rules for this band:
|
|
180
|
-
|
|
181
|
-
- Band-E work lands in the toolkit repo, never mirrored into this one. Shipping it is `/multi-agent:sync` Step 3d.
|
|
182
|
-
- A finding that changes the tool surface (new / renamed / removed tool) pairs with a pipeline-side item: bump the minimum toolkit version wherever a pipeline skill declares one.
|
|
183
|
-
- If the current working directory IS the toolkit repo, skip band E and let bands A/B/C cover it - never report the same finding twice.
|
|
117
|
+
Full procedure - resolution (configuration first, never a hardcoded path; skip when nothing resolves or `enabled` is false), the 5 research axes, the audit command block, and the band-E output table + rules - lives in `$HOME/.claude/multi-agent-refs/refactor/dev-toolkit-research.md`. Read it before running this step.
|
|
184
118
|
|
|
185
119
|
## Step 0d: RUN-ERRORS - what the pipeline actually failed on
|
|
186
120
|
|
|
@@ -18,7 +18,7 @@ When invoked, it synchronizes all targets in order. It detects what changed, upd
|
|
|
18
18
|
|
|
19
19
|
| # | Target | Path | Direction |
|
|
20
20
|
|---|-------|-----|-----|
|
|
21
|
-
| 1 | Claude Code (source of truth) | `~/.claude/commands/multi-agent
|
|
21
|
+
| 1 | Claude Code (source of truth) | `~/.claude/commands/multi-agent/` + `~/.claude/multi-agent-refs/` + `~/.claude/agents/` + `~/.claude/scripts/` + `~/.claude/lib/` | source |
|
|
22
22
|
| 2 | Copilot CLI | `~/.copilot/copilot-instructions.md` + `~/.copilot/skills/` | <- from Claude |
|
|
23
23
|
| 2b | Codex CLI | `~/.codex/AGENTS.md` + `~/.codex/skills/multi-agent/` + `~/.codex/multi-agent-refs/` + `~/.codex/agents/*.toml` | <- from Claude (path-rewritten) |
|
|
24
24
|
| 3 | multi-agent-pipeline repo | `~/multi-agent-pipeline/pipeline/` | <- from Claude (genericized) |
|
|
@@ -56,7 +56,7 @@ If nothing is stale -> report "All targets up to date" and stop.
|
|
|
56
56
|
|
|
57
57
|
1. **Read first** - Understand the current state of all targets before making changes
|
|
58
58
|
2. **Preserve file structure** - Each target has its own format:
|
|
59
|
-
- `multi-agent
|
|
59
|
+
- `commands/multi-agent/` SKILL tree + `multi-agent-refs/phases/`: detailed pipeline spec (frontmatter, phases, routing)
|
|
60
60
|
- `copilot-instructions.md`: general development instructions + pipeline summary section
|
|
61
61
|
- `multi-agent-pipeline/pipeline/`: generic open-source version (NO personal data)
|
|
62
62
|
3. **Sync shared sections** (Claude <-> Copilot):
|
|
@@ -71,7 +71,7 @@ If nothing is stale -> report "All targets up to date" and stop.
|
|
|
71
71
|
|
|
72
72
|
## Pipeline Repo Sync Rules (CRITICAL)
|
|
73
73
|
|
|
74
|
-
1. **Source**: `~/.claude/commands/multi-agent
|
|
74
|
+
1. **Source**: `~/.claude/commands/multi-agent/` + `~/.claude/multi-agent-refs/` + `~/.claude/agents/` + `~/.claude/lib/`
|
|
75
75
|
2. **Target**: `~/multi-agent-pipeline/pipeline/`
|
|
76
76
|
3. **Genericization** (must be applied BEFORE writing to the target):
|
|
77
77
|
- Jira project key (`prefs.global.defaultJiraKey`) -> `{JIRA_KEY}`, `PROJ` in examples
|
|
@@ -82,8 +82,8 @@ If nothing is stale -> report "All targets up to date" and stop.
|
|
|
82
82
|
- **Verify**: scan for personal/corporate data inside pipeline/ with `grep` -> 0 results
|
|
83
83
|
4. **File mapping**:
|
|
84
84
|
```
|
|
85
|
-
~/.claude/commands/multi-agent.md -> pipeline/commands/multi-agent.md
|
|
86
85
|
~/.claude/commands/multi-agent/ -> pipeline/commands/multi-agent/
|
|
86
|
+
~/.claude/multi-agent-refs/ -> pipeline/multi-agent-refs/ (refs + pickers, non-command)
|
|
87
87
|
~/.claude/commands/sim-test.md -> pipeline/commands/sim-test.md
|
|
88
88
|
~/.claude/agents/*.md -> pipeline/agents/
|
|
89
89
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
version: 1.
|
|
1
|
+
version: 1.2.0
|
|
2
2
|
updated: 2026-07-29
|
|
3
3
|
owner: iOS platform
|
|
4
4
|
description: >
|
|
@@ -454,9 +454,11 @@ rules:
|
|
|
454
454
|
enforcement: lint
|
|
455
455
|
rationale: readability
|
|
456
456
|
check: >
|
|
457
|
-
Scene, ViewModel,
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
Scene, ViewModel, AnalyticsTracking, UseCase, Repository (+protocol +mock), Mapper +
|
|
458
|
+
models - each present when its responsibility exists. In a converted module the screen's
|
|
459
|
+
Output enum lives contract-side, not here, and a module-local CoordinatorEvent or
|
|
460
|
+
LocalizedText aggregator is pre-conversion residue reported as debt. Report a missing file
|
|
461
|
+
whose responsibility leaked elsewhere AND a ceremonial empty file.
|
|
460
462
|
|
|
461
463
|
- id: STRUCT-03
|
|
462
464
|
title: Every screen sits at the same depth with the same internal grouping
|
|
@@ -509,11 +511,12 @@ rules:
|
|
|
509
511
|
not a defect.
|
|
510
512
|
check: >
|
|
511
513
|
What this rule protects is that a reader finds a screen's exits under ONE name across the
|
|
512
|
-
module
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
514
|
+
module. The settled shape is an `Output` enum consumed through a plain closure, with the
|
|
515
|
+
enum living in the flow-contracts target. A `CoordinatorEvent` enum with a handler alias
|
|
516
|
+
is the pre-conversion spelling: legitimate only in a module that has not converted yet, a
|
|
517
|
+
debt finding in one that has, and never a choice for new code. The hard finding is a module
|
|
518
|
+
that uses both, or a screen whose exits are spread across an enum and loose ad-hoc
|
|
519
|
+
callbacks. Report the spelling per screen and the count of each.
|
|
517
520
|
|
|
518
521
|
- id: NAME-02
|
|
519
522
|
title: Our models use RequestModel/ResponseModel; transport suffixes stop at the data layer
|
|
@@ -1815,3 +1818,70 @@ rules:
|
|
|
1815
1818
|
accept_from_vocabulary: IndexLabel
|
|
1816
1819
|
rationale: readability
|
|
1817
1820
|
check: One spelling for "at this position" across the module; two make the call sites read as two concepts.
|
|
1821
|
+
|
|
1822
|
+
# ── UNIT - the settled screen dialect ────────────────────────────────────
|
|
1823
|
+
- id: UNIT-01
|
|
1824
|
+
title: A new screen's view model is the unit type, never the deprecated scene base
|
|
1825
|
+
severity: blocking
|
|
1826
|
+
enforcement: scan
|
|
1827
|
+
mechanism: >
|
|
1828
|
+
grep the diff's ADDED files for a subclass of the deprecated pre-unit scene base; grep the
|
|
1829
|
+
declaration of the unit generic (ViewModel<) for the four type parameters.
|
|
1830
|
+
rationale: flexibility
|
|
1831
|
+
check: >
|
|
1832
|
+
The unit dialect - ViewModel<State, ViewAction, Action, Output> with explicit conformer
|
|
1833
|
+
typealiases - is the only legal shape for new screen code. The pre-unit base class is
|
|
1834
|
+
deprecated at its declaration; existing screens convert on touch, and that migration is its
|
|
1835
|
+
own work. The finding is a NEW file building on the deprecated base, or a unit view model
|
|
1836
|
+
missing its typealias block (the all-compiler-version spelling).
|
|
1837
|
+
|
|
1838
|
+
- id: UNIT-02
|
|
1839
|
+
title: Unit state is written only inside the reducer
|
|
1840
|
+
severity: important
|
|
1841
|
+
enforcement: scan
|
|
1842
|
+
mechanism: 'custom regex: assignments to the unit state outside next(_:on:) in ViewModel files'
|
|
1843
|
+
rationale: readability
|
|
1844
|
+
check: >
|
|
1845
|
+
The unit's state moves in one place - the reducer - so a reader replays a screen's
|
|
1846
|
+
behaviour from a single function. A state write from an action handler, a task body or a
|
|
1847
|
+
view callback bypasses that replay and is the finding, wherever it compiles.
|
|
1848
|
+
|
|
1849
|
+
- id: UNIT-03
|
|
1850
|
+
title: Dependencies are observation-ignored, defaulted parameters
|
|
1851
|
+
severity: important
|
|
1852
|
+
enforcement: lint
|
|
1853
|
+
mechanism: 'custom regex: stored service/use-case/analytics properties in view models'
|
|
1854
|
+
rationale: flexibility
|
|
1855
|
+
check: >
|
|
1856
|
+
Every dependency is an @ObservationIgnored stored property injected as an init parameter
|
|
1857
|
+
whose default resolves from the container. Tests then inject doubles with no container
|
|
1858
|
+
setup, and observation never tracks a service handle. A dependency resolved inline at the
|
|
1859
|
+
call site, or a stored property observation can see, is the finding.
|
|
1860
|
+
|
|
1861
|
+
- id: SAFE-03
|
|
1862
|
+
title: A field reset that must erase the value writes the value before clear()
|
|
1863
|
+
severity: important
|
|
1864
|
+
enforcement: scan
|
|
1865
|
+
mechanism: 'custom regex: .clear() on a form field with no value write in the surrounding statement group'
|
|
1866
|
+
rationale: flexibility
|
|
1867
|
+
check: >
|
|
1868
|
+
The shared form field's clear() resets STATE only - the value survives it. Every reset
|
|
1869
|
+
path that must also erase what was typed (switching an entry method, finishing a secure
|
|
1870
|
+
flow, leaving a screen that held credentials) writes value = "" (a picker: deselect())
|
|
1871
|
+
first, then clear(), in that order - the value write may fire a validation policy and the
|
|
1872
|
+
trailing clear() leaves the field pristine. A bare clear() on such a path has shipped
|
|
1873
|
+
stale identifiers and surviving passwords before; treat it as the finding unless the reset
|
|
1874
|
+
demonstrably must keep the value.
|
|
1875
|
+
|
|
1876
|
+
- id: MOD-08
|
|
1877
|
+
title: Screens bind the shared formatters and rule facades; a hand-rolled one is a finding
|
|
1878
|
+
severity: important
|
|
1879
|
+
enforcement: scan
|
|
1880
|
+
mechanism: 'custom regex: DateFormatter(/NumberFormatter( construction and digit/dial-code string surgery inside Screens/**'
|
|
1881
|
+
rationale: flexibility
|
|
1882
|
+
check: >
|
|
1883
|
+
Dates, money, grouped amounts, dial-code spellings and validation bounds each have one
|
|
1884
|
+
shared home - the core formatter families and the module's form-rules facade. A screen
|
|
1885
|
+
that constructs its own formatter or re-derives a bound duplicates a wire contract that
|
|
1886
|
+
already has an owner, and the copies drift apart silently. The finding is the construction
|
|
1887
|
+
site; the fix is binding, not re-deriving.
|
|
@@ -73,3 +73,24 @@ surface has not chosen a different dialect, it is missing the surface, so that s
|
|
|
73
73
|
|
|
74
74
|
The test when you are tempted to add one: can you write the second value's paragraph without it
|
|
75
75
|
reading as an excuse? If not, it is a rule.
|
|
76
|
+
|
|
77
|
+
## Multi-target packages
|
|
78
|
+
|
|
79
|
+
A domain that outgrows one target splits into one target per flow inside a single package, and
|
|
80
|
+
the tree rules then apply PER TARGET. What the shape adds, and what the overlay binds for it:
|
|
81
|
+
|
|
82
|
+
- `screen.root` binds with a leading `Sources/*/` so every target's `Screens/` is walked; a
|
|
83
|
+
target with no screens simply matches nothing.
|
|
84
|
+
- Each screen-bearing target carries its own dependency configurator - registration, validation
|
|
85
|
+
pins, and the store entry the composition root aggregates - bound as `target.configurator`
|
|
86
|
+
and proven by STRUCT-21. Registration lives with the target, never pooled in a sibling.
|
|
87
|
+
- The flow-contracts side mirrors the split: one contracts sub-folder per target, named
|
|
88
|
+
`<Module><Target>FlowContracts`-style, under the module's contracts target
|
|
89
|
+
(`contracts.root` + STRUCT-18 prove each screen's pair).
|
|
90
|
+
- A scaffold target - empty configurator, a route enum with an empty destination switch, no
|
|
91
|
+
screens yet - is a legitimate recorded state, not a finding: it reserves the flow's seams so
|
|
92
|
+
the first screen lands as a fill-in, not a redesign.
|
|
93
|
+
- Tests mirror per target (`Tests/<Target>Tests/`), so the source/test mirror is a set of
|
|
94
|
+
prefix pairs; a single-swap `source.root`/`test.root` binding cannot express it and stays
|
|
95
|
+
unbound until the checker learns per-target templates.
|
|
96
|
+
|
|
@@ -79,6 +79,13 @@ dialect:
|
|
|
79
79
|
# Leave a role empty when the module genuinely has no such thing - the rules that read it are then
|
|
80
80
|
# reported as lost coverage, which is the honest answer.
|
|
81
81
|
roles:
|
|
82
|
+
# Unit-dialect vocabulary (bind when UnitDialect is adopted):
|
|
83
|
+
# screen.viewstate: "Presentation/Models/*ViewState.swift"
|
|
84
|
+
# screen.viewaction: "Presentation/Models/*ViewAction.swift"
|
|
85
|
+
# screen.viewmodelaction: "Presentation/Models/*ViewModelAction.swift"
|
|
86
|
+
# screen.viewmodelstate: "Presentation/Models/*ViewModelState.swift"
|
|
87
|
+
# Multi-target packages (STRUCT-21):
|
|
88
|
+
# target.configurator: "Sources/*/Configuration/*DependencyConfigurator.swift"
|
|
82
89
|
screen.root: # e.g. "Sources/*/Screens/*"
|
|
83
90
|
screen.entry:
|
|
84
91
|
screen.viewmodel:
|
|
@@ -110,6 +117,20 @@ vocabulary:
|
|
|
110
117
|
BareVerbs: [] # verbs a presentation type may not use alone
|
|
111
118
|
ForbiddenTypeSuffixes: [] # last words that describe no role
|
|
112
119
|
|
|
120
|
+
# UnitDialect: adopted | pre-unit
|
|
121
|
+
# Count view models subclassing the unit generic vs the deprecated pre-unit base.
|
|
122
|
+
# pre-unit is a RECORDED migration state, reported as debt - a new module binds adopted.
|
|
123
|
+
|
|
124
|
+
# --- Contracts ---------------------------------------------------------
|
|
125
|
+
# Only for a CONVERTED module whose screens are reached through a flow-contracts target.
|
|
126
|
+
# root is relative to the module root; leave the whole block out for a pre-conversion module
|
|
127
|
+
# and STRUCT-18 reports lost coverage instead of guessing. Bind the two contract roles under
|
|
128
|
+
# roles: with {screen}-substituted patterns, e.g.
|
|
129
|
+
# contracts.output: "{screen}Output.swift"
|
|
130
|
+
# contracts.providing: "{screen}ScreenProviding.swift"
|
|
131
|
+
contracts: {}
|
|
132
|
+
# root: ../../CrossDomains/Contracts/Sources/<Module>FlowContracts
|
|
133
|
+
|
|
113
134
|
# --- Limits ------------------------------------------------------------
|
|
114
135
|
limits:
|
|
115
136
|
source_target:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
version: 0.
|
|
2
|
-
updated: 2026-08-
|
|
1
|
+
version: 0.3.0
|
|
2
|
+
updated: 2026-08-18
|
|
3
3
|
owner: iOS platform
|
|
4
4
|
description: >
|
|
5
5
|
The ios-module-structure rule registry. Where a declaration lives, what its file is called, and what
|
|
@@ -61,10 +61,30 @@ roles:
|
|
|
61
61
|
description: the type holding the screen's behaviour and service calls.
|
|
62
62
|
screen.state:
|
|
63
63
|
description: the type holding the screen's UI state, when the module separates it from the view model.
|
|
64
|
+
screen.viewstate:
|
|
65
|
+
description: the unit dialect's view-facing state value, one declaration per file.
|
|
66
|
+
screen.viewaction:
|
|
67
|
+
description: the unit dialect's view-action vocabulary, one declaration per file.
|
|
68
|
+
screen.viewmodelaction:
|
|
69
|
+
description: the unit dialect's own-action vocabulary, one declaration per file.
|
|
70
|
+
screen.viewmodelstate:
|
|
71
|
+
description: the unit dialect's reducer-owned state, one declaration per file.
|
|
72
|
+
target.configurator:
|
|
73
|
+
description: >
|
|
74
|
+
a screen-bearing target's dependency configurator - the registration + validation home a
|
|
75
|
+
multi-target package carries once per target.
|
|
64
76
|
screen.analytics:
|
|
65
77
|
description: the screen's analytics surface.
|
|
66
78
|
screen.factory:
|
|
67
79
|
description: the seam another module calls to build the screen.
|
|
80
|
+
contracts.output:
|
|
81
|
+
description: >
|
|
82
|
+
the screen's Output enum in the module's flow-contracts target - every navigation exit,
|
|
83
|
+
nothing else. Lives OUTSIDE the module tree; the overlay's contracts.root names where.
|
|
84
|
+
contracts.providing:
|
|
85
|
+
description: >
|
|
86
|
+
the screen's vending seam (ScreenProviding protocol) in the flow-contracts target,
|
|
87
|
+
beside its Output. Lives OUTSIDE the module tree; the overlay's contracts.root names where.
|
|
68
88
|
screen.mapper:
|
|
69
89
|
description: the wire-to-domain translation for the screen.
|
|
70
90
|
service.dir:
|
|
@@ -128,7 +148,8 @@ module_overlay_slots:
|
|
|
128
148
|
governs: [STRUCT-05]
|
|
129
149
|
values:
|
|
130
150
|
per-screen-factory: each screen ships its own construction seam.
|
|
131
|
-
shared-factory: one factory per module builds every screen
|
|
151
|
+
shared-factory: one factory per module builds every screen - pre-conversion residue;
|
|
152
|
+
a converted module binds per-screen-factory, and a NEW module may not choose this.
|
|
132
153
|
note: >
|
|
133
154
|
Both put construction behind a seam, which is the property STRUCT-05 protects. Counting
|
|
134
155
|
factories against screens tells you which one the module chose.
|
|
@@ -136,7 +157,8 @@ module_overlay_slots:
|
|
|
136
157
|
governs: [STRUCT-06]
|
|
137
158
|
values:
|
|
138
159
|
separate-state-type: UI state lives in its own type beside the view model.
|
|
139
|
-
view-model-owned: the view model holds UI state directly
|
|
160
|
+
view-model-owned: the view model holds UI state directly - pre-conversion residue for
|
|
161
|
+
form screens; the settled shape is the separate state type.
|
|
140
162
|
note: >
|
|
141
163
|
Unbound, STRUCT-06 is disabled - "the view model holds a form field" is only a finding in a
|
|
142
164
|
module that decided it should not.
|
|
@@ -177,6 +199,16 @@ module_overlay_slots:
|
|
|
177
199
|
copy-layer: a per-screen type owns every string the screen shows.
|
|
178
200
|
note: Both keep copy findable; mixing them is what costs.
|
|
179
201
|
|
|
202
|
+
- id: UnitDialect
|
|
203
|
+
governs: [STRUCT-19, STRUCT-20]
|
|
204
|
+
question: which view-model generation do this module's screens ride?
|
|
205
|
+
values:
|
|
206
|
+
adopted: screens ride the unit view model (the State/ViewAction/Action/Output generic)
|
|
207
|
+
with the one-declaration-per-file vocabulary under the presentation models folder.
|
|
208
|
+
pre-unit: screens still ride the deprecated pre-unit scene base. Legitimate only as a
|
|
209
|
+
recorded migration state - the run reports it as debt, and a NEW module may not
|
|
210
|
+
choose it.
|
|
211
|
+
|
|
180
212
|
# ---------------------------------------------------------------------------
|
|
181
213
|
# Rules
|
|
182
214
|
# ---------------------------------------------------------------------------
|
|
@@ -349,8 +381,11 @@ rules:
|
|
|
349
381
|
check: >
|
|
350
382
|
The entry file holds its properties, its init, one body and its previews. A second view
|
|
351
383
|
member on it is a fragment that either belongs in the body's composition or belongs beside
|
|
352
|
-
the screen as an extracted view.
|
|
353
|
-
|
|
384
|
+
the screen as an extracted view. Moving those members into a view-composition extension
|
|
385
|
+
file (a "+ViewBuilder" / "+Subviews" companion) is the same finding relocated, not a fix -
|
|
386
|
+
that pattern is retired; the extracted subview pair is its replacement. The exception is a
|
|
387
|
+
fragment the type system pins to the call site - an alert's content, a modifier-constrained
|
|
388
|
+
builder - which cannot be moved.
|
|
354
389
|
|
|
355
390
|
- id: STRUCT-11
|
|
356
391
|
title: A cross-screen folder holds no type named after one screen
|
|
@@ -557,3 +592,82 @@ rules:
|
|
|
557
592
|
A scripted implementation that also carries its own payload literals mixes the decision of
|
|
558
593
|
which scenario to answer with the content of the answer. The tool can surface the size ratio;
|
|
559
594
|
the split itself is a judgement.
|
|
595
|
+
|
|
596
|
+
- id: STRUCT-18
|
|
597
|
+
title: A converted screen ships its contract pair in the flow-contracts target
|
|
598
|
+
severity: important
|
|
599
|
+
enforcement: scan
|
|
600
|
+
predicate: contract_pair_required
|
|
601
|
+
params:
|
|
602
|
+
output_role: contracts.output
|
|
603
|
+
providing_role: contracts.providing
|
|
604
|
+
applies_when: >
|
|
605
|
+
the overlay binds contracts.root (the path of the module's flow-contracts target, relative
|
|
606
|
+
to the module root) AND both contracts.output and contracts.providing. Unbound, DISABLED -
|
|
607
|
+
a pre-conversion module has no contract side, and the run reports the lost coverage instead
|
|
608
|
+
of guessing one.
|
|
609
|
+
rationale: module boundaries
|
|
610
|
+
check: >
|
|
611
|
+
A converted flow's screen is reachable only through its contract seam: an Output enum naming
|
|
612
|
+
every navigation exit and a providing protocol vending the built screen. Both live in the
|
|
613
|
+
flow-contracts target, not in the module, so the module tree alone cannot prove a screen is
|
|
614
|
+
actually wired. Each screen directory must have a matching directory under contracts.root
|
|
615
|
+
holding both files; a screen without one is invisible to every consumer of the seam.
|
|
616
|
+
|
|
617
|
+
- id: STRUCT-19
|
|
618
|
+
title: A unit screen carries the four-file vocabulary, one declaration per file
|
|
619
|
+
severity: important
|
|
620
|
+
enforcement: lint
|
|
621
|
+
predicate: file_required_in_dir
|
|
622
|
+
params:
|
|
623
|
+
slot: UnitDialect
|
|
624
|
+
slot_value: adopted
|
|
625
|
+
role: screen.viewmodelstate
|
|
626
|
+
companion_roles: [screen.viewstate, screen.viewaction, screen.viewmodelaction]
|
|
627
|
+
applies_when: >
|
|
628
|
+
the module binds UnitDialect to adopted AND the four unit vocabulary roles. Unbound or
|
|
629
|
+
pre-unit, DISABLED - and the pre-unit binding itself is reported as recorded debt.
|
|
630
|
+
rationale: readability
|
|
631
|
+
check: >
|
|
632
|
+
The unit dialect spells a screen's contract as four value files beside the presentation
|
|
633
|
+
models - view state, view action, own action, reducer state - each a single declaration in
|
|
634
|
+
a file of its own name. A vocabulary member folded into the view model file or sharing a
|
|
635
|
+
file with a sibling hides the screen's surface from the reader who navigates by name.
|
|
636
|
+
|
|
637
|
+
- id: STRUCT-20
|
|
638
|
+
title: An adopted module's view models ride the unit generic, never the deprecated base
|
|
639
|
+
severity: blocking
|
|
640
|
+
enforcement: scan
|
|
641
|
+
predicate: forbidden_pattern
|
|
642
|
+
params:
|
|
643
|
+
slot: UnitDialect
|
|
644
|
+
slot_value: adopted
|
|
645
|
+
subject_role: screen.viewmodel
|
|
646
|
+
pattern: ':\s*BaseViewModel\b'
|
|
647
|
+
applies_when: >
|
|
648
|
+
the module binds UnitDialect to adopted. In a pre-unit module this rule is DISABLED - the
|
|
649
|
+
migration converts screens on touch and drowning it in findings would hide the ones that
|
|
650
|
+
matter - but the binding is reported as debt every run.
|
|
651
|
+
rationale: flexibility
|
|
652
|
+
check: >
|
|
653
|
+
The pre-unit scene base is deprecated at its declaration. In a module that declared itself
|
|
654
|
+
adopted, any view model still subclassing it is a regression, not a leftover - the overlay
|
|
655
|
+
said this module is done.
|
|
656
|
+
|
|
657
|
+
- id: STRUCT-21
|
|
658
|
+
title: Every screen-bearing target carries its dependency configurator
|
|
659
|
+
severity: important
|
|
660
|
+
enforcement: lint
|
|
661
|
+
predicate: target_configurator_required
|
|
662
|
+
params:
|
|
663
|
+
role: target.configurator
|
|
664
|
+
applies_when: >
|
|
665
|
+
the overlay binds target.configurator. Unbound, DISABLED and reported - a single-target
|
|
666
|
+
module may register at its screens instead, and that is a dialect, not a defect.
|
|
667
|
+
rationale: module boundaries
|
|
668
|
+
check: >
|
|
669
|
+
In a multi-target package each target that ships screens owns one registration home: the
|
|
670
|
+
configurator that registers its repositories, use cases and screen seams, validates them,
|
|
671
|
+
and exposes the store entry the composition root aggregates. A screen-bearing target
|
|
672
|
+
without one leaves its seams registered from another target - reachable, but owned by
|
|
673
|
+
nobody the reader can find.
|
|
@@ -68,6 +68,7 @@ class Bindings:
|
|
|
68
68
|
|
|
69
69
|
def __init__(self, overlay, registry):
|
|
70
70
|
self.roles = dict(overlay.get("roles") or {})
|
|
71
|
+
self.contracts = dict(overlay.get("contracts") or {})
|
|
71
72
|
self.dialect = {k: v for k, v in (overlay.get("dialect") or {}).items()
|
|
72
73
|
if v and not k.endswith("_evidence")}
|
|
73
74
|
self.vocabulary = dict(overlay.get("vocabulary") or {})
|
|
@@ -223,10 +224,57 @@ def p_file_required_in_dir(screen, root, rule, params, b):
|
|
|
223
224
|
if not any(trigger_re.search(read(p)) for p in iter_glob(screen, trigger_pattern)):
|
|
224
225
|
return []
|
|
225
226
|
|
|
226
|
-
|
|
227
|
-
|
|
227
|
+
findings = []
|
|
228
|
+
for role_name in [params["role"]] + list(params.get("companion_roles") or []):
|
|
229
|
+
role_pattern = b.role(role_name)
|
|
230
|
+
if not role_pattern:
|
|
231
|
+
return []
|
|
232
|
+
wanted = expand(role_pattern, screen=screen.name)
|
|
233
|
+
if not iter_glob(screen, wanted):
|
|
234
|
+
findings.append({"path": str(screen), "line": 0, "detail": wanted})
|
|
235
|
+
return findings
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def module_target_configurator(root, rule, params, b, screens):
|
|
239
|
+
pattern = b.role(params["role"])
|
|
240
|
+
if not pattern:
|
|
241
|
+
return []
|
|
242
|
+
screen_units = {}
|
|
243
|
+
for screen in screens:
|
|
244
|
+
rel = screen.relative_to(root).parts
|
|
245
|
+
unit = rel[1] if rel[0] == "Sources" and len(rel) > 2 else rel[0]
|
|
246
|
+
screen_units.setdefault(unit, screen)
|
|
247
|
+
hits = iter_glob(root, pattern)
|
|
248
|
+
covered = set()
|
|
249
|
+
for hit in hits:
|
|
250
|
+
rel = hit.relative_to(root).parts
|
|
251
|
+
covered.add(rel[1] if rel[0] == "Sources" and len(rel) > 2 else rel[0])
|
|
252
|
+
return [{"path": str(screen_units[u]), "line": 0,
|
|
253
|
+
"detail": f"no configurator matched {pattern} for target {u}"}
|
|
254
|
+
for u in sorted(screen_units) if u not in covered]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def p_contract_pair_required(screen, root, rule, params, b):
|
|
258
|
+
croot = b.contracts.get("root")
|
|
259
|
+
if not croot:
|
|
228
260
|
return []
|
|
229
|
-
|
|
261
|
+
base = (root / croot).resolve()
|
|
262
|
+
if not base.is_dir():
|
|
263
|
+
return [{"path": str(screen), "line": 0,
|
|
264
|
+
"detail": f"contracts.root does not resolve: {croot}"}]
|
|
265
|
+
matches = [d for d in base.rglob(screen.name) if d.is_dir()]
|
|
266
|
+
if not matches:
|
|
267
|
+
return [{"path": str(screen), "line": 0,
|
|
268
|
+
"detail": f"no contract directory named {screen.name} under {croot}"}]
|
|
269
|
+
findings = []
|
|
270
|
+
for role_key in (params["output_role"], params["providing_role"]):
|
|
271
|
+
pattern = b.role(role_key)
|
|
272
|
+
if not pattern:
|
|
273
|
+
return []
|
|
274
|
+
wanted = expand(pattern, screen=screen.name)
|
|
275
|
+
if not any(iter_glob(d, wanted) for d in matches):
|
|
276
|
+
findings.append({"path": str(screen), "line": 0, "detail": wanted})
|
|
277
|
+
return findings
|
|
230
278
|
|
|
231
279
|
|
|
232
280
|
def p_prefix_collision(screen, root, rule, params, b):
|
|
@@ -524,6 +572,8 @@ PREDICATES = {
|
|
|
524
572
|
"prefix_collision": p_prefix_collision,
|
|
525
573
|
"dir_required_in_dir": p_dir_required_in_dir,
|
|
526
574
|
"file_required_in_dir": p_file_required_in_dir,
|
|
575
|
+
"contract_pair_required": p_contract_pair_required,
|
|
576
|
+
"target_configurator_required": p_none, # module-level; dispatched before the per-screen loop
|
|
527
577
|
"pair_required_in_dir": p_pair_required_in_dir,
|
|
528
578
|
"sibling_required": p_sibling_required,
|
|
529
579
|
"forbidden_pattern": p_forbidden_pattern,
|
|
@@ -553,10 +603,13 @@ def rule_status(rule, bindings):
|
|
|
553
603
|
wanted = params.get("slot_value")
|
|
554
604
|
if wanted and bound != wanted:
|
|
555
605
|
return False, f"slot {slot} is {bound}"
|
|
556
|
-
for key in ("role", "subject_role", "sibling_role", "container_role", "left_role", "right_role"
|
|
606
|
+
for key in ("role", "subject_role", "sibling_role", "container_role", "left_role", "right_role",
|
|
607
|
+
"output_role", "providing_role"):
|
|
557
608
|
name = params.get(key)
|
|
558
609
|
if name and not bindings.role(name):
|
|
559
610
|
return False, f"role {name} unbound"
|
|
611
|
+
if rule.get("predicate") == "contract_pair_required" and not bindings.contracts.get("root"):
|
|
612
|
+
return False, "contracts.root unbound"
|
|
560
613
|
if params.get("vocabulary_key") and not bindings.vocabulary.get(params["vocabulary_key"]):
|
|
561
614
|
return False, f"vocabulary {params['vocabulary_key']} unbound"
|
|
562
615
|
if params.get("accept_from_vocabulary") and not bindings.vocabulary.get(params["accept_from_vocabulary"]):
|
|
@@ -608,6 +661,15 @@ def run(root, registry, bindings, only_rule=None, only_screen=None):
|
|
|
608
661
|
})
|
|
609
662
|
continue
|
|
610
663
|
|
|
664
|
+
if rule.get("predicate") == "target_configurator_required":
|
|
665
|
+
for hit in module_target_configurator(root, rule, params, bindings, screens):
|
|
666
|
+
findings.append({
|
|
667
|
+
"rule_id": rule["id"], "severity": rule["severity"], "screen": "(targets)",
|
|
668
|
+
"path": str(Path(hit["path"]).relative_to(root)), "line": 0,
|
|
669
|
+
"message": rule["title"], "detail": hit["detail"],
|
|
670
|
+
})
|
|
671
|
+
continue
|
|
672
|
+
|
|
611
673
|
if rule.get("predicate") == "prefix_collision":
|
|
612
674
|
for hit in module_prefix_collision(root, rule, params, bindings, screens):
|
|
613
675
|
findings.append({
|