@mmerterden/multi-agent-pipeline 15.4.0 → 15.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,23 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [15.6.0] - 2026-08-18
20
+
21
+ ### Fixed
22
+ - **The heart goes platform-blind** (platform-dynamic audit, 15 findings): Phase 3's RED run, target resolution and build verification become `case "$STACK"` arms (gradle/pytest/npm beside xcodebuild, with the Gradle build-lock decision stated); Phase 4 Gate 1 matches its own stack-generic Gates 2-3; Phase 2 dispatches the platform's architect agent; Phase 5's device-check table gains the Android MCP tools; the figma-config schema stops claiming SwiftUI as universal; wiki scope gains a `platform` value.
23
+
24
+ ### Added
25
+ - **Per-stack routing hatches**: `STACK_ONLY` lists in `_stack-routing.mjs` so a stack-only skill (ktlint, hilt-di, ...) routes with one list entry instead of a regex widening - and `--check-routing` now FAILS on unrouted skills. `lint-skills` accepts `platform: backend|frontend`.
26
+
27
+ ## [15.5.0] - 2026-08-18
28
+
29
+ ### Added
30
+ - **`sharedUtilities` census bucket** - the bind-don't-rebuild inventory: formatter families, validation rule types + per-module facades and design-token namespaces living outside screen slices, counted with samples; Phase 3 treats a non-empty bucket as binding.
31
+
32
+ ### Fixed
33
+ - **Smoke runs never touch the live dashboard** - run-smokes exports `MULTI_AGENT_SMOKE=1` and phase-tracker's live ping returns under it (test gates were leaving phantom "running" rows on the timeline).
34
+ - **Routing resolves the multi-agent-plugins toolkit first** - the public toolkit family is the pipeline's standard companion; a corporate variant is the fallback, not the default.
35
+
19
36
  ## [15.4.0] - 2026-08-18
20
37
 
21
38
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "15.4.0",
3
+ "version": "15.6.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",
@@ -1026,6 +1026,43 @@ bucket_di_registration() {
1026
1026
  # =============================================================================
1027
1027
  # Assemble final JSON
1028
1028
  # =============================================================================
1029
+
1030
+ # =============================================================================
1031
+ # C13 - sharedUtilities: the bind-don't-rebuild inventory
1032
+ # =============================================================================
1033
+ # What shared machinery already exists OUTSIDE screen slices - formatter families,
1034
+ # validation rule types + per-module rule facades, design-token namespaces. A dev
1035
+ # phase that sees a non-empty bucket binds these instead of hand-rolling a
1036
+ # duplicate; the counts say how settled each family is.
1037
+ bucket_shared_utilities() {
1038
+ local ext screens_seg
1039
+ case "$PLATFORM" in
1040
+ ios) ext='*.swift' ; screens_seg='/Screens/' ;;
1041
+ android) ext='*.kt' ; screens_seg='/screens/' ;;
1042
+ *) empty_bucket; return ;;
1043
+ esac
1044
+ local files
1045
+ files=$(run_find -type f -name "$ext" 2>/dev/null | grep -v "$screens_seg" | grep -viE '/tests?/|/generated/' || true)
1046
+ if [ -z "$files" ]; then empty_bucket; return; fi
1047
+ local fmt rules facades tokens
1048
+ fmt=$(printf '%s
1049
+ ' "$files" | grep -cE '(Formatter|Format)\.(swift|kt)$' || true)
1050
+ rules=$(printf '%s
1051
+ ' "$files" | grep -cE '[A-Za-z]Rule\.(swift|kt)$' || true)
1052
+ facades=$(printf '%s
1053
+ ' "$files"| grep -cE '(FormRules|ValidationRules)\.(swift|kt)$' || true)
1054
+ tokens=$(printf '%s
1055
+ ' "$files" | grep -cE '(Tokens?|Spacing|Radius|Typography)[A-Za-z]*\.(swift|kt)$' || true)
1056
+ fmt=${fmt:-0}; rules=${rules:-0}; facades=${facades:-0}; tokens=${tokens:-0}
1057
+ local total=$((fmt + rules + facades + tokens))
1058
+ if [ "$total" -eq 0 ]; then empty_bucket; return; fi
1059
+ local sample
1060
+ sample=$(printf '%s
1061
+ ' "$files" | grep -E '(Formatter|Format|Rule|FormRules|ValidationRules|Tokens?|Spacing|Radius|Typography)[A-Za-z]*\.(swift|kt)$' | head -5 | sed "s|^$REPO_PATH/||")
1062
+ emit_bucket "formatters:$fmt rules:$rules facades:$facades tokens:$tokens" "$(printf '%s
1063
+ ' "$sample" | head -1)" "$(confidence_for "$total")" "$(files_to_json "$sample")" "[]"
1064
+ }
1065
+
1029
1066
  folderStructure=$(run_bucket_with_timeout bucket_folder_structure)
1030
1067
  stateHolderNaming=$(run_bucket_with_timeout bucket_state_holder)
1031
1068
  viewNaming=$(run_bucket_with_timeout bucket_view_naming)
@@ -1038,6 +1075,7 @@ testMethodNaming=$(run_bucket_with_timeout bucket_test_method_naming)
1038
1075
  accessibilityIdentifier=$(run_bucket_with_timeout bucket_accessibility_identifier)
1039
1076
  localizationKey=$(run_bucket_with_timeout bucket_localization_key)
1040
1077
  diRegistration=$(run_bucket_with_timeout bucket_di_registration)
1078
+ sharedUtilities=$(run_bucket_with_timeout bucket_shared_utilities)
1041
1079
 
1042
1080
  jq -n \
1043
1081
  --argjson folderStructure "$folderStructure" \
@@ -1052,6 +1090,7 @@ jq -n \
1052
1090
  --argjson accessibilityIdentifier "$accessibilityIdentifier" \
1053
1091
  --argjson localizationKey "$localizationKey" \
1054
1092
  --argjson diRegistration "$diRegistration" \
1093
+ --argjson sharedUtilities "$sharedUtilities" \
1055
1094
  '{
1056
1095
  folderStructure: $folderStructure,
1057
1096
  stateHolderNaming: $stateHolderNaming,
@@ -1064,7 +1103,8 @@ jq -n \
1064
1103
  testMethodNaming: $testMethodNaming,
1065
1104
  accessibilityIdentifier: $accessibilityIdentifier,
1066
1105
  localizationKey: $localizationKey,
1067
- diRegistration: $diRegistration
1106
+ diRegistration: $diRegistration,
1107
+ sharedUtilities: $sharedUtilities
1068
1108
  }'
1069
1109
 
1070
1110
  exit 0
@@ -52,7 +52,7 @@ a single atom is `component`. When it cannot be decided, ask - do not default
52
52
  omits the wiring.
53
53
 
54
54
  **Pre-implementation validation is not optional on iOS.** Before the create skill
55
- runs, dispatch `ai-ios-toolkit:figma-validate` for the frame. It checks
55
+ runs, dispatch `<toolkit>:figma-validate` (probed like every dual-name skill; a toolkit that ships none records the skip) for the frame. It checks
56
56
  registry presence, Code Connect strategy, **design token compliance**, dependency
57
57
  readiness, atomic scope and already-implemented status in about ten seconds. Those
58
58
  are precisely the checks whose absence produced guessed spacing and an unpublished
@@ -22,15 +22,15 @@ Platform comes from the same mapping component dispatch uses, so the two cannot
22
22
 
23
23
  | `state.platform` / detected stack | Toolkit candidates, first enabled wins |
24
24
  |---|---|
25
- | ios, swift | `ai-ios-engineering-toolkit`, then `ai-ios-toolkit` |
26
- | android, kotlin | `ai-android-engineering-toolkit`, then `ai-android-toolkit` |
25
+ | ios, swift | `ai-ios-toolkit`, then `ai-ios-engineering-toolkit` |
26
+ | android, kotlin | `ai-android-toolkit`, then `ai-android-engineering-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.
29
+ A row is an ORDERED candidate list, not one name: the multi-agent-plugins toolkit is the
30
+ pipeline's standard companion and resolves first; a corporate variant is the fallback for a
31
+ repo that enables only it. Probe the candidates in order against the enabled plugins and take
32
+ the first hit. When none is enabled, the recorded no-op names every candidate probed - an
33
+ enabled toolkit the table did not know about must show up as a probe miss, never as silence.
34
34
 
35
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.
36
36
 
@@ -26,7 +26,7 @@ Pre-flight steps (run in order, abort on failure).
26
26
 
27
27
  5. **Standards binding citations**: read `analysis Section 21 References` table. For each row with `Rol: bağlayıcı / binding`, persist the source path into `state.dev.standardsBindings[]`. Phase 3 Dev tasks MUST cite at least one binding source per architectural decision (pre-existing Locked decision 8).
28
28
 
29
- 6. **Conventions handoff**: read `analysis Section 13.1 Concept Table` (Pass B output with footnotes). Persist concept-to-realization mapping into `state.dev.conventions[<concept>]`. Phase 3 implementation uses these names verbatim (e.g., if Section 13.1 says "State holder: PassengerFlightViewModel", Phase 3 names the class exactly `PassengerFlightViewModel`).
29
+ 6. **Conventions handoff**: read `analysis Section 13.1 Concept Table` (Pass B output with footnotes). Persist concept-to-realization mapping into `state.dev.conventions[<concept>]`. Phase 3 implementation uses these names verbatim, and a non-empty `sharedUtilities` bucket is binding: bind those formatters/rule-facades/tokens, never hand-roll a duplicate (e.g., if Section 13.1 says "State holder: PassengerFlightViewModel", Phase 3 names the class exactly `PassengerFlightViewModel`).
30
30
 
31
31
  7. **MCP forbidden**: calling `mcp__claude_ai_Figma__*` in Phase 3 is a violation. `smoke-no-mcp-in-dev-phases.sh` reads `state.telemetry.mcpCalls[]` after the run and fails if Phase 3 contributed an entry.
32
32
 
@@ -122,16 +122,17 @@ For each task (respecting dependency order):
122
122
  - `describe` / `it` → Quick/Nimble
123
123
  - Test naming: `test{Scenario}_{Expected}` (e.g. `testKeychainReturnsNil_doesNotCrash`)
124
124
  - One test per behavior change - not one test per file
125
- - Run test to confirm RED:
125
+ - Run test to confirm RED - the command is the platform's, one arm per stack:
126
126
  ```bash
127
- acquire_build_lock "$TASK_ID"
128
- xcodebuild test \
129
- -scheme "{scheme}" \
130
- -destination "platform=iOS Simulator,name={simulator}" \
131
- -derivedDataPath "{worktreePath}/.DerivedData" \
132
- -only-testing:"{testTarget}/{testClass}/{testMethod}" \
133
- 2>&1 | tail -5
134
- release_build_lock
127
+ case "$STACK" in
128
+ ios) acquire_build_lock "$TASK_ID"
129
+ xcodebuild test -scheme "{scheme}" -destination "platform=iOS Simulator,name={simulator}" \
130
+ -derivedDataPath "{worktreePath}/.DerivedData" -only-testing:"{testTarget}/{testClass}/{testMethod}" 2>&1 | tail -5
131
+ release_build_lock ;;
132
+ android) ./gradlew test --tests "{testClass}.{testMethod}" 2>&1 | tail -5 ;;
133
+ backend) pytest "{test_file}::{test_name}" 2>&1 | tail -5 ;;
134
+ frontend) npm test -- --testPathPattern="{file}" 2>&1 | tail -5 ;;
135
+ esac
135
136
  ```
136
137
  - Must fail for the RIGHT reason (expected assertion, not compilation error)
137
138
 
@@ -154,49 +155,22 @@ For each task (respecting dependency order):
154
155
  - Only if duplication or naming is poor
155
156
  - Re-run tests after refactor → still GREEN
156
157
 
157
- **Scheme + destination resolution** (auto-detect once per project, cache in `agent-state.json`):
158
+ **Target resolution** (auto-detect once per project, cache in `agent-state.json`; ios resolves scheme + simulator, android resolves module + variant, backend/frontend need none):
158
159
  ```bash
159
- # List available schemes
160
- xcodebuild -list -json -project "{projectPath}" 2>/dev/null \
161
- || xcodebuild -list -json -workspace "{workspacePath}" 2>/dev/null
162
- # Pick: prefer scheme matching project name, then first non-test scheme
163
-
164
- # Available simulators
165
- xcrun simctl list devices available -j | jq '.devices | to_entries[] | select(.key | contains("iOS")) | .value[0].name'
160
+ case "$STACK" in
161
+ ios) xcodebuild -list -json -project "{projectPath}" 2>/dev/null || xcodebuild -list -json -workspace "{workspacePath}" 2>/dev/null
162
+ # prefer the scheme matching the project name, then the first non-test scheme
163
+ xcrun simctl list devices available -j | jq '.devices | to_entries[] | select(.key | contains("iOS")) | .value[0].name' ;;
164
+ android) ./gradlew projects 2>/dev/null | grep -E "^\+--- Project" ; ./gradlew tasks --all 2>/dev/null | grep -m5 "assemble.*Debug" ;;
165
+ esac
166
166
  ```
167
167
 
168
- **Non-Xcode projects:**
169
- - Python: `pytest {test_file}::{test_name}` → `pytest`
170
- - Node.js: `npm test -- --testPathPattern={file}` `npm test`
171
- - Android: `./gradlew test --tests "{testClass}.{testMethod}"` `./gradlew test`
172
-
173
- 4. **Build verification with queue lock** (see Build Queue section below):
174
-
175
- **Preferred - MCP tool (token-efficient, dev-toolkit-mcp ≥ 2.3.0):**
176
- ```
177
- acquire_build_lock "$TASK_ID"
178
- mcp__dev-toolkit__ios_xcodebuild({
179
- project: "{projectPath}", // or workspace
180
- scheme: "{scheme}",
181
- configuration: "Release",
182
- destination: "generic/platform=iOS",
183
- derived_data_path: "{worktreePath}/.DerivedData"
184
- })
185
- release_build_lock
186
- ```
187
- Returns one line: `Build: SUCCESS|FAILURE (E errors, W warnings) [xcresult-<id>]`. On failure, drill in only when needed via `mcp__dev-toolkit__ios_xcresult({id, mode: "errors"})`. Saves thousands of tokens per build cycle vs. raw xcodebuild output.
188
-
189
- **Fallback - raw xcodebuild** (when MCP server is unavailable):
190
- ```bash
191
- acquire_build_lock "$TASK_ID"
192
- xcodebuild build \
193
- -scheme "{scheme}" \
194
- -destination "generic/platform=iOS" \
195
- -derivedDataPath "{worktreePath}/.DerivedData" \
196
- 2>&1 | tail -5
197
- release_build_lock
198
- ```
199
- 5. If build fails → fix → rebuild (max 3 attempts, track `retryCount` in state). With MCP path: pass the captured xcresult ID to `ios_xcresult` mode `errors` first, then `warnings` only if needed - never dump full log into context.
168
+ 4. **Build verification** (per stack; ios/android under the build queue lock, see below):
169
+ - **ios, preferred (MCP, dev-toolkit ≥ 2.3.0)**: `acquire_build_lock` → `mcp__dev-toolkit__ios_xcodebuild({project|workspace, scheme, configuration: "Release", destination: "generic/platform=iOS", derived_data_path: "{worktreePath}/.DerivedData"})` → `release_build_lock`. Returns one line `Build: SUCCESS|FAILURE (E errors, W warnings) [xcresult-<id>]`; on failure drill in via `mcp__dev-toolkit__ios_xcresult({id, mode: "errors"})`, never dump the full log.
170
+ - **ios, fallback (raw)**: same lock pair around `xcodebuild build -scheme "{scheme}" -destination "generic/platform=iOS" -derivedDataPath "{worktreePath}/.DerivedData" 2>&1 | tail -5`.
171
+ - **android**: lock pair around `./gradlew assembleDebug 2>&1 | tail -5` (the Gradle daemon and `build/` outputs contend across parallel worktrees exactly as DerivedData does - the lock applies).
172
+ - **backend / frontend**: `python -m compileall .` / `npm run build --if-present 2>&1 | tail -5`; no lock.
173
+ 5. If build fails fix → rebuild (max 3 attempts, track `retryCount` in state).
200
174
  6. **Intermediate commit** (after each completed task in the plan):
201
175
  ```bash
202
176
  git -C "{worktreePath}" add -A
@@ -247,7 +221,7 @@ release_build_lock() { rm -rf "$BUILD_LOCK"; }
247
221
  - Phase 4 Step 1 Gate 1 (build gate before review)
248
222
  - Phase 4 Step 1 Gate 3 (test gate before review)
249
223
 
250
- **Non-Xcode projects** (Python, Node.js): No lock needed - these can build/test in parallel without conflicts.
224
+ **Android**: same lock discipline - the Gradle daemon and `build/` outputs contend across parallel worktrees. **Backend/frontend** (Python, Node.js): no lock needed - these build/test in parallel without conflicts.
251
225
 
252
226
  ---
253
227
 
@@ -10,8 +10,8 @@ Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md` -
10
10
  If any gate fails → fix first, don't waste AI tokens reviewing broken code.
11
11
 
12
12
  ```bash
13
- # Gate 1: Build (Xcode uses build queue lock - see Phase 3) - tee output to a log
14
- xcodebuild ... 2>&1 | tee "$WORKTREE/.build.log"
13
+ # Gate 1: Build (xcodebuild/gradle assemble/tsc/py compile - stack-dependent; Xcode uses the build queue lock, see Phase 3) - tee output to a log
14
+ <build-command> 2>&1 | tee "$WORKTREE/.build.log"
15
15
  # Gate 2: Lint (swiftlint/ktlint/ruff/eslint - stack-dependent)
16
16
  # Gate 3: Tests pass (xcodebuild test/gradle test/pytest/npm test) - tee output to a log
17
17
  <test-command> 2>&1 | tee "$WORKTREE/.test.log"
@@ -126,12 +126,12 @@ Before or during user testing, run device-level audits via Bash if user requests
126
126
 
127
127
  | Check | When | Command |
128
128
  | ------------------- | ----------------- | ----------------------------------------- |
129
- | Accessibility audit | UI changes | `mcp__dev-toolkit__ios_accessibility_audit` (or `swift ui-tree-dumper.swift`) |
130
- | Biometric test | Auth flow changes | `mcp__dev-toolkit__ios_biometric` (or `xcrun simctl keychain biometric-match`) |
131
- | Launch time | Android project | `adb shell am start -W` |
132
- | Visual test | Any UI changes | `/multi-agent test` (sim-test) |
133
- | Snapshot regression | Component / pixel-stable UI changes | `mcp__dev-toolkit__ios_visual_diff` (dev-toolkit-mcp 2.3.0) |
134
- | App Store screenshots | `taskType === screenshot` | `mcp__dev-toolkit__ios_status_bar({preset: "clean"})` before each capture |
129
+ | Accessibility audit | UI changes | `mcp__dev-toolkit__{ios,android}_accessibility_audit` |
130
+ | Biometric test | Auth flow changes | ios: `mcp__dev-toolkit__ios_biometric` (android: manual) |
131
+ | Launch time | Perf-sensitive changes | ios: app-launch instrument · android: `mcp__dev-toolkit__android_launch_time` |
132
+ | Visual test | Any UI changes | `/multi-agent test` (sim-test, both platforms) |
133
+ | Snapshot regression | Component / pixel-stable UI changes | ios: `mcp__dev-toolkit__ios_visual_diff` · android: `mcp__dev-toolkit__android_screenshot` + compare |
134
+ | Store screenshots | `taskType === screenshot` | ios: `ios_status_bar({preset: "clean"})` · android: `android_screenshot` |
135
135
 
136
136
  Results included in Phase 7 report. MCP tools preferred when available - concise structured output, lower token cost.
137
137
 
@@ -129,13 +129,13 @@ gh pr create --base {baseBranch} --head {branch} $REVIEWERS \
129
129
  --body-file /tmp/pr-body-$TASK_ID.md
130
130
  ```
131
131
 
132
- **UI Component PR (when SwiftUI component detected):**
132
+ **UI Component PR (when a UI component is detected (SwiftUI/Compose)):**
133
133
 
134
134
  Same `gh pr create` pattern with component-specific body sections:
135
135
 
136
136
  - Component Details: file table (Configuration, View, Modifiers, Tests)
137
137
  - Variants table, Design Tokens Used, Figma URL
138
- - Accessibility: labels, identifiers, 44pt tap target, Dynamic Type
138
+ - Accessibility: labels, identifiers, platform-minimum tap target (44pt iOS / 48dp Android), Dynamic Type
139
139
  - Test Coverage: structural/behavioral/snapshot counts
140
140
  - Checklist: no magic numbers, configuration purity, modifier chain, accessibility, dark mode + RTL, build passes
141
141
 
@@ -139,7 +139,7 @@ This is the single source of truth. When a contributor or model is unsure where
139
139
 
140
140
  ## Build Queue
141
141
 
142
- - All `xcodebuild` / `xcodebuild test` calls acquire `/tmp/claude-xcodebuild.lock` before running. Parallel Xcode builds corrupt DerivedData and simulator state.
142
+ - `xcodebuild` and `./gradlew` build/test calls acquire `/tmp/claude-xcodebuild.lock` first: parallel runs corrupt DerivedData/simulators and contend on the Gradle daemon.
143
143
  - Each worktree uses its own `-derivedDataPath "{worktreePath}/.DerivedData"` to prevent cross-contamination.
144
144
  - Lock auto-releases; stale locks (>15min) get force-cleaned.
145
145
  - Non-Xcode builds (Gradle, npm, Python) don't need the lock - they handle their own concurrency.
@@ -18,27 +18,61 @@
18
18
  "testMethodNaming",
19
19
  "accessibilityIdentifier",
20
20
  "localizationKey",
21
- "diRegistration"
21
+ "diRegistration",
22
+ "sharedUtilities"
22
23
  ],
23
24
  "properties": {
24
- "folderStructure": { "$ref": "#/$defs/conventionBucket" },
25
- "stateHolderNaming": { "$ref": "#/$defs/conventionBucket" },
26
- "viewNaming": { "$ref": "#/$defs/conventionBucket" },
27
- "navigatorNaming": { "$ref": "#/$defs/conventionBucket" },
28
- "useCaseNaming": { "$ref": "#/$defs/conventionBucket" },
29
- "repositoryNaming": { "$ref": "#/$defs/conventionBucket" },
30
- "dtoNaming": { "$ref": "#/$defs/conventionBucket" },
31
- "uiStateModel": { "$ref": "#/$defs/conventionBucket" },
32
- "testMethodNaming": { "$ref": "#/$defs/conventionBucket" },
33
- "accessibilityIdentifier": { "$ref": "#/$defs/conventionBucket" },
34
- "localizationKey": { "$ref": "#/$defs/conventionBucket" },
35
- "diRegistration": { "$ref": "#/$defs/conventionBucket" }
25
+ "folderStructure": {
26
+ "$ref": "#/$defs/conventionBucket"
27
+ },
28
+ "stateHolderNaming": {
29
+ "$ref": "#/$defs/conventionBucket"
30
+ },
31
+ "viewNaming": {
32
+ "$ref": "#/$defs/conventionBucket"
33
+ },
34
+ "navigatorNaming": {
35
+ "$ref": "#/$defs/conventionBucket"
36
+ },
37
+ "useCaseNaming": {
38
+ "$ref": "#/$defs/conventionBucket"
39
+ },
40
+ "repositoryNaming": {
41
+ "$ref": "#/$defs/conventionBucket"
42
+ },
43
+ "dtoNaming": {
44
+ "$ref": "#/$defs/conventionBucket"
45
+ },
46
+ "uiStateModel": {
47
+ "$ref": "#/$defs/conventionBucket"
48
+ },
49
+ "testMethodNaming": {
50
+ "$ref": "#/$defs/conventionBucket"
51
+ },
52
+ "accessibilityIdentifier": {
53
+ "$ref": "#/$defs/conventionBucket"
54
+ },
55
+ "localizationKey": {
56
+ "$ref": "#/$defs/conventionBucket"
57
+ },
58
+ "diRegistration": {
59
+ "$ref": "#/$defs/conventionBucket"
60
+ },
61
+ "sharedUtilities": {
62
+ "$ref": "#/$defs/conventionBucket"
63
+ }
36
64
  },
37
65
  "$defs": {
38
66
  "conventionBucket": {
39
67
  "type": "object",
40
68
  "additionalProperties": false,
41
- "required": ["pattern", "example", "confidence", "evidenceFiles", "alternativeCandidates"],
69
+ "required": [
70
+ "pattern",
71
+ "example",
72
+ "confidence",
73
+ "evidenceFiles",
74
+ "alternativeCandidates"
75
+ ],
42
76
  "properties": {
43
77
  "pattern": {
44
78
  "type": "string",
@@ -50,18 +84,27 @@
50
84
  },
51
85
  "confidence": {
52
86
  "type": "string",
53
- "enum": ["high", "medium", "low", "none"],
87
+ "enum": [
88
+ "high",
89
+ "medium",
90
+ "low",
91
+ "none"
92
+ ],
54
93
  "description": "high: 5+ examples; medium: 3-4; low: 1-2 or mixed; none: no evidence."
55
94
  },
56
95
  "evidenceFiles": {
57
96
  "type": "array",
58
- "items": { "type": "string" },
97
+ "items": {
98
+ "type": "string"
99
+ },
59
100
  "maxItems": 5,
60
101
  "description": "Repo-relative or absolute paths backing the detection (capped at 5)."
61
102
  },
62
103
  "alternativeCandidates": {
63
104
  "type": "array",
64
- "items": { "type": "string" },
105
+ "items": {
106
+ "type": "string"
107
+ },
65
108
  "description": "Competing patterns that were observed but lost the majority vote."
66
109
  }
67
110
  }
@@ -276,7 +276,7 @@
276
276
  "ui": {
277
277
  "type": "object",
278
278
  "additionalProperties": false,
279
- "description": "UI interaction systems. Consumed by the figma-navigation / figma-overlays / figma-bottom-sheets convention skills and by Phase 4 review. When a section is absent or its mode is 'native', the pipeline uses stock SwiftUI (NavigationStack, .alert/.sheet(item:), .sheet+presentationDetents). Set mode 'custom' to route to a project-supplied system by the type names below.",
279
+ "description": "UI interaction systems. Consumed by the figma-navigation / figma-overlays / figma-bottom-sheets convention skills and by Phase 4 review. When a section is absent or its mode is 'native', the pipeline uses the platform's stock system (SwiftUI NavigationStack/.sheet on iOS, Compose Navigation/dialogs on Android). Set mode 'custom' to route to a project-supplied system by the type names below.",
280
280
  "properties": {
281
281
  "navigationSystem": {
282
282
  "type": "object",
@@ -27,7 +27,11 @@ export const COMMON_SKILLS = [
27
27
  "localization-reuse-map",
28
28
  ];
29
29
 
30
- // Apple/Xcode-only skills that match no stack pattern iOS plugin only.
30
+ // Stack-only escape hatches: skill names that match no stack pattern route to
31
+ // exactly one plugin. Every stack gets one - a stack whose new skill matches
32
+ // nothing must be addable HERE (one list entry), never by widening a regex or
33
+ // releasing new pipeline logic. Apple's list came first; the peers are seeded
34
+ // with the names that plausibly arrive next.
31
35
  export const APPLE_ONLY = [
32
36
  "avkit",
33
37
  "cryptokit",
@@ -42,6 +46,38 @@ export const APPLE_ONLY = [
42
46
  "xcode-project-analyzer",
43
47
  ];
44
48
 
49
+ export const ANDROID_ONLY = [
50
+ "ktlint",
51
+ "detekt",
52
+ "espresso",
53
+ "hilt-di",
54
+ "proguard-r8",
55
+ "workmanager",
56
+ "jetpack-glance",
57
+ "coroutines",
58
+ "material3-theming",
59
+ "baseline-profiles",
60
+ ];
61
+
62
+ export const BACKEND_ONLY = [
63
+ "grpc-services",
64
+ "message-queues",
65
+ "sql-migrations",
66
+ ];
67
+
68
+ export const FRONTEND_ONLY = [
69
+ "storybook",
70
+ "web-vitals",
71
+ "service-workers",
72
+ ];
73
+
74
+ export const STACK_ONLY = {
75
+ "ai-ios-toolkit": APPLE_ONLY,
76
+ "ai-android-toolkit": ANDROID_ONLY,
77
+ "ai-backend-toolkit": BACKEND_ONLY,
78
+ "ai-frontend-toolkit": FRONTEND_ONLY,
79
+ };
80
+
45
81
  // stack → matching pattern (per-stack routing patterns (formerly in stack-swap.sh))
46
82
  //
47
83
  // A skill lands in every plugin whose pattern it matches, so a bare substring
@@ -70,7 +106,9 @@ export const STACK_PATTERNS = {
70
106
  */
71
107
  export function routeSkill(name) {
72
108
  if (COMMON_SKILLS.includes(name)) return [COMMON_PLUGIN];
73
- if (APPLE_ONLY.includes(name)) return ["ai-ios-toolkit"];
109
+ for (const [plugin, list] of Object.entries(STACK_ONLY)) {
110
+ if (list.includes(name)) return [plugin];
111
+ }
74
112
  const hits = [];
75
113
  for (const [plugin, re] of Object.entries(STACK_PATTERNS)) {
76
114
  if (re.test(name)) hits.push(plugin);
@@ -56,6 +56,7 @@ import {
56
56
  COMMON_PLUGIN,
57
57
  COMMON_SKILLS,
58
58
  APPLE_ONLY,
59
+ STACK_ONLY,
59
60
  STACK_PATTERNS,
60
61
  } from "./_stack-routing.mjs";
61
62
 
@@ -119,9 +120,12 @@ for (const skill of allSkills) {
119
120
  desired[COMMON_PLUGIN].add(skill);
120
121
  continue;
121
122
  }
122
- if (APPLE_ONLY.includes(skill)) {
123
- desired["ai-ios-toolkit"].add(skill);
124
- continue;
123
+ {
124
+ const only = Object.entries(STACK_ONLY).find(([, list]) => list.includes(skill));
125
+ if (only) {
126
+ desired[only[0]].add(skill);
127
+ continue;
128
+ }
125
129
  }
126
130
  let matched = false;
127
131
  for (const [plugin, re] of Object.entries(STACK_PATTERNS)) {
@@ -144,7 +148,7 @@ for (const skill of allSkills) {
144
148
  // source in this repo, so CI can gate it without a checkout of the plugins repo.
145
149
  const multiRouted = [];
146
150
  for (const skill of allSkills) {
147
- if (COMMON_SKILLS.includes(skill) || APPLE_ONLY.includes(skill)) continue;
151
+ if (COMMON_SKILLS.includes(skill) || Object.values(STACK_ONLY).some((l) => l.includes(skill))) continue;
148
152
  const owners = Object.entries(STACK_PATTERNS)
149
153
  .filter(([, re]) => re.test(skill))
150
154
  .map(([p]) => p);
@@ -160,8 +164,14 @@ if (args.includes("--check-routing")) {
160
164
  console.error("fix: anchor the offending STACK_PATTERNS alternative to the exact skill name");
161
165
  process.exit(1);
162
166
  }
167
+ if (unrouted.length) {
168
+ console.error(`check-routing: ${unrouted.length} skill(s) route to NO plugin`);
169
+ for (const s of unrouted) console.error(` FAIL ${s}`);
170
+ console.error("fix: add the name to its stack's STACK_ONLY list in _stack-routing.mjs");
171
+ process.exit(1);
172
+ }
163
173
  console.log(
164
- `check-routing: clean (${allSkills.length} skills, ${unrouted.length} unrouted, 0 multi-routed)`,
174
+ `check-routing: clean (${allSkills.length} skills, 0 unrouted, 0 multi-routed)`,
165
175
  );
166
176
  process.exit(0);
167
177
  }
@@ -121,6 +121,9 @@ need_jq() {
121
121
  # the Phase 7 / halt emit that reads the real run status. The emitter no-ops
122
122
  # unless prefs.global.usageLog.enabled; detached so it never blocks a boundary.
123
123
  usage_live_ping() {
124
+ # Smoke runs exercise this script's state handling, never the live dashboard -
125
+ # a test gate must not leave phantom "running" rows on the timeline.
126
+ [ -n "${MULTI_AGENT_SMOKE:-}" ] && return 0
124
127
  local task="$1" phase="$2"
125
128
  local script="$HOME/.claude/scripts/usage-report.mjs"
126
129
  local prefs="$HOME/.claude/multi-agent-preferences.json"