@mmerterden/multi-agent-pipeline 20.20.0 → 20.22.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 +58 -0
- package/README.md +24 -13
- package/README.tr.md +21 -10
- package/docs/adr/0017-change-impact-from-the-graph.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/ecosystem.md +7 -5
- package/docs/facts.json +6 -4
- package/docs/features.md +1 -0
- package/install/catalog-history.json +1 -1
- package/install/templates/copilot-instructions.md +1 -0
- package/manifest.json +71 -64
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/route-on/SKILL.md +32 -8
- package/pipeline/commands/multi-agent/route-status/SKILL.md +12 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +8 -16
- package/pipeline/lib/model-dispatch.sh +72 -24
- package/pipeline/lib/outbound-gate.mjs +2 -0
- package/pipeline/lib/route-state.sh +118 -10
- package/pipeline/lib/toolkit-llm.sh +30 -0
- package/pipeline/lib/unattended-profile-check.mjs +23 -2
- package/pipeline/multi-agent-refs/_dev-context.md +15 -0
- package/pipeline/multi-agent-refs/analysis/evidence.md +7 -3
- package/pipeline/multi-agent-refs/analysis/locked.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +9 -3
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/doctor.md +11 -0
- package/pipeline/multi-agent-refs/features/free-models.md +111 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +21 -15
- package/pipeline/multi-agent-refs/features/product-context.md +238 -0
- package/pipeline/multi-agent-refs/features/unattended-security.md +4 -2
- package/pipeline/multi-agent-refs/knowledge.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +7 -12
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +7 -6
- package/pipeline/multi-agent-refs/platform-parity.md +23 -9
- package/pipeline/multi-agent-refs/setup/free-models.md +22 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +24 -0
- package/pipeline/preferences-template.json +1 -0
- package/pipeline/schemas/agent-state.schema.json +31 -0
- package/pipeline/schemas/analysis-spec.schema.json +14 -0
- package/pipeline/schemas/code-graph.schema.json +12 -1
- package/pipeline/schemas/diff-risk.schema.json +2 -1
- package/pipeline/schemas/prefs.schema.json +68 -1
- package/pipeline/schemas/route-config.schema.json +33 -8
- package/pipeline/schemas/secret-patterns.json +14 -0
- package/pipeline/scripts/_code-graph.mjs +76 -3
- package/pipeline/scripts/_diff-impact.mjs +312 -0
- package/pipeline/scripts/bulk-read.sh +98 -12
- package/pipeline/scripts/capture-flush.sh +58 -3
- package/pipeline/scripts/cost-table.json +9 -1
- package/pipeline/scripts/diff-risk-score.mjs +80 -9
- package/pipeline/scripts/doctor.mjs +100 -0
- package/pipeline/scripts/graph-affected.mjs +167 -36
- package/pipeline/scripts/graph-query.mjs +16 -4
- package/pipeline/scripts/pre-commit-check.sh +1 -1
- package/pipeline/scripts/product-context.mjs +539 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +39 -1
- package/pipeline/scripts/unattended_policy.py +20 -0
- package/pipeline/scripts/validate-code-graph.mjs +2 -1
- package/pipeline/scripts/validate-diff-risk.mjs +1 -0
- package/pipeline/skills/.skill-manifest.json +6 -6
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -1
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +31 -7
- package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +12 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +9 -6
|
@@ -24,6 +24,10 @@ node "$HOME/.claude/scripts/doctor.mjs"
|
|
|
24
24
|
|
|
25
25
|
Run it before the first question and again at the end. The first run turns setup from a fixed script into a targeted one: there is no point asking for a token that is already mapped and answering. The second run is the only honest way to end - "setup complete" is a claim, and the doctor's exit code is the evidence for or against it. Report both in `outputLanguage`, and if the second run still shows a BLOCK, say so plainly rather than closing on the word "complete".
|
|
26
26
|
|
|
27
|
+
## Sections
|
|
28
|
+
|
|
29
|
+
`/multi-agent:setup hosts`: the Step 3.5 host prompt per mapped hosted service, then Token Save Flow Step 3.7 per mapped GitHub/Bitbucket token. `accounts`: Step 3.6. `jira-keys`: the Jira key mapping. Tokens are left alone.
|
|
30
|
+
|
|
27
31
|
## Setup (Credential Store + Git Identity Onboarding)
|
|
28
32
|
|
|
29
33
|
Uses `$HOME/.claude/lib/credential-store.sh` to read/write secrets in the macOS Keychain (`security`, built in). The pipeline is macOS only (ADR-0012).
|
|
@@ -209,11 +213,12 @@ The Step 2 discovery summary is never the end of setup. Immediately after showin
|
|
|
209
213
|
10. `jenkins`
|
|
210
214
|
11. App Store Connect block (Step 3b; only when the target repo is iOS)
|
|
211
215
|
12. Google Play block (Step 3d; only when the target repo is Android)
|
|
216
|
+
13. Free-model keys `llm-<id>` (only after `route-on --free`): `$HOME/.claude/multi-agent-refs/setup/free-models.md`
|
|
212
217
|
|
|
213
218
|
Loop rules:
|
|
214
219
|
|
|
215
220
|
- **One prompt per service.** Never collapse the list into a single "add missing tokens now?" yes/no, and never offer one multi-select covering all services: each service has its own source URL, key name, identity binding and host prompt, so each gets its own Token Save Flow pass.
|
|
216
|
-
- **Each service pass asks, in order: token → author → host.** Step A/B collect the secret, Step C binds the git identity (author) - Bitbucket and GitHub included; every saved token gets an author - and Step 3.5 asks the host domain inline for hosted services (jira, confluence, bitbucket, fortify, graylog) - plus the Fortify project versions and the optional Graylog test instance. GitHub and Figma skip the host question (fixed domains); `bitbucket_user` skips author binding (username, not a PAT). Never end a service pass with the token saved but the author or a required host unasked.
|
|
221
|
+
- **Each service pass asks, in order: token → author → host → read binding (GitHub, Bitbucket; Token Save Flow Step 3.7).** Step A/B collect the secret, Step C binds the git identity (author) - Bitbucket and GitHub included; every saved token gets an author - and Step 3.5 asks the host domain inline for hosted services (jira, confluence, bitbucket, fortify, graylog) - plus the Fortify project versions and the optional Graylog test instance. GitHub and Figma skip the host question (fixed domains); `bitbucket_user` skips author binding (username, not a PAT). Never end a service pass with the token saved but the author or a required host unasked.
|
|
217
222
|
- **Skip is per-service.** A Skip answer leaves that service's `keychainMapping` at `null` and the loop continues with the next service. Skips never abort the loop.
|
|
218
223
|
- **`figma` and `figma_mcp` are distinct credentials for distinct tiers - ask both.** Skipping the PAT does not imply skipping MCP, and vice versa. The `figma_mcp` pass opens with a Remote / Local mode question instead of a clipboard paste (see "Figma MCP Setup" below): Remote generates a `figu_` OAuth token for the current user, Local wires the PAT-based local server.
|
|
219
224
|
- The loop ends only when every missing service has been asked exactly once; then continue to Step 4 (Verify all).
|
|
@@ -256,23 +261,10 @@ Use the global default ({hosts.jira}) or set a per-account host?
|
|
|
256
261
|
2) Set per-account host
|
|
257
262
|
```
|
|
258
263
|
|
|
259
|
-
If `2`, prompt for `jiraHost` / `bitbucketHost` / `confluenceHost` (each optional). Save to `prefs.global.accounts[]
|
|
260
|
-
|
|
261
|
-
```json
|
|
262
|
-
{
|
|
263
|
-
"global": {
|
|
264
|
-
"accounts": [
|
|
265
|
-
{ "id": "personal", "jiraHost": "personal.atlassian.net" },
|
|
266
|
-
{ "id": "client-x", "jiraHost": "client-x.atlassian.net", "bitbucketHost": "bitbucket.client-x.com" }
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
264
|
+
If `2`, prompt for `jiraHost` / `bitbucketHost` / `confluenceHost` (each optional). Save to `prefs.global.accounts[]` as `{ \"id\": \"client-x\", \"jiraHost\": \"client-x.atlassian.net\", \"bitbucketHost\": \"bitbucket.client-x.com\" }`.
|
|
271
265
|
|
|
272
266
|
`account-resolver.sh` reads this on every invocation; per-account override wins over `global.hosts.*`. Skip this step on autopilot - `accounts[]` stays empty until the user opts in via `/multi-agent:setup accounts`.
|
|
273
267
|
|
|
274
|
-
**Re-run / update**: `/multi-agent:setup accounts` opens the editor for this block without re-running token discovery.
|
|
275
|
-
|
|
276
268
|
---
|
|
277
269
|
|
|
278
270
|
### Step 4 - Verify all
|
|
@@ -377,7 +369,7 @@ Standard key names are a convention for NEW tokens; discovery may find existing
|
|
|
377
369
|
|
|
378
370
|
### Inline Token Save Pattern
|
|
379
371
|
|
|
380
|
-
All tokens go through `~/.claude/lib/credential-store.sh
|
|
372
|
+
All tokens go through `~/.claude/lib/credential-store.sh` (Python helper and label/service attributes: see "Setup" above).
|
|
381
373
|
|
|
382
374
|
```bash
|
|
383
375
|
# PAT / API Key / JSON contents (from clipboard)
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
# model-dispatch.sh <call-site> [--persona P] [--phase N] [--task-kind K]
|
|
10
10
|
# [--default RUNG] [--task-id ID]
|
|
11
11
|
#
|
|
12
|
-
# <call-site> is one of the `scope` members: subagent | bulk-read |
|
|
13
|
-
# stdout is a single rung NAME. Rung names are the contract; the model
|
|
14
|
-
# one lives in cost-table.json and moves without a config edit.
|
|
12
|
+
# <call-site> is one of the `scope` members: subagent | bulk-read | delegate |
|
|
13
|
+
# research. stdout is a single rung NAME. Rung names are the contract; the model
|
|
14
|
+
# id behind one lives in cost-table.json and moves without a config edit.
|
|
15
15
|
#
|
|
16
|
-
# Exit is always 0 and stdout is always a usable
|
|
16
|
+
# Exit is always 0 and stdout is always a usable answer. A router that can fail
|
|
17
17
|
# turns every call site into a place the run can die, for a feature that ships
|
|
18
18
|
# disabled; when anything is missing, unparseable or out of scope, the caller's
|
|
19
19
|
# default comes back unchanged.
|
|
@@ -22,16 +22,18 @@
|
|
|
22
22
|
#
|
|
23
23
|
# Layer 1 an Anthropic rung. Reachable from every call site, because it
|
|
24
24
|
# writes to a seam that already exists and opens no new network path.
|
|
25
|
-
# Layer 2
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
25
|
+
# Layer 2 the `free` rung: the multi-agent-toolkit's verified free-model
|
|
26
|
+
# chain. Reachable only where the pipeline makes the call itself -
|
|
27
|
+
# bulk-read and delegate - and only with modelRouting.free.enabled.
|
|
28
|
+
# Its caller must keep its default as the fallback, because the chain
|
|
29
|
+
# answers ESCALATE_TO_CLAUDE whenever no free answer survives
|
|
30
|
+
# verification. A subagent is dispatched by the HOST, so `free` and
|
|
31
|
+
# every other non-Anthropic rung is refused there and the rule's next
|
|
32
|
+
# rung answers instead.
|
|
33
|
+
#
|
|
34
|
+
# Strategies: manual keeps the rule's order; task-fit tries the cheapest rung
|
|
35
|
+
# first; cost-ceiling keeps the order until cost-budget-check.mjs reports the run
|
|
36
|
+
# at its warn threshold of budgetCeilingUsd, then tries the cheapest first.
|
|
35
37
|
|
|
36
38
|
set -uo pipefail
|
|
37
39
|
|
|
@@ -74,7 +76,7 @@ fi
|
|
|
74
76
|
[ "$DEFAULT_RUNG" = "fable" ] && [ "$FABLE_ON" != "true" ] && DEFAULT_RUNG="opus"
|
|
75
77
|
|
|
76
78
|
case "$CALL_SITE" in
|
|
77
|
-
subagent|bulk-read|research) ;;
|
|
79
|
+
subagent|bulk-read|delegate|research) ;;
|
|
78
80
|
*) emit "$DEFAULT_RUNG" ;;
|
|
79
81
|
esac
|
|
80
82
|
|
|
@@ -103,11 +105,45 @@ MATCH=$(jq -r \
|
|
|
103
105
|
and ((.when.taskKind // null) as $k | $k == null or $k == $kind)
|
|
104
106
|
)
|
|
105
107
|
] | first | (.prefer // []) | join(" ")' "$PREFS" 2>/dev/null) || emit "$DEFAULT_RUNG"
|
|
106
|
-
[
|
|
108
|
+
[ "$MATCH" = "null" ] && MATCH=""
|
|
109
|
+
|
|
110
|
+
FREE_ON=$(jq -r '.global.modelRouting.free.enabled // false' "$PREFS" 2>/dev/null)
|
|
111
|
+
CANDIDATES="$MATCH"
|
|
112
|
+
case "$CALL_SITE" in
|
|
113
|
+
bulk-read|delegate) [ "$FREE_ON" = "true" ] && CANDIDATES="free $MATCH" ;;
|
|
114
|
+
esac
|
|
115
|
+
[ -n "${CANDIDATES// /}" ] || emit "$DEFAULT_RUNG"
|
|
107
116
|
|
|
108
117
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
109
118
|
COST_TABLE="$HERE/../scripts/cost-table.json"
|
|
110
119
|
|
|
120
|
+
STRATEGY=$(jq -r '.global.modelRouting.strategy // "manual"' "$PREFS" 2>/dev/null)
|
|
121
|
+
CHEAPEST_FIRST=false
|
|
122
|
+
STRATEGY_NOTE=""
|
|
123
|
+
case "$STRATEGY" in
|
|
124
|
+
task-fit) CHEAPEST_FIRST=true; STRATEGY_NOTE="task-fit" ;;
|
|
125
|
+
cost-ceiling)
|
|
126
|
+
CEILING=$(jq -r '.global.modelRouting.budgetCeilingUsd // empty' "$PREFS" 2>/dev/null)
|
|
127
|
+
if [ -n "$CEILING" ] && [ "$TASK_ID" != "unknown" ] && command -v node >/dev/null 2>&1 \
|
|
128
|
+
&& [ -f "$HERE/../scripts/cost-budget-check.mjs" ]; then
|
|
129
|
+
node "$HERE/../scripts/cost-budget-check.mjs" --task-id "$TASK_ID" --max-usd "$CEILING" --enabled >/dev/null 2>&1
|
|
130
|
+
case $? in
|
|
131
|
+
10|11) CHEAPEST_FIRST=true; STRATEGY_NOTE="cost-ceiling reached" ;;
|
|
132
|
+
esac
|
|
133
|
+
fi
|
|
134
|
+
;;
|
|
135
|
+
esac
|
|
136
|
+
|
|
137
|
+
if [ "$CHEAPEST_FIRST" = "true" ] && [ -f "$COST_TABLE" ]; then
|
|
138
|
+
CANDIDATES=$(
|
|
139
|
+
jq -r --arg c "$CANDIDATES" '
|
|
140
|
+
.prices as $p
|
|
141
|
+
| ($c | split(" ") | map(select(length > 0)))
|
|
142
|
+
| to_entries
|
|
143
|
+
| sort_by([($p[.value].inPerMtok // 1e9), .key])
|
|
144
|
+
| map(.value) | join(" ")' "$COST_TABLE" 2>/dev/null || printf '%s' "$CANDIDATES")
|
|
145
|
+
fi
|
|
146
|
+
|
|
111
147
|
# The fable rung answers only when the fable switch is on (FABLE_ON, read
|
|
112
148
|
# above). Routing is a policy over what is available; it is not a second way to
|
|
113
149
|
# turn a rung on, or `/multi-agent:model off` would stop meaning anything the
|
|
@@ -116,13 +152,13 @@ COST_TABLE="$HERE/../scripts/cost-table.json"
|
|
|
116
152
|
record() {
|
|
117
153
|
[ -x "$HERE/../scripts/log-metric.sh" ] || return 0
|
|
118
154
|
local rec
|
|
119
|
-
rec=$(jq -r '.global.modelRouting.recordDecisions
|
|
155
|
+
rec=$(jq -r 'if .global.modelRouting.recordDecisions == false then "false" else "true" end' "$PREFS" 2>/dev/null)
|
|
120
156
|
[ "$rec" = "false" ] && return 0
|
|
121
157
|
"$HERE/../scripts/log-metric.sh" "$TASK_ID" "${PHASE:-0}" model_routing.decision \
|
|
122
158
|
call_site="$CALL_SITE" rung="$1" reason="$2" >/dev/null 2>&1 || true
|
|
123
159
|
}
|
|
124
160
|
|
|
125
|
-
for rung in $
|
|
161
|
+
for rung in $CANDIDATES; do
|
|
126
162
|
if [ ! -f "$COST_TABLE" ]; then
|
|
127
163
|
record "$rung" "no cost table; rung taken on trust"
|
|
128
164
|
emit "$rung"
|
|
@@ -139,15 +175,27 @@ for rung in $MATCH; do
|
|
|
139
175
|
echo "model-dispatch: rule prefers 'fable' but modelFallback.fableEnabled is false; skipping it" >&2
|
|
140
176
|
continue
|
|
141
177
|
fi
|
|
178
|
+
if [ "$PROVIDER" = "free-chain" ]; then
|
|
179
|
+
if [ "$FREE_ON" != "true" ]; then
|
|
180
|
+
echo "model-dispatch: rule prefers 'free' but modelRouting.free.enabled is false (route-on --free); skipping it" >&2
|
|
181
|
+
continue
|
|
182
|
+
fi
|
|
183
|
+
case "$CALL_SITE" in
|
|
184
|
+
bulk-read|delegate)
|
|
185
|
+
record "$rung" "free chain${STRATEGY_NOTE:+, $STRATEGY_NOTE}"
|
|
186
|
+
emit "$rung" ;;
|
|
187
|
+
esac
|
|
188
|
+
echo "model-dispatch: rung 'free' is reachable only at bulk-read and delegate; a $CALL_SITE is dispatched by the host, skipping it" >&2
|
|
189
|
+
continue
|
|
190
|
+
fi
|
|
142
191
|
if [ "$PROVIDER" != "anthropic" ]; then
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
echo "model-dispatch: rung '$rung' is $PROVIDER and no call site can dispatch to one yet; skipping it" >&2
|
|
192
|
+
# Every call site that asks here hands any other rung to something that
|
|
193
|
+
# speaks the Anthropic ladder or the free chain, so returning an external
|
|
194
|
+
# rung would produce a failed call rather than a cheaper one.
|
|
195
|
+
echo "model-dispatch: rung '$rung' is $PROVIDER and no call site dispatches to it; skipping it" >&2
|
|
148
196
|
continue
|
|
149
197
|
fi
|
|
150
|
-
record "$rung" "rule matched"
|
|
198
|
+
record "$rung" "rule matched${STRATEGY_NOTE:+, $STRATEGY_NOTE}"
|
|
151
199
|
emit "$rung"
|
|
152
200
|
done
|
|
153
201
|
|
|
@@ -82,6 +82,8 @@ export const RULES = [
|
|
|
82
82
|
{ name: "stripe-key", certain: true, re: /\b[sr]k_live_[A-Za-z0-9]{20,}\b/g },
|
|
83
83
|
{ name: "perplexity-key", certain: true, re: /\bpplx-[A-Za-z0-9]{32,}\b/g },
|
|
84
84
|
{ name: "huggingface-token", certain: true, re: /\bhf_[A-Za-z0-9]{30,}\b/g },
|
|
85
|
+
{ name: "groq-key", certain: true, re: /\bgsk_[A-Za-z0-9]{40,}\b/g },
|
|
86
|
+
{ name: "openrouter-key", certain: true, re: /\bsk-or-v1-[A-Za-z0-9]{40,}\b/g },
|
|
85
87
|
{
|
|
86
88
|
name: "service-account-json",
|
|
87
89
|
certain: true,
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
# for a configuration the user already gave.
|
|
11
11
|
#
|
|
12
12
|
# Usage:
|
|
13
|
-
# route-state.sh status print the current policy
|
|
14
|
-
# route-state.sh on [--strategy=S] [--scope=a,b]
|
|
13
|
+
# route-state.sh status print the current policy and savings
|
|
14
|
+
# route-state.sh on [--strategy=S] [--scope=a,b] [--free [--allow-training]] [--no-free]
|
|
15
15
|
# route-state.sh off
|
|
16
|
-
# route-state.sh set-rules <file.json> replace rules[] from a JSON array
|
|
16
|
+
# route-state.sh set-rules <file.json> replace rules[] from a JSON array,
|
|
17
|
+
# checked against the schema and cost table
|
|
17
18
|
#
|
|
18
19
|
# Exit codes:
|
|
19
20
|
# 0 - done
|
|
@@ -34,7 +35,7 @@ if [ -f "$_MA_RS_HERE/user-facing.sh" ]; then . "$_MA_RS_HERE/user-facing.sh"; e
|
|
|
34
35
|
# included - through a third layer, breaks the subscription's auth model, and
|
|
35
36
|
# silently changes which model answers. The schema encodes the same list so a
|
|
36
37
|
# hand-edited preferences file is rejected too, rather than only this entry point.
|
|
37
|
-
ALLOWED_SCOPE="subagent bulk-read research"
|
|
38
|
+
ALLOWED_SCOPE="subagent bulk-read delegate research"
|
|
38
39
|
|
|
39
40
|
die() { echo "route-state: $1" >&2; exit "${2:-3}"; }
|
|
40
41
|
|
|
@@ -47,6 +48,9 @@ jq -e . "$PREFS" >/dev/null 2>&1 || die "$PREFS is not valid JSON - refusing t
|
|
|
47
48
|
|
|
48
49
|
ACTION="${1:-status}"; shift 2>/dev/null || true
|
|
49
50
|
|
|
51
|
+
COST_TABLE="$_MA_RS_HERE/../scripts/cost-table.json"
|
|
52
|
+
USAGE_LOG="${MCP_TOOLKIT_INDEX_DIR:-$HOME/.claude/logs/multi-agent-toolkit/.index}/llm-usage.jsonl"
|
|
53
|
+
|
|
50
54
|
write_prefs() {
|
|
51
55
|
local filter="$1"; shift
|
|
52
56
|
local tmp
|
|
@@ -56,6 +60,57 @@ write_prefs() {
|
|
|
56
60
|
mv "$tmp" "$PREFS"
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
# Last seven days of the toolkit's usage logs (attended, and an unattended run's
|
|
64
|
+
# own under ~/.multi-agent-unattended): verified answers, cache hits and
|
|
65
|
+
# escalations, and what those tokens would have cost on haiku, the rung bulk-read
|
|
66
|
+
# and simple tasks fall back to. An estimate, and labelled as one.
|
|
67
|
+
free_savings() {
|
|
68
|
+
local logs=() f
|
|
69
|
+
for f in "$USAGE_LOG" "$HOME/.multi-agent-unattended/toolkit-index/llm-usage.jsonl"; do
|
|
70
|
+
[ -f "$f" ] && logs+=("$f")
|
|
71
|
+
done
|
|
72
|
+
if [ "${#logs[@]}" -eq 0 ]; then
|
|
73
|
+
echo "savings: no free-model task recorded yet ($USAGE_LOG)"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
local since
|
|
77
|
+
since=$(( $(date +%s) - 7 * 86400 ))
|
|
78
|
+
jq -rs --argjson since "$since" --slurpfile ct "$COST_TABLE" '
|
|
79
|
+
map(select((.ts | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601? // 0) >= $since))
|
|
80
|
+
| ($ct[0].prices.haiku // {inPerMtok: 1, outPerMtok: 5}) as $h
|
|
81
|
+
| {
|
|
82
|
+
n: length,
|
|
83
|
+
ok: (map(select(.ok and (.cached | not))) | length),
|
|
84
|
+
cached: (map(select(.ok and .cached)) | length),
|
|
85
|
+
esc: (map(select(.ok | not)) | length),
|
|
86
|
+
pin: (map(.prompt_tokens // 0) | add // 0),
|
|
87
|
+
pout: (map(.completion_tokens // 0) | add // 0),
|
|
88
|
+
by: (map(select(.ok and .provider)) | group_by(.provider) | map("\(.[0].provider) \(length)") | join(", "))
|
|
89
|
+
}
|
|
90
|
+
| . + {usd: ((.pin * $h.inPerMtok + .pout * $h.outPerMtok) / 1000000)}
|
|
91
|
+
| "savings (7 days): \(.n) task(s), \(.ok) answered free and verified, \(.cached) from cache, \(.esc) handed back to Claude\n"
|
|
92
|
+
+ " tokens kept off Claude: \(.pin) in + \(.pout) out (~$\(.usd * 100 | round / 100) at haiku rates, an estimate)"
|
|
93
|
+
+ (if .by != "" then "\n by provider: \(.by)" else "" end)
|
|
94
|
+
' "${logs[@]}" 2>/dev/null || echo "savings: the usage log could not be read"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# What the toolkit can reach, without a single network call: configured
|
|
98
|
+
# providers and where their keys come from. Names only, never values.
|
|
99
|
+
free_providers() {
|
|
100
|
+
local tl="$_MA_RS_HERE/toolkit-llm.sh"
|
|
101
|
+
[ -f "$tl" ] || tl="$HOME/.claude/lib/toolkit-llm.sh"
|
|
102
|
+
[ -f "$tl" ] || { echo "free chain: toolkit-llm.sh is not installed"; return 1; }
|
|
103
|
+
# shellcheck source=/dev/null
|
|
104
|
+
. "$tl"
|
|
105
|
+
local out
|
|
106
|
+
out=$(ma_toolkit_llm providers ${1:+--allow-training} 2>/dev/null) || { echo "free chain: the multi-agent-toolkit CLI could not be run (node and npx are needed)"; return 1; }
|
|
107
|
+
jq -r '
|
|
108
|
+
"free chain: \(.configured) provider(s) usable under the \(.data_policy) data policy",
|
|
109
|
+
(.providers[] | " \(if .configured and .allowed_by_policy then "ready " elif .configured then "policy " else "missing" end) \(.id)\(if .key_source then " (" + .key_source + ")" else "" end)\(if (.configured | not) then " - " + .setup else "" end)")
|
|
110
|
+
' <<< "$out"
|
|
111
|
+
[ "$(jq -r '.configured' <<< "$out")" -gt 0 ]
|
|
112
|
+
}
|
|
113
|
+
|
|
59
114
|
case "$ACTION" in
|
|
60
115
|
status)
|
|
61
116
|
ENABLED=$(jq -r '.global.modelRouting.enabled // false' "$PREFS")
|
|
@@ -63,12 +118,16 @@ case "$ACTION" in
|
|
|
63
118
|
SCOPE=$(jq -r '(.global.modelRouting.scope // ["subagent"]) | join(", ")' "$PREFS")
|
|
64
119
|
NRULES=$(jq -r '(.global.modelRouting.rules // []) | length' "$PREFS")
|
|
65
120
|
CEIL=$(jq -r '.global.modelRouting.budgetCeilingUsd // "none"' "$PREFS")
|
|
66
|
-
REC=$(jq -r '.global.modelRouting.recordDecisions
|
|
121
|
+
REC=$(jq -r 'if .global.modelRouting.recordDecisions == false then "false" else "true" end' "$PREFS")
|
|
122
|
+
|
|
123
|
+
FREE=$(jq -r '.global.modelRouting.free.enabled // false' "$PREFS")
|
|
124
|
+
TRAIN=$(jq -r '.global.modelRouting.free.allowTraining // false' "$PREFS")
|
|
67
125
|
|
|
68
126
|
echo "routing: $ENABLED"
|
|
69
127
|
echo "strategy: $STRATEGY"
|
|
70
128
|
echo "scope: $SCOPE"
|
|
71
129
|
echo "rules: $NRULES"
|
|
130
|
+
echo "free: $FREE$([ "$FREE" = "true" ] && echo " (data policy: $([ "$TRAIN" = "true" ] && echo any || echo no-training))")"
|
|
72
131
|
echo "ceiling: $CEIL"
|
|
73
132
|
echo "decisions: $REC"
|
|
74
133
|
echo ""
|
|
@@ -79,34 +138,46 @@ case "$ACTION" in
|
|
|
79
138
|
exit 0
|
|
80
139
|
fi
|
|
81
140
|
|
|
82
|
-
if [ "$NRULES" -eq 0 ]; then
|
|
141
|
+
if [ "$NRULES" -eq 0 ] && [ "$FREE" != "true" ]; then
|
|
83
142
|
echo "Armed with no rules. Nothing matches, so nothing is routed - this is a"
|
|
84
143
|
echo "configuration state, not an error."
|
|
85
|
-
|
|
144
|
+
elif [ "$NRULES" -gt 0 ]; then
|
|
86
145
|
jq -r '(.global.modelRouting.rules // [])[] |
|
|
87
146
|
" when " + ([.when | to_entries[] | "\(.key)=\(.value)"] | join(" ")) +
|
|
88
147
|
" -> " + (.prefer | join(" > "))' "$PREFS"
|
|
89
148
|
fi
|
|
90
149
|
|
|
150
|
+
if [ "$FREE" = "true" ]; then
|
|
151
|
+
echo " free chain first at: bulk-read, delegate (Anthropic default when it escalates)"
|
|
152
|
+
echo ""
|
|
153
|
+
free_savings
|
|
154
|
+
fi
|
|
155
|
+
|
|
91
156
|
echo ""
|
|
92
157
|
# The limit is printed every time rather than documented once, because the
|
|
93
158
|
# question it answers ("routing is on, why is the reviewer still on Opus")
|
|
94
159
|
# otherwise arrives days later as a bug report.
|
|
95
160
|
echo "Honest limit: on Claude Code a subagent cannot be sent to a non-Anthropic"
|
|
96
161
|
echo "model - subagent dispatch belongs to the host. Phase 1/2/3 personas stay"
|
|
97
|
-
echo "inside the Anthropic ladder whatever the rules say.
|
|
98
|
-
echo "
|
|
162
|
+
echo "inside the Anthropic ladder whatever the rules say. Free models answer only"
|
|
163
|
+
echo "where this pipeline makes the call itself (bulk-read, delegate), only after"
|
|
164
|
+
echo "the toolkit verified the answer against its source, and Claude takes over"
|
|
165
|
+
echo "whenever none passes."
|
|
99
166
|
;;
|
|
100
167
|
|
|
101
168
|
on)
|
|
102
|
-
STRATEGY="manual"; SCOPE_ARG=""
|
|
169
|
+
STRATEGY="manual"; SCOPE_ARG=""; FREE_ARG=""; TRAIN_ARG=false
|
|
103
170
|
for arg in "$@"; do
|
|
104
171
|
case "$arg" in
|
|
105
172
|
--strategy=*) STRATEGY="${arg#*=}" ;;
|
|
106
173
|
--scope=*) SCOPE_ARG="${arg#*=}" ;;
|
|
174
|
+
--free) FREE_ARG=on ;;
|
|
175
|
+
--no-free) FREE_ARG=off ;;
|
|
176
|
+
--allow-training) TRAIN_ARG=true ;;
|
|
107
177
|
*) die "unknown option: $arg" 2 ;;
|
|
108
178
|
esac
|
|
109
179
|
done
|
|
180
|
+
[ "$TRAIN_ARG" = "true" ] && [ "$FREE_ARG" != "on" ] && die "--allow-training only goes with --free" 2
|
|
110
181
|
case "$STRATEGY" in
|
|
111
182
|
manual|task-fit|cost-ceiling) ;;
|
|
112
183
|
*) die "strategy must be manual, task-fit or cost-ceiling (got '$STRATEGY')" 2 ;;
|
|
@@ -138,6 +209,26 @@ case "$ACTION" in
|
|
|
138
209
|
| .global.modelRouting.scope //= ["subagent"]
|
|
139
210
|
' --arg st "$STRATEGY"
|
|
140
211
|
fi
|
|
212
|
+
if [ "$FREE_ARG" = "on" ]; then
|
|
213
|
+
write_prefs '
|
|
214
|
+
.global.modelRouting.free = {enabled: true, allowTraining: $tr}
|
|
215
|
+
| .global.modelRouting.scope = (((.global.modelRouting.scope // ["subagent"]) + ["bulk-read", "delegate"]) | unique)
|
|
216
|
+
' --argjson tr "$TRAIN_ARG"
|
|
217
|
+
echo "free chain enabled at bulk-read and delegate (data policy: $([ "$TRAIN_ARG" = "true" ] && echo any || echo no-training))"
|
|
218
|
+
TRAIN_FLAG=""
|
|
219
|
+
[ "$TRAIN_ARG" = "true" ] && TRAIN_FLAG=1
|
|
220
|
+
if ! free_providers "$TRAIN_FLAG"; then
|
|
221
|
+
echo ""
|
|
222
|
+
echo "No free provider is usable yet, so every task still runs on Claude. Add one key"
|
|
223
|
+
echo "(free at https://console.groq.com/keys) with $(ma_command setup), or from a shell:"
|
|
224
|
+
echo " bash \"\$HOME/.claude/lib/credential-store.sh\" set llm-groq - (paste the key, then Ctrl-D)"
|
|
225
|
+
echo "or export GROQ_API_KEY for the server, then run $(ma_command route-status)."
|
|
226
|
+
fi
|
|
227
|
+
echo ""
|
|
228
|
+
elif [ "$FREE_ARG" = "off" ]; then
|
|
229
|
+
write_prefs '.global.modelRouting.free.enabled = false'
|
|
230
|
+
echo "free chain disabled; Anthropic rungs only"
|
|
231
|
+
fi
|
|
141
232
|
echo "routing enabled (strategy: $STRATEGY)"
|
|
142
233
|
exec "$0" status
|
|
143
234
|
;;
|
|
@@ -154,6 +245,23 @@ case "$ACTION" in
|
|
|
154
245
|
[ -n "$FILE" ] || die "set-rules needs a JSON file containing an array of rules" 2
|
|
155
246
|
[ -f "$FILE" ] || die "no such file: $FILE" 2
|
|
156
247
|
jq -e 'type == "array"' "$FILE" >/dev/null 2>&1 || die "$FILE must contain a JSON ARRAY of rules" 2
|
|
248
|
+
# The schema's rule shape, enforced here so a hand-written file cannot store
|
|
249
|
+
# a rule that dispatch silently skips: when has 1-3 known keys with valid
|
|
250
|
+
# values, prefer is a non-empty list of rungs the cost table prices.
|
|
251
|
+
RUNGS=$(jq -c '.prices | keys' "$COST_TABLE" 2>/dev/null || echo '[]')
|
|
252
|
+
BAD=$(jq -r --argjson rungs "$RUNGS" '
|
|
253
|
+
to_entries[] | .key as $i | .value as $r
|
|
254
|
+
| if ($r | type) != "object" then "rule \($i): not an object"
|
|
255
|
+
elif (($r | keys) - ["when", "prefer"]) != [] then "rule \($i): unknown key(s) \((($r | keys) - ["when", "prefer"]) | join(", "))"
|
|
256
|
+
elif ($r.when | type) != "object" or ($r.when | length) == 0 then "rule \($i): when needs persona, phase or taskKind"
|
|
257
|
+
elif (($r.when | keys) - ["persona", "phase", "taskKind"]) != [] then "rule \($i): when has unknown key(s) \((($r.when | keys) - ["persona", "phase", "taskKind"]) | join(", "))"
|
|
258
|
+
elif ($r.when.persona != null and ($r.when.persona | type) != "string") then "rule \($i): persona must be a string"
|
|
259
|
+
elif ($r.when.phase != null and (($r.when.phase | type) != "number" or $r.when.phase < 0 or $r.when.phase > 5 or ($r.when.phase | floor) != $r.when.phase)) then "rule \($i): phase must be an integer 0-5"
|
|
260
|
+
elif ($r.when.taskKind != null and ([$r.when.taskKind] | inside(["bugfix", "feature", "refactor", "chore", "component"]) | not)) then "rule \($i): taskKind must be bugfix, feature, refactor, chore or component"
|
|
261
|
+
elif ($r.prefer | type) != "array" or ($r.prefer | length) == 0 then "rule \($i): prefer needs at least one rung"
|
|
262
|
+
elif ([$r.prefer[] | select((type != "string") or (. as $x | $rungs | index($x) | not))] | length) > 0 then "rule \($i): unknown rung(s) \([$r.prefer[] | select((type != "string") or (. as $x | $rungs | index($x) | not))] | map(tostring) | join(", ")) (known: \($rungs | join(", ")))"
|
|
263
|
+
else empty end' "$FILE" 2>/dev/null)
|
|
264
|
+
[ -z "$BAD" ] || die "$(printf 'rules rejected, nothing written:\n%s' "$BAD")" 2
|
|
157
265
|
write_prefs '.global.modelRouting //= {} | .global.modelRouting.rules = $r' --slurpfile _ignore /dev/null --argjson r "$(cat "$FILE")"
|
|
158
266
|
echo "rules replaced: $(jq -r 'length' "$FILE")"
|
|
159
267
|
;;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# toolkit-llm.sh - run the multi-agent-toolkit's free-model CLI.
|
|
3
|
+
#
|
|
4
|
+
# Sourced by bulk-read.sh and route-state.sh. `ma_toolkit_llm <args>` runs
|
|
5
|
+
# `multi-agent-toolkit-llm <args>` from the first place it exists:
|
|
6
|
+
#
|
|
7
|
+
# 1. MULTI_AGENT_TOOLKIT_LLM a path to the CLI script
|
|
8
|
+
# 2. multi-agent-toolkit-llm on PATH a global install
|
|
9
|
+
# 3. npx -p @mmerterden/multi-agent-toolkit-mcp@latest, the same package
|
|
10
|
+
# the MCP registration runs
|
|
11
|
+
#
|
|
12
|
+
# A maintainer's working copy (prefs.global.devToolkit) is deliberately not a
|
|
13
|
+
# source: it can sit on an unreleased branch, and a run should use what ships.
|
|
14
|
+
#
|
|
15
|
+
# The CLI prints one JSON object and exits 0 on a verified answer, 3 on
|
|
16
|
+
# ESCALATE_TO_CLAUDE, 2 on a usage error. Any other status (no node, no network
|
|
17
|
+
# for npx) is the caller's cue to keep its own default.
|
|
18
|
+
|
|
19
|
+
ma_toolkit_llm() {
|
|
20
|
+
if [ -n "${MULTI_AGENT_TOOLKIT_LLM:-}" ] && [ -f "$MULTI_AGENT_TOOLKIT_LLM" ]; then
|
|
21
|
+
node "$MULTI_AGENT_TOOLKIT_LLM" "$@"
|
|
22
|
+
return $?
|
|
23
|
+
fi
|
|
24
|
+
if command -v multi-agent-toolkit-llm >/dev/null 2>&1; then
|
|
25
|
+
multi-agent-toolkit-llm "$@"
|
|
26
|
+
return $?
|
|
27
|
+
fi
|
|
28
|
+
command -v npx >/dev/null 2>&1 || return 127
|
|
29
|
+
npx -y --@mmerterden:registry=https://registry.npmjs.org -p @mmerterden/multi-agent-toolkit-mcp@latest multi-agent-toolkit-llm "$@"
|
|
30
|
+
}
|
|
@@ -22,9 +22,22 @@ import { join } from "node:path";
|
|
|
22
22
|
|
|
23
23
|
const PREFS_HOST_KEYS = ["jira", "confluence", "bitbucket"];
|
|
24
24
|
|
|
25
|
+
// The free-model hosts a sandboxed bulk-read may reach once route-on --free is
|
|
26
|
+
// on: Groq alone under the default no-training policy, all five with
|
|
27
|
+
// allowTraining. A local Ollama needs no host; allowLocalBinding covers it.
|
|
28
|
+
const FREE_HOSTS_NO_TRAINING = ["api.groq.com"];
|
|
29
|
+
const FREE_HOSTS_ANY = [
|
|
30
|
+
"api.groq.com",
|
|
31
|
+
"api.mistral.ai",
|
|
32
|
+
"generativelanguage.googleapis.com",
|
|
33
|
+
"openrouter.ai",
|
|
34
|
+
"api.z.ai",
|
|
35
|
+
];
|
|
36
|
+
|
|
25
37
|
/**
|
|
26
38
|
* Hosts from prefs.global.hosts, so the sandbox's network list covers this
|
|
27
|
-
* machine's own Jira / Confluence / Bitbucket
|
|
39
|
+
* machine's own Jira / Confluence / Bitbucket, plus the free-model providers
|
|
40
|
+
* modelRouting.free allows.
|
|
28
41
|
*
|
|
29
42
|
* @param {string} home
|
|
30
43
|
* @returns {string[]}
|
|
@@ -35,7 +48,7 @@ export function prefsHosts(home) {
|
|
|
35
48
|
readFileSync(join(home, ".claude", "multi-agent-preferences.json"), "utf-8"),
|
|
36
49
|
);
|
|
37
50
|
const hosts = prefs?.global?.hosts || {};
|
|
38
|
-
|
|
51
|
+
const own = PREFS_HOST_KEYS.map((k) => hosts[k])
|
|
39
52
|
.filter((v) => typeof v === "string" && v.trim())
|
|
40
53
|
.map(
|
|
41
54
|
(v) =>
|
|
@@ -44,6 +57,14 @@ export function prefsHosts(home) {
|
|
|
44
57
|
.replace(/^[a-z]+:\/\//i, "")
|
|
45
58
|
.split("/")[0],
|
|
46
59
|
);
|
|
60
|
+
const routing = prefs?.global?.modelRouting;
|
|
61
|
+
const free =
|
|
62
|
+
routing?.enabled === true && routing?.free?.enabled === true
|
|
63
|
+
? routing.free.allowTraining === true
|
|
64
|
+
? FREE_HOSTS_ANY
|
|
65
|
+
: FREE_HOSTS_NO_TRAINING
|
|
66
|
+
: [];
|
|
67
|
+
return [...own, ...free];
|
|
47
68
|
} catch {
|
|
48
69
|
return [];
|
|
49
70
|
}
|
|
@@ -81,6 +81,21 @@ Selects extra repos the pipeline may touch beyond the primary repo(s) - typica
|
|
|
81
81
|
| node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE"
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
6. **Product map (once per project, attended only).** If the project has never
|
|
85
|
+
answered, offer to map the product it belongs to, so Phase 1 can read the
|
|
86
|
+
other repositories of it:
|
|
87
|
+
```bash
|
|
88
|
+
PRIMARY_REMOTE=$(git -C "$REPO_PATH" remote get-url origin)
|
|
89
|
+
node $HOME/.claude/scripts/product-context.mjs needs-mapping --project "$PRIMARY_REPO_NAME" --developed "$PRIMARY_REMOTE"
|
|
90
|
+
```
|
|
91
|
+
Exit 3 = already answered, or the remote is already in a product: skip. Exit
|
|
92
|
+
0: list candidates with `repo-cache.sh` and `product-context.mjs suggest`
|
|
93
|
+
(commands in the contract). One multi-select of the suggestions (each with a stack the user sets; no
|
|
94
|
+
stack is guessed from a name), plus "not part of a product". Confirmed →
|
|
95
|
+
`product-context.mjs learn`; declined → `product-context.mjs decline`, which
|
|
96
|
+
stops the offer for good. Autopilot skips this step and uses the map as it
|
|
97
|
+
is. Contract: `$HOME/.claude/multi-agent-refs/features/product-context.md`.
|
|
98
|
+
|
|
84
99
|
## Web repos - `webRepos`
|
|
85
100
|
|
|
86
101
|
Web repos are rarely submodules of the app repo, so submodule detection never finds them. `prefs.projects[<key>].webRepos[]` (or the pre-rename `frontendRepos[]`) is the primary source for them and is merged into the candidate list alongside the detected submodules. Only `/multi-agent:analysis` used to read this key, which meant the pipeline's own dev-context picker could not offer a web repo at all.
|
|
@@ -27,7 +27,11 @@ For each entry in `state.analysisSpec.contextLinks[]`, fan out by `type`. Entrie
|
|
|
27
27
|
2. Surface a one-line warning to the user during Phase 1 progress output: `WARN: Confluence requires browser auth for <url>. Falling back to repo evidence; add the page text manually to /tmp/analysis-paste.md and re-run if needed.`
|
|
28
28
|
3. Continue Phase 1 with remaining evidence sources rather than aborting.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**Long sources:** with the free chain on, summarize long confluence/document/jira/generic-doc texts via `llm_delegate` first (`features/free-models.md`).
|
|
31
|
+
|
|
32
|
+
For each repo in `state.analysisSpec.repos[]`, run parallel greps (lightweight surface scan; the deeper 13-bucket extraction happens in Phase 1b).
|
|
33
|
+
|
|
34
|
+
Other repos are read at their latest branch via `repo_*` (`features/product-context.md`), shas in `evidence.repoShas[]`.
|
|
31
35
|
|
|
32
36
|
```bash
|
|
33
37
|
# Localization
|
|
@@ -202,9 +206,9 @@ Phase 2 Section 20 Risks reads this list and emits one open question per entry.
|
|
|
202
206
|
|
|
203
207
|
**Fallback source**: when `confidence == "none"` AND `evidence.standards[]` does not contain an explicit rule for that field, the renderer reads `$HOME/.claude/multi-agent-refs/conventions-defaults.md` and applies the platform default.
|
|
204
208
|
|
|
205
|
-
**Caching (Locked 26)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || options.redesign || hash(evidence.repoEvidence) || hash(evidence.conventions))`. `options.redesign` is
|
|
209
|
+
**Caching (Locked 26)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || options.redesign || evidence.repoShas || hash(evidence.repoEvidence) || hash(evidence.conventions))`. `options.redesign` is an input so a redesign never reuses a normal run's cache, which would skip Phase 1b and render an empty current-behaviour table. Cache key on disk: `/tmp/multi-agent-analysis-cache/<digest>.json` with mtime <= 24h. Cache hit skips Phase 1b and Phase 1c. `--no-cache` flag forces re-run.
|
|
206
210
|
|
|
207
|
-
Phase 1d is
|
|
211
|
+
Phase 1d is not a digest input: community signal changes hourly and would invalidate the cache, re-running the two costly repo phases, every run. A hit reuses yesterday's advisory rows; `--no-cache` refreshes them.
|
|
208
212
|
|
|
209
213
|
### Phase 1d - Outside facts (optional, analyst toolkit)
|
|
210
214
|
|
|
@@ -72,7 +72,7 @@ behind.
|
|
|
72
72
|
23. **Convention extraction mandatory (Phase 1c).** After Phase 1b repo evidence, Phase 1c extracts seven pattern groups (folder structure, class naming, UI state model, test method naming, accessibility identifier, localization key, DI registration) per selected repo via `~/.claude/lib/extract-conventions.sh`. Output lands in `state.analysisSpec.evidence.conventions[<repo>]` with confidence levels (high / medium / low / none) and evidence file citations.
|
|
73
73
|
24. **Pass B cell footnote mandatory.** Every cell Pass B fills in Section 13 (Architecture Plan concept table) and any other per-platform projection carries a footnote of the form `^[<convention-key> <confidence>: <evidence-source>]` pointing back to Phase 1c output. Cells with `confidence: low | none` cite `conventions-defaults.md:C<n>-<platform>` and emit a row in Section 20 Risks ("convention fallback applied"). Footnote-less cells fail the dispatch gate.
|
|
74
74
|
25. **Pass B preview before render.** Phase 2a presents the resolved convention table to the user before Phase 2b emits any platform file. The user can approve, override individual cells, or cancel. Empty answers do not imply consent (`feedback_no-inferred-defaults-from-empty-answer`); the picker re-asks on empty submit.
|
|
75
|
-
26. **Evidence digest caches Phase 1b and 1c.** `evidence_digest = sha256(featureName || sorted(platforms) || repoEvidence.summary || conventions.summary)`. When the same feature name is invoked again against the same set of repos and the digest matches, Phase 1b and 1c are skipped and the cached `evidence.repoEvidence` / `evidence.conventions` is reused. Cache TTL is 24 hours; manual invalidation via `--no-cache` flag.
|
|
75
|
+
26. **Evidence digest caches Phase 1b and 1c.** `evidence_digest = sha256(featureName || sorted(platforms) || repoShas || repoEvidence.summary || conventions.summary)`. When the same feature name is invoked again against the same set of repos and the digest matches, Phase 1b and 1c are skipped and the cached `evidence.repoEvidence` / `evidence.conventions` is reused. Cache TTL is 24 hours; manual invalidation via `--no-cache` flag.
|
|
76
76
|
27. **SwiftUI Preview block mandatory (iOS projection, SwiftUI only).** When the iOS file is produced AND the affected view is a SwiftUI view (detected via `import SwiftUI` + `: View` protocol conformance in `evidence.repoEvidence[<repo>].buckets.uiComponents`), Section 13.6 renders a Preview block table covering at minimum: canonical default (LTR Light), Dark, RTL, Dynamic Type accessibilityLarge, and one error variant. Loading state and edge-case variants are added when distinct from canonical. UIKit-only features (no SwiftUI view artefact) drop Section 13.6 with note `(N/A: UIKit-only feature)`. Preview macro convention (`#Preview` for Swift 5.9+ vs legacy `PreviewProvider`) is read from `evidence.conventions[<repo>].previewMacro`. Each Preview variant listed in Section 13.6 must have a matching row in Section 15.2 Snapshot Tests; a Preview without a snapshot row triggers a Section 20 Risk.
|
|
77
77
|
28. **Variant usage explicit and bounded.** Section 6 inventory rows list which variants this feature consumes per component (concrete enum case + bool value). New Section 6.X (Variant Usage Matrix) catalogues the full variant axis vs. used subset with a rationale per excluded variant. Sections 13.6 (Preview) and 15.2 (Snapshot) cover only the used subset; expanding the variant set requires updating Section 6.X first.
|
|
78
78
|
29. **Analysis as self-contained design bridge - no MCP outside analysis phase (BLOCKING, pipeline-wide).** The analysis document is the sole design source for every downstream phase. After Phase 1 of `/multi-agent:analysis` produces `analysis/<feature>-<platform>.md`, Phase 1 Plan, Phase 2 Dev, Phase 3 Review, Phase 4 Commit, and Phase 5 Report consume only the analysis document plus repo Code Connect mappings (`*.figma.swift` / `*.figma.kt`). Calling `mcp__claude_ai_Figma__*`, hitting `api.figma.com`, or fetching a `figma.com/design/...` URL during Phase 2+ is a violation. Applies to every mode that runs Phase 2+ (`/multi-agent` and `/multi-agent:autopilot`), in either workspace. Hard requirement (v9.0.0): Phase 2 Pre-item and Phase 3 Pre-item (BLOCKING) abort the run when the analysis document is missing. Memory: `[[mcp-only-in-analysis]]`. Generic rule rationale and access matrix: see `$HOME/.claude/rules/figma-pipeline.md` "MUST: No MCP outside analysis phase".
|
|
@@ -115,6 +115,8 @@ it does not. Contracts: `features/exploratory-testing.md`,
|
|
|
115
115
|
| Web states and locators | `web_route`, `web_network_state`, `web_clock`, `web_dialog`, `web_upload`, `web_drag`, `web_hover`, `web_fill_form`, `web_find`, `web_generate_locator`, `web_trace`; `request_id` on `web_network`; `locator` on `web_click` / `web_type` | ≥ v3.20.0 |
|
|
116
116
|
| Document fetch and crawl | `web_scrape` (formats, cache, `change_tracking`), `url` / `sitemap` / `include_paths` / `llms_txt` on `web_crawl` and `web_map` | ≥ v3.20.0 |
|
|
117
117
|
| Research modes and threads | `mode`, `recency`, `domains`, `sources`, `thread_id` on `research_ask`; `arxiv` and `fetch_content` on `research_search` | ≥ v3.20.0 |
|
|
118
|
+
| Free-model delegation | `llm_delegate`, `llm_providers`; `ESCALATE_TO_CLAUDE`; the `multi-agent-toolkit-llm` CLI (`delegate`, `providers`, `probe`, `usage`) | ≥ v3.21.0 |
|
|
119
|
+
| Related repositories | `repo_resolve`, `repo_grep`, `repo_read`, `repo_ls`, `repo_log`, `repo_find_keys`; `stale`, `not_found_at_sha` (`features/product-context.md`) | ≥ v3.22.1 |
|
|
118
120
|
|
|
119
121
|
---
|
|
120
122
|
|
|
@@ -270,9 +272,13 @@ happen.
|
|
|
270
272
|
The routing trio (`route-on`, `route-off`, `route-status`) has no such split -
|
|
271
273
|
it writes `prefs.global.modelRouting` identically everywhere. Its honest limit is
|
|
272
274
|
different and applies on every host: a subagent cannot be dispatched to a
|
|
273
|
-
non-Anthropic model, because subagent dispatch belongs to the host.
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
non-Anthropic model, because subagent dispatch belongs to the host. The `free`
|
|
276
|
+
rung (`route-on --free`) is reachable only where the pipeline makes the call
|
|
277
|
+
itself: `bulk-read.sh` and the `delegate` call site, where a skill asks
|
|
278
|
+
`model-dispatch.sh delegate --default host` and calls `llm_delegate` on `free`.
|
|
279
|
+
The toolkit and its CLI are the same on all three hosts, so the free chain
|
|
280
|
+
behaves identically everywhere. `route-status` prints the limit on every run.
|
|
281
|
+
Contract: `features/free-models.md`.
|
|
276
282
|
|
|
277
283
|
## 3. Frontmatter Transform Rules (Claude ↔ Copilot)
|
|
278
284
|
|