@mmerterden/multi-agent-pipeline 16.31.0 → 17.0.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 +88 -0
- package/README.md +101 -101
- package/README.tr.md +101 -101
- package/docs/adr/0011-dormant-ci.md +10 -1
- package/docs/adr/0012-macos-only.md +98 -0
- package/docs/adr/README.md +1 -0
- package/docs/engineering.md +1 -1
- package/index.js +26 -0
- package/install/_dev-only-files.mjs +0 -1
- package/install/index.mjs +10 -0
- package/package.json +5 -3
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +40 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +15 -7
- package/pipeline/commands/multi-agent/stack/SKILL.md +31 -32
- package/pipeline/commands/multi-agent/status/SKILL.md +17 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +26 -21
- package/pipeline/lib/account-resolver.sh +1 -1
- package/pipeline/lib/stack-detect.sh +200 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +29 -10
- package/pipeline/multi-agent-refs/features/doctor.md +15 -3
- package/pipeline/multi-agent-refs/features/visual-evidence.md +49 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +14 -5
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +26 -12
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +6 -4
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +2 -3
- package/pipeline/multi-agent-refs/rules.md +4 -5
- package/pipeline/schemas/agent-state.schema.json +99 -25
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/scripts/_stack-routing.mjs +91 -0
- package/pipeline/scripts/capture-resume.sh +76 -14
- package/pipeline/scripts/doctor.mjs +26 -3
- package/pipeline/scripts/gc-abandoned.sh +352 -0
- package/pipeline/scripts/keychain.py +1 -1
- package/pipeline/scripts/usage-report.mjs +5 -5
- package/pipeline/scripts/gate-linux.sh +0 -62
|
@@ -817,13 +817,21 @@ Stack skills ship as versioned plugins in the `{owner}/multi-agent-plugins` mark
|
|
|
817
817
|
```bash
|
|
818
818
|
claude marketplace add {owner}/multi-agent-plugins 2>/dev/null || true
|
|
819
819
|
```
|
|
820
|
-
2.
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
820
|
+
2. Ask the detector, then derive. No marker list here: a second list is a second
|
|
821
|
+
answer, and this one defaulted to `ai-ios-toolkit` when nothing matched - how
|
|
822
|
+
a Next.js site and several Node CLIs ended up with the iOS toolkit.
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
eval "$(bash $HOME/.claude/lib/stack-detect.sh "$PROJECT_ROOT")" # MA_STACKS, MA_STACK_WHY
|
|
826
|
+
node -e 'import(process.env.HOME+"/.claude/scripts/_stack-routing.mjs").then(m=>
|
|
827
|
+
console.log(m.pluginsForStacks(process.argv[1].split(" ").filter(Boolean)).plugins.join(" ")))' "$MA_STACKS"
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
Report `MA_STACK_WHY` with the result. `ai-common-toolkit` and
|
|
831
|
+
`ai-analyst-toolkit` are in every answer, the empty one included: neither is
|
|
832
|
+
stack-specific. **No marker matched is an answer** - those two, not a guess.
|
|
833
|
+
A name the marketplace does not carry is dropped with `dropped[]` naming it,
|
|
834
|
+
so a toolkit that was never shipped is caught before it fails to load.
|
|
827
835
|
3. Report the enabled set. To change later, run `/multi-agent:stack <ios|android|web|backend|...>` in the repo. Pipeline Phase 1 auto-detects the stack for its own routing regardless of enablement.
|
|
828
836
|
|
|
829
837
|
The marketplace repo name (`multi-agent-plugins`) is generic; a different org points `{owner}` at its own fork - nothing in the pipeline is coupled to a specific account.
|
|
@@ -29,17 +29,26 @@ This replaces the old `stack-swap.sh` mechanic that physically moved skill direc
|
|
|
29
29
|
|
|
30
30
|
## Stack → plugin map
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
Derived, not listed. `pluginsForStacks()` in `scripts/_stack-routing.mjs` owns
|
|
33
|
+
it, and this file deliberately carries no copy: the mapping was written out by
|
|
34
|
+
hand in four places, and every copy is a chance to get the one asymmetry wrong -
|
|
35
|
+
the detector says `web`, the marketplace ships `ai-frontend-toolkit`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node -e 'import(process.env.HOME+"/.claude/scripts/_stack-routing.mjs").then(m=>{
|
|
39
|
+
const st=process.argv.slice(1).flatMap(a=>m.expandStackArg(a));
|
|
40
|
+
if(!st.length){console.error("Unknown stack. One of: "+m.KNOWN_STACKS.join(" ")+" mobile fullstack all");process.exit(1);}
|
|
41
|
+
const r=m.pluginsForStacks(st);
|
|
42
|
+
if(r.dropped.length) console.error("not in the marketplace, dropped: "+r.dropped.join(" "));
|
|
43
|
+
console.log(r.plugins.join(" "));
|
|
44
|
+
})' "$@"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`ai-common-toolkit` and `ai-analyst-toolkit` are in every answer, including the
|
|
48
|
+
empty one: neither is stack-specific. Multiple args union their sets, and the
|
|
49
|
+
aliases (`frontend`, `mobile`, `fullstack`, `all`) expand in the module so
|
|
50
|
+
`/multi-agent:stack mobile` cannot mean one thing here and another in the
|
|
51
|
+
installer.
|
|
43
52
|
|
|
44
53
|
## Behaviour
|
|
45
54
|
|
|
@@ -70,13 +79,6 @@ if ! claude marketplace list 2>/dev/null | grep -q "$MP"; then
|
|
|
70
79
|
|| echo "note: add the marketplace once with: claude marketplace add {owner}/multi-agent-plugins"
|
|
71
80
|
fi
|
|
72
81
|
|
|
73
|
-
COMMON="ai-common-toolkit@${MP}"
|
|
74
|
-
ANALYST="ai-analyst-toolkit@${MP}"
|
|
75
|
-
IOS="ai-ios-toolkit@${MP}"
|
|
76
|
-
ANDROID="ai-android-toolkit@${MP}"
|
|
77
|
-
WEB="ai-frontend-toolkit@${MP}"
|
|
78
|
-
BACKEND="ai-backend-toolkit@${MP}"
|
|
79
|
-
|
|
80
82
|
# Enable-mode only: with zero args the write rules below would set every stack
|
|
81
83
|
# toolkit to false (nothing but the always-on pair is in $ON), silently wiping the repo's
|
|
82
84
|
# selection. No args belongs to the picker / status path (Behaviour step 1).
|
|
@@ -85,20 +87,17 @@ if [ "$#" -eq 0 ]; then
|
|
|
85
87
|
exit 0
|
|
86
88
|
fi
|
|
87
89
|
|
|
88
|
-
# resolve every arg
|
|
89
|
-
ON
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*) echo "Unknown stack '$ARG'. One of: ios android mobile backend web fullstack all"; exit 1 ;;
|
|
100
|
-
esac
|
|
101
|
-
done
|
|
90
|
+
# resolve every arg and derive the ON set - one owner, see the map above.
|
|
91
|
+
# The marketplace suffix is appended INSIDE node: `$ON` is a single string, and
|
|
92
|
+
# re-splitting it in the shell to suffix each name is the zsh word-splitting trap
|
|
93
|
+
# (`printf %s@x $ON` suffixes the whole string, not each word).
|
|
94
|
+
ON=$(MP="$MP" node -e 'import(process.env.HOME+"/.claude/scripts/_stack-routing.mjs").then(m=>{
|
|
95
|
+
const st=process.argv.slice(1).flatMap(a=>m.expandStackArg(a));
|
|
96
|
+
if(!st.length){console.error("Unknown stack. One of: "+m.KNOWN_STACKS.join(" ")+" mobile fullstack all");process.exit(1);}
|
|
97
|
+
const r=m.pluginsForStacks(st);
|
|
98
|
+
if(r.dropped.length) console.error("not in the marketplace, dropped: "+r.dropped.join(" "));
|
|
99
|
+
console.log(r.plugins.map(p=>p+"@"+process.env.MP).join(" "));
|
|
100
|
+
})' "$@") || exit 1
|
|
102
101
|
```
|
|
103
102
|
|
|
104
103
|
After resolving `$ON`, edit `.claude/settings.json` (create `{ "enabledPlugins": {} }` if absent) so that:
|
|
@@ -22,7 +22,23 @@ Show every active and completed task as a table.
|
|
|
22
22
|
3. **Parse state** - for each task:
|
|
23
23
|
- `taskId`, `branch`, `currentPhase`, `status`, `startedAt`, `autopilot`
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
3b. **Sort into three groups, and label them.** `in_progress` alone cannot tell
|
|
26
|
+
a run that is waiting for you from one that died, and on this machine that
|
|
27
|
+
difference covered 20 runs: 3 were holding at Phase 6/7 with their PR already
|
|
28
|
+
open, 11 were left at a Phase 0 question with nothing built, 6 stopped
|
|
29
|
+
mid-development. One "in progress" label for all three is what made a
|
|
30
|
+
finished job and a crashed one share a row.
|
|
31
|
+
|
|
32
|
+
| Group | Test | Action offered |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Waiting on you | `status == "awaiting_input"`, or a `pr.url`, or phase 6/7 | `resume #N` - the work landed, it needs your answer |
|
|
35
|
+
| Stopped mid-development | anything else past phase 0 | `resume #N` or `kill #N` |
|
|
36
|
+
| Left at a question | phase 0 | `garbage-collect --abandoned` - nothing was built |
|
|
37
|
+
|
|
38
|
+
A run with no `status` is **not** placed in any group. Unknown is not a
|
|
39
|
+
finding, and calling it dead is the same false claim in the other direction.
|
|
40
|
+
|
|
41
|
+
4. **Render as a table**, grouped per 3b, with the group as a section heading:
|
|
26
42
|
```
|
|
27
43
|
🤖 Multi-Agent Tasks
|
|
28
44
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "One-shot sync of the entire multi-agent ecosystem: Claude Code, Copilot CLI, pipeline repo, website, and the multi-agent-toolkit MCP server. Use when work is finished and should be propagated across Claude Code, Copilot CLI, the repos, the website and the multi-agent-toolkit server."
|
|
3
3
|
description-tr: "Tüm multi-agent ekosisteminin tek atımlık senkronu: Claude Code, Copilot CLI, pipeline repo, website ve multi-agent-toolkit MCP sunucusu."
|
|
4
|
-
argument-hint: "[release] [multi-agent-toolkit]
|
|
4
|
+
argument-hint: "[release] [multi-agent-toolkit]"
|
|
5
5
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, WebFetch
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, WebFetch
|
|
|
9
9
|
|
|
10
10
|
**One command. Full ecosystem sync. No arguments needed.**
|
|
11
11
|
|
|
12
|
-
When invoked, it synchronizes all targets in order. It detects what changed, updates only the stale ones, and returns a result report.
|
|
12
|
+
When invoked, it synchronizes all targets in order. It detects what changed, updates only the stale ones, and returns a result report. macOS only (ADR-0012); credential access goes through `~/.claude/lib/credential-store.sh` so no call site names the keychain backend.
|
|
13
13
|
|
|
14
14
|
**Input**: $ARGUMENTS (optional)
|
|
15
15
|
|
|
@@ -18,7 +18,6 @@ When invoked, it synchronizes all targets in order. It detects what changed, upd
|
|
|
18
18
|
| (none) | Full ecosystem sync: Claude Code, Copilot CLI, pipeline repo, website, multi-agent-toolkit MCP server. |
|
|
19
19
|
| `release` | Full sync + version bump + tag + npm publish + website deploy (plus the multi-agent-toolkit ship path) |
|
|
20
20
|
| `multi-agent-toolkit` | Run Step 3d only: gate, commit and publish the companion MCP server |
|
|
21
|
-
| `--platform=<macos\|linux\|windows>` | Override automatic platform detection. For CI / cross-platform smoke. |
|
|
22
21
|
| `"change description"` | Apply the description to every target at once |
|
|
23
22
|
|
|
24
23
|
## Targets
|
|
@@ -32,25 +31,32 @@ When invoked, it synchronizes all targets in order. It detects what changed, upd
|
|
|
32
31
|
| 4 | Website | `{owner}/{website-host}` | <- version + features |
|
|
33
32
|
| 5 | multi-agent-toolkit MCP server | resolved from `prefs.global.devToolkit` or the `mcpServers` registration | own repo: gate, commit, publish |
|
|
34
33
|
|
|
35
|
-
## Platform Support (macOS
|
|
34
|
+
## Platform Support (macOS only)
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
v17.0.0 removed Linux and Windows (ADR-0012). `package.json` declares
|
|
37
|
+
`os: ["darwin"]`, which npm enforces on the root package, so this tree cannot be
|
|
38
|
+
installed elsewhere - a detection table for hosts that cannot run the installer
|
|
39
|
+
would be a table the agent executes against a case that no longer exists.
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| macOS | `[[ "$OSTYPE" == "darwin"* ]]` | Default. BSD `stat -f %m`, `security` for Keychain. |
|
|
42
|
-
| Linux | `[[ "$OSTYPE" == "linux"* ]]` | GNU `stat -c %Y`, `secret-tool` for Keychain (libsecret). |
|
|
43
|
-
| Windows (Git Bash / MSYS2) | `[[ "$OSTYPE" == "msys" \|\| "$OSTYPE" == "cygwin" ]]` | `~` resolves to `/c/Users/<name>`. Powershell `CredentialManager` for Keychain. POSIX paths inside Git Bash. |
|
|
44
|
-
| Windows (WSL) | `grep -qi microsoft /proc/version` | Linux toolchain inside WSL. Hostname format `WSL2-...`. |
|
|
45
|
-
|
|
46
|
-
**Cross-platform contract for synced shell scripts:**
|
|
41
|
+
**Contract for synced shell scripts.** The runtime is BSD userland and bash 3.2,
|
|
42
|
+
and that is a stricter constraint than "cross-platform", not a looser one:
|
|
47
43
|
|
|
48
44
|
1. Path handling: always quote (`"$f"`); use `$HOME`, never `/Users/...`.
|
|
49
|
-
2.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
2. `grep -P` is banned. BSD grep has no `-P`: it exits 2, and with `2>/dev/null`
|
|
46
|
+
that reads as "found nothing". Two gates have already shipped green for
|
|
47
|
+
exactly this reason. Use `-E`.
|
|
48
|
+
3. Timestamps: `stat -c %Y` FIRST, then `stat -f %m`. GNU-first ordering is
|
|
49
|
+
required because `stat -f` is a valid GNU flag (`--file-system`) that
|
|
50
|
+
succeeds and prints something that is not a timestamp.
|
|
51
|
+
4. Hashes: `sha256sum || shasum`, in that order. The gate fails a file that
|
|
52
|
+
names either alone.
|
|
53
|
+
5. `find` traversal: `-prune` for `node_modules`, `Pods`, `.build`,
|
|
54
|
+
`DerivedData`, `.next`.
|
|
55
|
+
6. Sed / awk: POSIX-portable. `-E`, never `-r`; `sed -i ''` on BSD.
|
|
56
|
+
7. Keychain I/O: always via `~/.claude/lib/credential-store.sh get|set`, never
|
|
57
|
+
`security` directly - the wrapper is what keeps a secret off argv.
|
|
58
|
+
8. Verification: after Step 3 (REPO), `bash -n` every shell script in
|
|
59
|
+
`pipeline/lib/` and `pipeline/scripts/`.
|
|
54
60
|
|
|
55
61
|
## Default Behavior (no arguments)
|
|
56
62
|
|
|
@@ -58,7 +64,6 @@ Run every step automatically:
|
|
|
58
64
|
|
|
59
65
|
```
|
|
60
66
|
Step 0: DOCTOR doctor.mjs - exit 2 or 4 stops the sync
|
|
61
|
-
Step 1: PLATFORM Detect macOS / Linux / Windows (Git Bash / WSL); export PLATFORM env
|
|
62
67
|
Step 1.5: DETECT Compare timestamps, find stale targets
|
|
63
68
|
Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 53 sub-command skills)
|
|
64
69
|
Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + 53 specs as refs + 8 agent TOML)
|
|
@@ -384,8 +389,8 @@ Measured on this machine, which is why the order is what it is:
|
|
|
384
389
|
| Candidate | login | scopes |
|
|
385
390
|
|---|---|---|
|
|
386
391
|
| `keychainMapping.github` | corporate EMU account | cannot publish to a personal scope under any grant |
|
|
387
|
-
|
|
|
388
|
-
| `gh auth token -u
|
|
392
|
+
| the personal PAT in the store | personal | `admin:public_key, gist, read:org, repo` - no `write:packages` |
|
|
393
|
+
| `gh auth token -u {owner}` | personal | `gist, read:org, repo, workflow, write:packages` ✓ |
|
|
389
394
|
|
|
390
395
|
**Two 403s mean two different things, and neither says "wrong token" plainly:**
|
|
391
396
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# that precedes the provider segment. Examples:
|
|
9
9
|
# ${USER}_Github_Access_Token -> prefix=${USER}
|
|
10
10
|
# ${USER}_Tktech_Github_Access_Token -> prefix=${USER}_Tktech
|
|
11
|
-
#
|
|
11
|
+
# {prefix}_Github_Auth_Token -> prefix={prefix}
|
|
12
12
|
#
|
|
13
13
|
# Usage:
|
|
14
14
|
# ./account-resolver.sh # JSON array
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# stack-detect.sh - answer "what is this repo built with" from the repo itself.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists: `/multi-agent:stack` writes `enabledPlugins` and a human picks
|
|
5
|
+
# the stack. When that conversation never happens the repo inherits the global
|
|
6
|
+
# setting, and on the machine this was written against only 3 of 25 checkouts had
|
|
7
|
+
# ever had it - so a Next.js site and several Node CLIs were all routed to the iOS
|
|
8
|
+
# toolkit. An unsupervised run has nobody to notice.
|
|
9
|
+
#
|
|
10
|
+
# So: infer, deterministically, from file markers - never a model call - so the
|
|
11
|
+
# same repo always answers the same way and the answer can be shown with its
|
|
12
|
+
# reason.
|
|
13
|
+
#
|
|
14
|
+
# This file answers ONE question: which of `ios android web backend` describes
|
|
15
|
+
# this repo. It deliberately does not know plugin names. "Which plugin carries a
|
|
16
|
+
# stack" is a different question with a different owner
|
|
17
|
+
# (`pipeline/scripts/_stack-routing.mjs`), and the two were one table here until
|
|
18
|
+
# that put a tenth copy of the stack-to-plugin mapping in the tree - the exact
|
|
19
|
+
# duplication `multi-agent-refs/stack-skill-routing.md` exists to prevent.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# . stack-detect.sh; ma_stack_detect <repo-root> # sets MA_STACKS, MA_STACK_WHY
|
|
23
|
+
# bash stack-detect.sh <repo-root> # prints KEY=VALUE
|
|
24
|
+
# bash stack-detect.sh --json <repo-root>
|
|
25
|
+
#
|
|
26
|
+
# Output: MA_STACKS is a space-separated subset of `ios android web backend`, in
|
|
27
|
+
# that fixed order so two runs on the same repo produce the same string. Empty is
|
|
28
|
+
# a real answer and means "no marker matched", NOT "not looked" - MA_STACK_WHY
|
|
29
|
+
# distinguishes them, because a caller that cannot tell those apart will treat an
|
|
30
|
+
# unreadable directory as a language-free repo.
|
|
31
|
+
|
|
32
|
+
# Markers whose mere presence decides a stack, as data rather than a `case`
|
|
33
|
+
# cascade: `stack:glob:maxdepth`. Adding a language is one line here, which is
|
|
34
|
+
# the point - a contributor who has to edit control flow to add Rust will
|
|
35
|
+
# instead widen somebody else's regex.
|
|
36
|
+
#
|
|
37
|
+
# Not every rule fits this shape, and forcing the two that do not would be worse
|
|
38
|
+
# than keeping them in code: Android is decided by FILE CONTENT (Gradle alone is
|
|
39
|
+
# a JVM service), and one package.json can be web, backend or both. Those two
|
|
40
|
+
# run after the table, with their reasoning at the point of decision.
|
|
41
|
+
MA_STACK_MARKERS="\
|
|
42
|
+
ios:Package.swift:3
|
|
43
|
+
ios:*.xcodeproj:3
|
|
44
|
+
ios:*.xcworkspace:3
|
|
45
|
+
ios:Podfile:3
|
|
46
|
+
web:vite.config.*:3
|
|
47
|
+
web:nuxt.config.*:3
|
|
48
|
+
web:angular.json:3
|
|
49
|
+
web:svelte.config.*:3
|
|
50
|
+
backend:requirements.txt:3
|
|
51
|
+
backend:pyproject.toml:3
|
|
52
|
+
backend:go.mod:3
|
|
53
|
+
backend:Cargo.toml:3
|
|
54
|
+
backend:pom.xml:3"
|
|
55
|
+
|
|
56
|
+
# Dependency names that decide which side of one package.json a repo is on. A
|
|
57
|
+
# Next app with API routes is honestly both, so both may be recorded.
|
|
58
|
+
MA_STACK_WEB_DEPS='"(react|next|vue|svelte|@angular/core|solid-js|astro|preact|remix)"[[:space:]]*:'
|
|
59
|
+
MA_STACK_BACKEND_DEPS='"(express|fastify|@nestjs/core|koa|hapi|@trpc/server|apollo-server)"[[:space:]]*:'
|
|
60
|
+
|
|
61
|
+
ma_stack_detect() {
|
|
62
|
+
local root="${1:-$PWD}"
|
|
63
|
+
MA_STACKS=""
|
|
64
|
+
MA_STACK_WHY=""
|
|
65
|
+
|
|
66
|
+
if [ -z "$root" ] || [ ! -d "$root" ]; then
|
|
67
|
+
MA_STACK_WHY="unreadable: $root"
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
# Strip trailing slashes before anything builds a path from $root. `find`
|
|
71
|
+
# prints `<root>/x`, so a root ending in `/` makes every `-path "$root/$sub"`
|
|
72
|
+
# prune carry a double slash and match nothing - the submodule prune then
|
|
73
|
+
# silently does not fire, and a vendored Package.swift reports a Compose app
|
|
74
|
+
# as iOS. A caller passing a directory with a trailing slash is normal.
|
|
75
|
+
while [ "${root%/}" != "$root" ] && [ "$root" != "/" ]; do root="${root%/}"; done
|
|
76
|
+
|
|
77
|
+
# Submodule paths are pruned: a vendored checkout is somebody else's repo and
|
|
78
|
+
# its markers are not this repo's stack. Measured, not theorised - the Android
|
|
79
|
+
# app vendors the shared configuration repo, which ships a Package.swift, and a
|
|
80
|
+
# depth-first scan reported that Compose app as an iOS repo.
|
|
81
|
+
local _prunes=(
|
|
82
|
+
-name node_modules -o -name .build -o -name Pods -o -name build -o -name dist
|
|
83
|
+
-o -name .next -o -name .gradle -o -name DerivedData -o -name .worktrees
|
|
84
|
+
-o -name .git -o -name vendor -o -name Carthage
|
|
85
|
+
)
|
|
86
|
+
local _sub _subs=()
|
|
87
|
+
if [ -f "$root/.gitmodules" ]; then
|
|
88
|
+
while IFS= read -r _sub; do
|
|
89
|
+
[ -n "$_sub" ] && _subs+=(-o -path "$root/$_sub")
|
|
90
|
+
done <<< "$(sed -n 's/^[[:space:]]*path[[:space:]]*=[[:space:]]*//p' "$root/.gitmodules" 2>/dev/null)"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# The repo ROOT is checked before anything deeper, and that ordering is the
|
|
94
|
+
# whole correctness argument: the root manifest is the repo's own declaration,
|
|
95
|
+
# while every deeper hit belongs to a submodule, a build output or a vendored
|
|
96
|
+
# dependency. Without it the answer is decided by `find` traversal order, which
|
|
97
|
+
# is arbitrary - it read a generated .next/package.json as a Next.js app's
|
|
98
|
+
# manifest and found no framework in it.
|
|
99
|
+
_ma_has() { # $1 = -name pattern; prints the first hit, empty when none
|
|
100
|
+
local _g
|
|
101
|
+
for _g in "$root"/$1; do
|
|
102
|
+
[ -e "$_g" ] && {
|
|
103
|
+
printf '%s\n' "$_g"
|
|
104
|
+
return 0
|
|
105
|
+
}
|
|
106
|
+
done
|
|
107
|
+
# No -mindepth here, deliberately. -mindepth suppresses predicate evaluation
|
|
108
|
+
# for shallower entries, so with -mindepth 2 the prune never fired on a
|
|
109
|
+
# depth-1 submodule directory and find walked straight into it. The root glob
|
|
110
|
+
# above already returned any root-level hit, so re-visiting depth 1 is free.
|
|
111
|
+
find "$root" -maxdepth "${2:-3}" \
|
|
112
|
+
-type d \( "${_prunes[@]}" "${_subs[@]}" \) -prune -o \
|
|
113
|
+
-name "$1" -print 2>/dev/null | head -1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_ma_add() { # $1 = stack, $2 = why-suffix
|
|
117
|
+
case " $MA_STACKS " in *" $1 "*) return 0 ;; esac
|
|
118
|
+
MA_STACKS="${MA_STACKS:+$MA_STACKS }$1"
|
|
119
|
+
_why="${_why:+$_why }$1<-$2"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
local _why="" hit row stack pat depth
|
|
123
|
+
|
|
124
|
+
# --- table markers ----------------------------------------------------
|
|
125
|
+
while IFS=: read -r stack pat depth; do
|
|
126
|
+
[ -n "$stack" ] || continue
|
|
127
|
+
case " $MA_STACKS " in *" $stack "*) continue ;; esac
|
|
128
|
+
hit=$(_ma_has "$pat" "$depth")
|
|
129
|
+
[ -n "$hit" ] && _ma_add "$stack" "${hit##*/}"
|
|
130
|
+
done <<EOF
|
|
131
|
+
$MA_STACK_MARKERS
|
|
132
|
+
EOF
|
|
133
|
+
|
|
134
|
+
# --- android, by content ----------------------------------------------
|
|
135
|
+
# Gradle alone does NOT mean Android - a JVM service builds with Gradle too.
|
|
136
|
+
# AndroidManifest.xml or the Android Gradle plugin is what separates them, and
|
|
137
|
+
# getting this wrong loads the Compose toolkit onto a Spring repo.
|
|
138
|
+
# AndroidManifest.xml lives at <module>/src/main/, which is depth 4 in every
|
|
139
|
+
# multi-module app, so this one search goes deeper than the rest.
|
|
140
|
+
hit=$(_ma_has "AndroidManifest.xml" 5)
|
|
141
|
+
if [ -z "$hit" ]; then
|
|
142
|
+
# A version catalogue is where a modern build declares the Android plugin;
|
|
143
|
+
# the root build.gradle.kts of the reference app names it nowhere.
|
|
144
|
+
local g
|
|
145
|
+
for g in "$root/gradle/libs.versions.toml" "$(_ma_has 'build.gradle*')" "$(_ma_has 'settings.gradle*')"; do
|
|
146
|
+
[ -n "$g" ] && [ -f "$g" ] || continue
|
|
147
|
+
if grep -qE "com\.android\.(application|library)|androidx|\bagp\b" "$g" 2>/dev/null; then
|
|
148
|
+
hit="$g"
|
|
149
|
+
break
|
|
150
|
+
fi
|
|
151
|
+
done
|
|
152
|
+
fi
|
|
153
|
+
[ -n "$hit" ] && _ma_add "android" "${hit##*/}"
|
|
154
|
+
|
|
155
|
+
# --- web / backend, by dependency -------------------------------------
|
|
156
|
+
local pkg
|
|
157
|
+
pkg=$(_ma_has "package.json")
|
|
158
|
+
if [ -n "$pkg" ]; then
|
|
159
|
+
local is_web=0
|
|
160
|
+
grep -qE "$MA_STACK_WEB_DEPS" "$pkg" 2>/dev/null && is_web=1
|
|
161
|
+
[ "$is_web" -eq 1 ] && _ma_add "web" "package.json"
|
|
162
|
+
if grep -qE "$MA_STACK_BACKEND_DEPS" "$pkg" 2>/dev/null; then
|
|
163
|
+
_ma_add "backend" "package.json"
|
|
164
|
+
elif [ "$is_web" -eq 0 ]; then
|
|
165
|
+
# A package.json with no web framework is still a Node project, and the
|
|
166
|
+
# backend toolkit is the one that covers Node. Reporting nothing here would
|
|
167
|
+
# send the caller to its no-marker fallback for a repo whose language is
|
|
168
|
+
# not in doubt.
|
|
169
|
+
_ma_add "backend" "package.json(node)"
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Fixed order, so the same repo always yields the same string.
|
|
174
|
+
local ordered="" s
|
|
175
|
+
for s in ios android web backend; do
|
|
176
|
+
case " $MA_STACKS " in *" $s "*) ordered="${ordered:+$ordered }$s" ;; esac
|
|
177
|
+
done
|
|
178
|
+
MA_STACKS="$ordered"
|
|
179
|
+
MA_STACK_WHY="${_why:-no marker matched}"
|
|
180
|
+
unset -f _ma_has _ma_add
|
|
181
|
+
return 0
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if [ "${BASH_SOURCE[0]:-$0}" = "$0" ]; then
|
|
185
|
+
_json=0
|
|
186
|
+
case "${1:-}" in --json)
|
|
187
|
+
_json=1
|
|
188
|
+
shift
|
|
189
|
+
;;
|
|
190
|
+
esac
|
|
191
|
+
ma_stack_detect "${1:-$PWD}" || true
|
|
192
|
+
if [ "$_json" -eq 1 ]; then
|
|
193
|
+
_arr=""
|
|
194
|
+
for _s in $MA_STACKS; do _arr="${_arr:+$_arr,}\"$_s\""; done
|
|
195
|
+
printf '{"stacks":[%s],"why":"%s"}\n' "$_arr" "$MA_STACK_WHY"
|
|
196
|
+
else
|
|
197
|
+
printf 'MA_STACKS=%s\n' "$(printf '%q' "$MA_STACKS")"
|
|
198
|
+
printf 'MA_STACK_WHY=%s\n' "$(printf '%q' "$MA_STACK_WHY")"
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
@@ -297,20 +297,39 @@ Modifier flags are orthogonal and compose:
|
|
|
297
297
|
|
|
298
298
|
---
|
|
299
299
|
|
|
300
|
-
## 7. Platform Guards (macOS
|
|
300
|
+
## 7. Platform Guards (macOS)
|
|
301
301
|
|
|
302
|
-
All three CLIs
|
|
302
|
+
All three CLIs are installed from this package, and `package.json` declares
|
|
303
|
+
`os: ["darwin"]` (ADR-0012), so every command executes on macOS. That is not a
|
|
304
|
+
licence to stop being careful about shell: the runtime is **BSD userland and
|
|
305
|
+
bash 3.2**, which is a narrower target than "portable", and three constructs
|
|
306
|
+
below exist because the BSD form silently differs rather than failing.
|
|
303
307
|
|
|
304
|
-
For Keychain I/O the canonical path is **`~/.claude/lib/credential-store.sh`**
|
|
308
|
+
For Keychain I/O the canonical path is **`~/.claude/lib/credential-store.sh`**
|
|
309
|
+
(or `~/.copilot/lib/credential-store.sh` / `~/.codex/lib/credential-store.sh` on
|
|
310
|
+
those installs). Call sites never invoke `security` themselves - the wrapper is
|
|
311
|
+
what keeps a secret off argv and what writes the audit entry.
|
|
305
312
|
|
|
306
|
-
| Purpose | Canonical
|
|
313
|
+
| Purpose | Canonical | Underlying backend (for debugging only) |
|
|
307
314
|
|---|---|---|
|
|
308
|
-
| Keychain read | `~/.claude/lib/credential-store.sh get <key>` |
|
|
309
|
-
| Keychain write | `~/.claude/lib/credential-store.sh set <key> <value>` (or `keychain.py set <key> -` to stream the secret over stdin) |
|
|
310
|
-
| Clipboard read |
|
|
311
|
-
| Clipboard clear |
|
|
312
|
-
|
|
313
|
-
|
|
315
|
+
| Keychain read | `~/.claude/lib/credential-store.sh get <key>` | `security find-generic-password -a "$USER" -s <key> -w` |
|
|
316
|
+
| Keychain write | `~/.claude/lib/credential-store.sh set <key> <value>` (or `keychain.py set <key> -` to stream the secret over stdin, keeping it off argv) | `security add-generic-password -a "$USER" -s <key> -w` |
|
|
317
|
+
| Clipboard read | `pbpaste` | - |
|
|
318
|
+
| Clipboard clear | `pbcopy < /dev/null` | - |
|
|
319
|
+
|
|
320
|
+
**The three that bite on BSD**, and why each is a rule rather than a preference:
|
|
321
|
+
|
|
322
|
+
- PCRE mode is absent from BSD grep, and is banned here. It exits 2, which under
|
|
323
|
+
`2>/dev/null` reads as "found nothing" - two gates shipped green for exactly
|
|
324
|
+
that reason. Use `-E`.
|
|
325
|
+
- `stat -c %Y` FIRST, then `stat -f %m`. GNU-first ordering is required because
|
|
326
|
+
`stat -f` is a valid GNU flag (`--file-system`) that succeeds and prints
|
|
327
|
+
something that is not a timestamp.
|
|
328
|
+
- `sed -E`, never `-r`; `sed -i ''` when editing in place.
|
|
329
|
+
|
|
330
|
+
Clipboard callers may use `pbpaste` / `pbcopy` directly; the three-way
|
|
331
|
+
`command -v` guard they used to carry was for hosts this package no longer
|
|
332
|
+
installs on.
|
|
314
333
|
|
|
315
334
|
---
|
|
316
335
|
|
|
@@ -25,9 +25,9 @@ resolver as a clean bill.
|
|
|
25
25
|
|
|
26
26
|
**A state in which a run will fail or leak. Not a state in which it will merely
|
|
27
27
|
be worse.** Without a closed definition "blocked" grows until nobody respects
|
|
28
|
-
exit 2, so only
|
|
29
|
-
`state-writable`, `prefs-valid` and `embedded-credentials`.
|
|
30
|
-
tops out at WARN however bad it looks.
|
|
28
|
+
exit 2, so only six checks can produce it: `host-platform`, `install-present`,
|
|
29
|
+
`script-surface`, `state-writable`, `prefs-valid` and `embedded-credentials`.
|
|
30
|
+
Every other check tops out at WARN however bad it looks.
|
|
31
31
|
|
|
32
32
|
## Who calls it, and what they do with the code
|
|
33
33
|
|
|
@@ -91,6 +91,18 @@ its host; everything after that is behind `--explain`.
|
|
|
91
91
|
|
|
92
92
|
## Checks
|
|
93
93
|
|
|
94
|
+
### host-platform
|
|
95
|
+
|
|
96
|
+
The host is macOS. BLOCK otherwise: every credential read shells `security`,
|
|
97
|
+
every iOS build shells `xcodebuild`, every piece of visual evidence shells
|
|
98
|
+
`simctl`, so a run on another platform does not degrade, it fails partway
|
|
99
|
+
through with a worktree and a branch already created (ADR-0012).
|
|
100
|
+
|
|
101
|
+
It runs **first**, before `install-present`, so an unsupported host reads one
|
|
102
|
+
line naming the cause instead of a cascade of consequences. The step names the
|
|
103
|
+
`MULTI_AGENT_ALLOW_NON_DARWIN=1` escape, which the entry points honour and
|
|
104
|
+
nothing tests.
|
|
105
|
+
|
|
94
106
|
### install-present
|
|
95
107
|
|
|
96
108
|
The installed tree exists and carries the subtrees a run reads: `commands/`,
|
|
@@ -33,6 +33,54 @@ model's reading of the task. A file counts as UI by stack:
|
|
|
33
33
|
Nothing about this section is optional-by-omission: when it is required and an
|
|
34
34
|
artefact is absent, the absence is written down with its reason (section 5).
|
|
35
35
|
|
|
36
|
+
### 1a. Who writes the verdict, and when
|
|
37
|
+
|
|
38
|
+
**Phase 0 Step 7.7 writes `required`, `requiredBy` and `platform`**, before it
|
|
39
|
+
probes. Naming the writer is not a formality: five phases read these three fields
|
|
40
|
+
and nothing set them, so `required` was never true, Step 7.7 never ran, Phase 3
|
|
41
|
+
never captured, and Phase 6's blocker never blocked. A contract with readers and
|
|
42
|
+
no writer reads exactly like a contract that is satisfied.
|
|
43
|
+
|
|
44
|
+
Phase 0 has no diff, so it decides on what it actually knows - `taskType` and
|
|
45
|
+
whether the intake carried a Figma reference - and that is enough for the whole
|
|
46
|
+
design-work row. The `bugfix` row needs a changed-file list, so Phase 0 records
|
|
47
|
+
it as provisional (`requiredBy: "bugfix (pending diff)"`) and **Phase 3 re-decides
|
|
48
|
+
from the real diff** before capturing. A provisional `false` never suppresses the
|
|
49
|
+
Phase 3 decision.
|
|
50
|
+
|
|
51
|
+
### 1b. The platform, and when there is not one
|
|
52
|
+
|
|
53
|
+
`platform` comes from the stack, not from the diff: it selects which device
|
|
54
|
+
tooling is probed, and that is a property of the repo.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
eval "$(bash $HOME/.claude/lib/stack-detect.sh "$PROJECT_ROOT")" # MA_STACKS
|
|
58
|
+
EVIDENCE_PLATFORM=""
|
|
59
|
+
case " $MA_STACKS " in
|
|
60
|
+
*" ios "*) EVIDENCE_PLATFORM=ios ;;
|
|
61
|
+
*" android "*) EVIDENCE_PLATFORM=android ;;
|
|
62
|
+
esac
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
iOS wins a tie, and the tie is recorded in `stackWhy`: `simctl` discovery is the
|
|
66
|
+
cheaper of the two, and a repo that is honestly both gets the same answer on
|
|
67
|
+
every run rather than one that depends on ordering.
|
|
68
|
+
|
|
69
|
+
**An empty value is a real outcome, not an error.** A web or backend repo has no
|
|
70
|
+
simulator to probe, and `probe-evidence-capability.sh` rejects anything but
|
|
71
|
+
`ios|android` with exit 2. So when `EVIDENCE_PLATFORM` is empty the probe **does
|
|
72
|
+
not run**, `platform` is written as `web` or `other` from `MA_STACKS`, and the
|
|
73
|
+
reason goes to `state.evidenceCapability.skippedReason` in the probe's own
|
|
74
|
+
`*_REASON` idiom - "no device platform in stacks [web backend]". Passing an empty
|
|
75
|
+
string instead is what the probe's exit 2 exists to refuse, and swallowing that
|
|
76
|
+
exit would turn a missing value into a silent skip.
|
|
77
|
+
|
|
78
|
+
Every `--platform` argument in this pipeline is `$EVIDENCE_PLATFORM`, read from
|
|
79
|
+
`state.visualEvidence.platform` in later phases. `$PLATFORM` is not a pipeline
|
|
80
|
+
variable: nothing ever assigned it, in any phase document, which is why
|
|
81
|
+
`smoke-evidence-probe.sh` now fails a `--platform` argument whose variable is not
|
|
82
|
+
assigned earlier in the same file.
|
|
83
|
+
|
|
36
84
|
## 2. Before - the reporter's screenshot, or nothing
|
|
37
85
|
|
|
38
86
|
**The pipeline does not build the old state to photograph it.** Reproducing a
|
|
@@ -61,7 +109,7 @@ never the only source.
|
|
|
61
109
|
|
|
62
110
|
```bash
|
|
63
111
|
bash $HOME/.claude/scripts/capture-evidence.sh after \
|
|
64
|
-
--task "$TASK_ID" --platform "$
|
|
112
|
+
--task "$TASK_ID" --platform "$EVIDENCE_PLATFORM" --label "<screen-slug>"
|
|
65
113
|
```
|
|
66
114
|
|
|
67
115
|
What the capture is worth checking against - the per-component catalog, the
|
|
@@ -591,21 +591,30 @@ Log: `Phase 0 Step 7.6: test baseline = {green|red|unknown} ({N} pre-existing fa
|
|
|
591
591
|
|
|
592
592
|
#### Step 7.7 - Evidence capability, then test depth
|
|
593
593
|
|
|
594
|
-
|
|
594
|
+
Decide, then probe, then ask, then run. Skipped only when `visualEvidence.enabled` is `false`. Contract: `features/visual-evidence.md` sections 1a, 1b, 4.
|
|
595
|
+
|
|
596
|
+
**This step is the only writer of `visualEvidence.required`, `requiredBy` and `platform`** (section 1a). No diff exists yet, so the `bugfix` row is provisional and Phase 3 re-decides.
|
|
595
597
|
|
|
596
598
|
```bash
|
|
597
|
-
eval "$(bash $HOME/.claude/
|
|
598
|
-
|
|
599
|
-
|
|
599
|
+
eval "$(bash $HOME/.claude/lib/stack-detect.sh "$PROJECT_ROOT")"
|
|
600
|
+
EVIDENCE_PLATFORM=""
|
|
601
|
+
case " $MA_STACKS " in *" ios "*) EVIDENCE_PLATFORM=ios ;; *" android "*) EVIDENCE_PLATFORM=android ;; esac
|
|
602
|
+
if [ -n "$EVIDENCE_PLATFORM" ]; then
|
|
603
|
+
eval "$(bash $HOME/.claude/scripts/probe-evidence-capability.sh \
|
|
604
|
+
--platform "$EVIDENCE_PLATFORM" --repo "$WORKTREE" \
|
|
605
|
+
--json-out "$WORKTREE/.pipeline/evidence-capability.json")"
|
|
606
|
+
fi
|
|
600
607
|
```
|
|
601
608
|
|
|
609
|
+
Empty is an outcome, not a failure: no simulator on web or backend, so the probe does not run and `evidenceCapability.skippedReason` says so. No `--changed` yet, by the same token.
|
|
610
|
+
|
|
602
611
|
One run, both forms: stdout is `EVIDENCE_*` (shell-quoted, so the eval is safe), and the same measurement lands as JSON.
|
|
603
612
|
|
|
604
613
|
Persist that file as `state.evidenceCapability`, then build the menu from it, never from a reading of the repo: `1. Sadece unit test` / `2. Unit + UI test, ekran kaydiyla` (tier 1) / `3. Unit + MCP ile akis kaydi` (tier 2).
|
|
605
614
|
|
|
606
615
|
A closed option keeps its row and prints the probe's reason verbatim. No `uiTestTargets` closes 2; `mcp` false closes 3; a missing `device` or `recorder` closes both. Targets present with no `matchingTests` leaves 2 open, warning that the whole UI suite will run. **When only option 1 is open, do not ask**: `testDepth = unit`, `testDepthSource = forced`, and the tier 3 gap is written.
|
|
607
616
|
|
|
608
|
-
|
|
617
|
+
Default as a 1-based index, never a label - same rule and same reason as Step 7.5:
|
|
609
618
|
|
|
610
619
|
```bash
|
|
611
620
|
DEPTH_DEFAULT_INDEX=1
|