@mmerterden/multi-agent-pipeline 15.5.0 → 15.6.1
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 +19 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +44 -41
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +24 -50
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +6 -6
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +2 -2
- package/pipeline/multi-agent-refs/rules.md +1 -1
- package/pipeline/schemas/figma-project-config.schema.json +1 -1
- package/pipeline/scripts/_stack-routing.mjs +40 -2
- package/pipeline/scripts/build-stack-plugins.mjs +15 -5
- package/pipeline/scripts/usage-report.mjs +51 -11
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,25 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [15.6.1] - 2026-08-19
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **`/multi-agent:update` installs from npm, not from a git clone**: the registry is the single update channel - latest published release resolved with a direct registry read (never `npm view`'s cache), downloaded via `npm pack` with the registry pinned, installed with `install.js --all`, changes rendered from the packaged CHANGELOG, smokes run from the tarball. A pipeline repo clone is now purely a maintainer workspace (synced by `/multi-agent:sync`); consumers need no git access at all, so collaborator grants on the private repo can stay read-only or be dropped.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`node --test` runs stop pinging the live dashboard**: the tracker-entities suite calls `phase-tracker.sh init` outside run-smokes' `MULTI_AGENT_SMOKE` guard, so every test run left a phantom "probe" row on the timeline. The suite now sets the flag itself, and `usage-report.mjs` refuses to emit under it as the last line of defense for any caller.
|
|
26
|
+
- **Usage report reads the tracker as it is actually written**: `tracker-state.json` stores `phases` as an array, but the reporter iterated it with `Object.entries`, so dashboard phase ids were array indexes - every phase after a skipped one was mislabeled (Commit id "6" reported as Faz 5). Failed-phase error tags carried the same wrong ids.
|
|
27
|
+
- **Run duration and terminal timestamp resolve from the tracker**: nothing stamps `state.finishedAt`, so every run reported `du=null` and a terminal emit was stamped with the reporter's wall clock (wrong for backfills). Both now fall back to the tracker's phase span (earliest start to latest completion).
|
|
28
|
+
- **Version and user fields stop reporting null**: the reporter reads the installer's `~/.claude/.pipeline-version` marker (installed trees have no adjacent `package.json`) and falls back to `identity.name` when `identity.username` is absent.
|
|
29
|
+
|
|
30
|
+
## [15.6.0] - 2026-08-18
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- **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`.
|
|
37
|
+
|
|
19
38
|
## [15.5.0] - 2026-08-18
|
|
20
39
|
|
|
21
40
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.6.1",
|
|
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",
|
|
@@ -1,43 +1,49 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Update the pipeline to the latest
|
|
3
|
-
description-tr: "Pipeline'ı son
|
|
2
|
+
description: "Update the pipeline to the latest published npm release: registry check, npm pack, install, migrate. Use when the installed pipeline is behind and should be brought to the latest version."
|
|
3
|
+
description-tr: "Pipeline'ı npm'deki son yayına günceller: registry kontrolü, npm pack, install, migrate."
|
|
4
4
|
allowed-tools: Bash, Read, Write, AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multi-agent update
|
|
8
8
|
|
|
9
|
-
Update the pipeline in one command. Existing preferences are preserved; only skill / script / schema files are refreshed.
|
|
9
|
+
Update the pipeline in one command. The npm registry is the single update channel: the latest published release is downloaded and installed. Existing preferences are preserved; only skill / script / schema files are refreshed.
|
|
10
|
+
|
|
11
|
+
A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/multi-agent:sync` - it is never consulted here. A fix that only exists on `main` reaches users when a release is published, not before.
|
|
10
12
|
|
|
11
13
|
## Steps
|
|
12
14
|
|
|
13
|
-
1. **
|
|
15
|
+
1. **Resolve the package and both versions** (run steps 1-3 in ONE shell block so the variables survive):
|
|
14
16
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
PKG="@{npm-scope}/multi-agent-pipeline"
|
|
18
|
+
REG="https://registry.npmjs.org"
|
|
19
|
+
CUR=$(tr -d '[:space:]' < "$HOME/.claude/.pipeline-version" 2>/dev/null)
|
|
20
|
+
[ -n "$CUR" ] || CUR="unknown"
|
|
21
|
+
# Read the registry directly - `npm view` can answer from a stale local cache.
|
|
22
|
+
LATEST=$(curl -fsS "$REG/$(printf '%s' "$PKG" | sed 's|/|%2F|')/latest" \
|
|
23
|
+
| node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).version' 2>/dev/null)
|
|
24
|
+
if [ -z "$LATEST" ]; then
|
|
25
|
+
echo "Registry unreachable ($REG) - check your network and retry."
|
|
22
26
|
exit 1
|
|
23
27
|
fi
|
|
28
|
+
echo "Current: v$CUR Latest: v$LATEST"
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
2. **
|
|
31
|
+
2. **Stop early when already current** (still refresh the marketplace, step 4b):
|
|
27
32
|
```bash
|
|
28
|
-
|
|
29
|
-
echo "Current: v$OLD_VERSION"
|
|
33
|
+
[ "$CUR" = "$LATEST" ] && echo "✓ Already up to date: v$LATEST"
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
3. **
|
|
36
|
+
3. **Download the release and install**:
|
|
33
37
|
```bash
|
|
34
|
-
|
|
38
|
+
UPD_DIR="${TMPDIR:-/tmp}/multi-agent-update"
|
|
39
|
+
rm -rf "$UPD_DIR" && mkdir -p "$UPD_DIR"
|
|
40
|
+
# --registry pinned: an ambient .npmrc must not reroute the scope elsewhere.
|
|
41
|
+
npm pack "$PKG@$LATEST" --registry "$REG" --pack-destination "$UPD_DIR" --silent >/dev/null
|
|
42
|
+
tar -xzf "$UPD_DIR"/*.tgz -C "$UPD_DIR"
|
|
43
|
+
node "$UPD_DIR/package/install.js" --all
|
|
35
44
|
```
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
node "$PIPE_DIR/install.js" --all
|
|
40
|
-
```
|
|
46
|
+
The installer refreshes every configured CLI target (Claude Code, Copilot CLI, Codex CLI) and writes the new version to `$HOME/.claude/.pipeline-version`.
|
|
41
47
|
|
|
42
48
|
4b. **Update the stack-plugin marketplace** (pulls the latest plugin versions):
|
|
43
49
|
```bash
|
|
@@ -98,44 +104,41 @@ Update the pipeline in one command. Existing preferences are preserved; only ski
|
|
|
98
104
|
fi
|
|
99
105
|
```
|
|
100
106
|
|
|
101
|
-
6. **Show the new version
|
|
107
|
+
6. **Show the new version and its changes** (from the packaged CHANGELOG - there is no git history on this channel):
|
|
102
108
|
```bash
|
|
103
|
-
|
|
109
|
+
NEW=$(tr -d '[:space:]' < "$HOME/.claude/.pipeline-version" 2>/dev/null)
|
|
104
110
|
echo ""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
cd "$PIPE_DIR" && git log --oneline "v$OLD_VERSION..v$NEW_VERSION" 2>/dev/null | head -15
|
|
112
|
-
fi
|
|
111
|
+
echo "✓ Updated: v$CUR → v$NEW"
|
|
112
|
+
echo ""
|
|
113
|
+
echo "Changes:"
|
|
114
|
+
awk -v new="## [$NEW]" -v cur="## [$CUR]" \
|
|
115
|
+
'index($0,new){on=1} on&&index($0,cur){exit} on' \
|
|
116
|
+
"$UPD_DIR/package/CHANGELOG.md" | head -40
|
|
113
117
|
```
|
|
114
118
|
|
|
115
|
-
7. **Smoke test (optional)
|
|
119
|
+
7. **Smoke test (optional)** - the release tarball ships exactly two smokes for this purpose:
|
|
116
120
|
```bash
|
|
117
121
|
echo ""
|
|
118
122
|
echo "Verification:"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
bash "$PIPE_DIR/pipeline/scripts/smoke-schema-validation.sh" 2>&1 | tail -1
|
|
123
|
-
bash "$PIPE_DIR/pipeline/scripts/smoke-cross-cli-behavior.sh" 2>&1 | tail -1
|
|
123
|
+
bash "$UPD_DIR/package/pipeline/scripts/smoke-schema-validation.sh" 2>&1 | tail -1
|
|
124
|
+
bash "$UPD_DIR/package/pipeline/scripts/smoke-cross-cli-behavior.sh" 2>&1 | tail -1
|
|
125
|
+
rm -rf "$UPD_DIR"
|
|
124
126
|
```
|
|
125
127
|
|
|
126
128
|
## Output
|
|
127
129
|
|
|
128
130
|
```
|
|
129
|
-
Current:
|
|
130
|
-
->
|
|
131
|
+
Current: v15.6.0 Latest: v15.6.1
|
|
132
|
+
-> npm pack @{npm-scope}/multi-agent-pipeline@15.6.1
|
|
131
133
|
-> node install.js --all (51 commands, 228 scripts, 205 skills)
|
|
132
134
|
-> migrate-prefs.mjs (0 changes - already v2.6.0)
|
|
133
135
|
|
|
134
|
-
✓ Updated:
|
|
136
|
+
✓ Updated: v15.6.0 → v15.6.1
|
|
135
137
|
|
|
136
138
|
Changes:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
## [15.6.1] - 2026-08-19
|
|
140
|
+
### Fixed
|
|
141
|
+
- Usage report reads the tracker as it is actually written
|
|
139
142
|
...
|
|
140
143
|
|
|
141
144
|
Verification:
|
|
@@ -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
|
|
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
|
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
2>&1 | tail -5
|
|
134
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
**
|
|
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
|
|
14
|
-
|
|
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-
|
|
130
|
-
| Biometric test | Auth flow changes | `mcp__dev-toolkit__ios_biometric` (
|
|
131
|
-
| Launch time |
|
|
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`
|
|
134
|
-
|
|
|
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
|
|
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,
|
|
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
|
-
-
|
|
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.
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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) ||
|
|
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,
|
|
174
|
+
`check-routing: clean (${allSkills.length} skills, 0 unrouted, 0 multi-routed)`,
|
|
165
175
|
);
|
|
166
176
|
process.exit(0);
|
|
167
177
|
}
|
|
@@ -212,6 +212,12 @@ function resolveStatePath({ state, taskId }) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
function packageVersion() {
|
|
215
|
+
try {
|
|
216
|
+
const marker = readFileSync(join(homedir(), ".claude", ".pipeline-version"), "utf-8").trim();
|
|
217
|
+
if (marker) return marker;
|
|
218
|
+
} catch {
|
|
219
|
+
/* marker absent */
|
|
220
|
+
}
|
|
215
221
|
const candidates = [
|
|
216
222
|
join(__dirname, "..", "..", "package.json"),
|
|
217
223
|
join(homedir(), ".claude", "multi-agent-pipeline", "package.json"),
|
|
@@ -260,10 +266,25 @@ function phaseDurationSec(p) {
|
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
function trackerSummary(state) {
|
|
269
|
+
const empty = {
|
|
270
|
+
tk: null,
|
|
271
|
+
cost: null,
|
|
272
|
+
phs: null,
|
|
273
|
+
failed: [],
|
|
274
|
+
models: [],
|
|
275
|
+
startedAt: null,
|
|
276
|
+
endedAt: null,
|
|
277
|
+
};
|
|
263
278
|
const path = resolveTracker(state);
|
|
264
|
-
if (!path) return
|
|
279
|
+
if (!path) return empty;
|
|
265
280
|
const tracker = readJson(path);
|
|
266
|
-
|
|
281
|
+
const raw = tracker?.phases;
|
|
282
|
+
const list = Array.isArray(raw)
|
|
283
|
+
? raw
|
|
284
|
+
: raw && typeof raw === "object"
|
|
285
|
+
? Object.entries(raw).map(([id, p]) => ({ id, ...(p ?? {}) }))
|
|
286
|
+
: null;
|
|
287
|
+
if (!list || list.length === 0) return empty;
|
|
267
288
|
|
|
268
289
|
let tin = 0;
|
|
269
290
|
let tout = 0;
|
|
@@ -271,7 +292,10 @@ function trackerSummary(state) {
|
|
|
271
292
|
const phs = [];
|
|
272
293
|
const failed = [];
|
|
273
294
|
const models = new Set();
|
|
274
|
-
|
|
295
|
+
let startedAt = typeof tracker?.started_at === "string" && tracker.started_at ? tracker.started_at : null;
|
|
296
|
+
let endedAt = null;
|
|
297
|
+
for (const p of list) {
|
|
298
|
+
const id = p?.id != null && String(p.id) !== "" ? String(p.id) : "?";
|
|
275
299
|
if (p?.status === "failed") failed.push(id);
|
|
276
300
|
if (p?.model) models.add(String(p.model));
|
|
277
301
|
const pin = Number(p?.tokens_in || 0);
|
|
@@ -280,6 +304,10 @@ function trackerSummary(state) {
|
|
|
280
304
|
tin += pin;
|
|
281
305
|
tout += pout;
|
|
282
306
|
tcache += pcache;
|
|
307
|
+
const ps = p?.started_at || p?.startedAt || null;
|
|
308
|
+
const pf = p?.completed_at || p?.finished_at || p?.finishedAt || null;
|
|
309
|
+
if (ps && (!startedAt || ps < startedAt)) startedAt = ps;
|
|
310
|
+
if (pf && (!endedAt || pf > endedAt)) endedAt = pf;
|
|
283
311
|
phs.push({
|
|
284
312
|
p: Number.isNaN(Number(id)) ? id : Number(id),
|
|
285
313
|
st: p?.status || null,
|
|
@@ -300,6 +328,8 @@ function trackerSummary(state) {
|
|
|
300
328
|
phs: phs.length ? phs : null,
|
|
301
329
|
failed,
|
|
302
330
|
models: Array.from(models).slice(0, 10),
|
|
331
|
+
startedAt,
|
|
332
|
+
endedAt,
|
|
303
333
|
};
|
|
304
334
|
}
|
|
305
335
|
|
|
@@ -312,11 +342,18 @@ function deriveErrors(state, failedPhases) {
|
|
|
312
342
|
return Array.from(errs).slice(0, 20);
|
|
313
343
|
}
|
|
314
344
|
|
|
315
|
-
function durationSec(state) {
|
|
345
|
+
function durationSec(state, spend) {
|
|
316
346
|
const s = state.startedAt ? Date.parse(state.startedAt) : NaN;
|
|
317
347
|
const f = state.finishedAt ? Date.parse(state.finishedAt) : NaN;
|
|
318
|
-
if (Number.isNaN(s)
|
|
319
|
-
|
|
348
|
+
if (!Number.isNaN(s) && !Number.isNaN(f) && f >= s) {
|
|
349
|
+
return Math.round((f - s) / 1000);
|
|
350
|
+
}
|
|
351
|
+
const ts = spend?.startedAt ? Date.parse(spend.startedAt) : NaN;
|
|
352
|
+
const tf = spend?.endedAt ? Date.parse(spend.endedAt) : NaN;
|
|
353
|
+
if (!Number.isNaN(ts) && !Number.isNaN(tf) && tf >= ts) {
|
|
354
|
+
return Math.round((tf - ts) / 1000);
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
320
357
|
}
|
|
321
358
|
|
|
322
359
|
function runId(state) {
|
|
@@ -324,8 +361,10 @@ function runId(state) {
|
|
|
324
361
|
return base == null ? null : String(base);
|
|
325
362
|
}
|
|
326
363
|
|
|
327
|
-
function eventTimestamp(state) {
|
|
328
|
-
|
|
364
|
+
function eventTimestamp(state, spend) {
|
|
365
|
+
if (state.finishedAt) return state.finishedAt;
|
|
366
|
+
if (isTerminalStatus(state) && spend?.endedAt) return spend.endedAt;
|
|
367
|
+
return state.updatedAt || new Date().toISOString();
|
|
329
368
|
}
|
|
330
369
|
|
|
331
370
|
const TERMINAL_STATUSES = ["complete", "completed", "failed", "paused", "halted", "error"];
|
|
@@ -360,8 +399,8 @@ function buildEvent(state) {
|
|
|
360
399
|
const ctx = deriveContext(state);
|
|
361
400
|
return {
|
|
362
401
|
id: runId(state),
|
|
363
|
-
t: eventTimestamp(state),
|
|
364
|
-
u: state.identity?.username || null,
|
|
402
|
+
t: eventTimestamp(state, spend),
|
|
403
|
+
u: state.identity?.username || state.identity?.name || null,
|
|
365
404
|
c: commandOf(state),
|
|
366
405
|
m: state.mode || null,
|
|
367
406
|
ap: Boolean(state.autopilot),
|
|
@@ -376,7 +415,7 @@ function buildEvent(state) {
|
|
|
376
415
|
? state.reviewIterations.length
|
|
377
416
|
: 0,
|
|
378
417
|
pr: Boolean(state.pr && (state.pr.url || state.pr.number)),
|
|
379
|
-
du: durationSec(state),
|
|
418
|
+
du: durationSec(state, spend),
|
|
380
419
|
tk: spend.tk,
|
|
381
420
|
cost: spend.cost,
|
|
382
421
|
phs: spend.phs,
|
|
@@ -416,6 +455,7 @@ async function post(endpoint, token, event) {
|
|
|
416
455
|
|
|
417
456
|
async function main() {
|
|
418
457
|
const args = parseArgs(process.argv.slice(2));
|
|
458
|
+
if (process.env.MULTI_AGENT_SMOKE && !args.dryRun) return;
|
|
419
459
|
const prefs = resolvePrefs();
|
|
420
460
|
const token = resolveToken();
|
|
421
461
|
const endpoint = prefs.endpoint || ENDPOINT_DEFAULT;
|