@mmerterden/multi-agent-pipeline 20.20.0 → 20.21.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 +25 -0
- package/README.md +24 -13
- package/README.tr.md +21 -10
- package/docs/ecosystem.md +6 -5
- package/docs/facts.json +5 -4
- package/docs/features.md +1 -0
- package/install/catalog-history.json +1 -1
- package/manifest.json +42 -39
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -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 +2 -1
- 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/analysis/evidence.md +3 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +8 -3
- 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/unattended-security.md +4 -2
- package/pipeline/multi-agent-refs/setup/free-models.md +22 -0
- package/pipeline/preferences-template.json +1 -0
- package/pipeline/schemas/prefs.schema.json +10 -1
- package/pipeline/schemas/route-config.schema.json +33 -8
- package/pipeline/schemas/secret-patterns.json +14 -0
- package/pipeline/scripts/bulk-read.sh +98 -12
- package/pipeline/scripts/cost-table.json +9 -1
- package/pipeline/scripts/doctor.mjs +100 -0
- package/pipeline/scripts/pre-commit-check.sh +1 -1
- package/pipeline/scripts/unattended_policy.py +20 -0
- package/pipeline/skills/.skill-manifest.json +5 -5
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -0
- 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 +1 -0
|
@@ -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
|
}
|
|
@@ -27,6 +27,8 @@ 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
|
+
**Long sources:** with the free chain on, summarize long confluence/document/jira/generic-doc texts via `llm_delegate` first (`features/free-models.md`).
|
|
31
|
+
|
|
30
32
|
For each repo in `state.analysisSpec.repos[]`, run parallel greps (lightweight surface scan; the deeper 13-bucket extraction happens in Phase 1b):
|
|
31
33
|
|
|
32
34
|
```bash
|
|
@@ -204,7 +206,7 @@ Phase 2 Section 20 Risks reads this list and emits one open question per entry.
|
|
|
204
206
|
|
|
205
207
|
**Caching (Locked 26)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || options.redesign || hash(evidence.repoEvidence) || hash(evidence.conventions))`. `options.redesign` is a digest input: without it a redesign within a day of a normal run on the same feature reuses that cache, skips Phase 1b, and renders an empty current-behaviour table every redesign check then passes over. 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
208
|
|
|
207
|
-
Phase 1d is
|
|
209
|
+
Phase 1d is not a digest input: community signal changes hourly, so it would invalidate the cache every run and re-run the two costly repo phases the cache skips. A cache hit reuses yesterday's signal rows, the right trade for an advisory tier; `--no-cache` refreshes them.
|
|
208
210
|
|
|
209
211
|
### Phase 1d - Outside facts (optional, analyst toolkit)
|
|
210
212
|
|
|
@@ -115,6 +115,7 @@ 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 |
|
|
118
119
|
|
|
119
120
|
---
|
|
120
121
|
|
|
@@ -270,9 +271,13 @@ happen.
|
|
|
270
271
|
The routing trio (`route-on`, `route-off`, `route-status`) has no such split -
|
|
271
272
|
it writes `prefs.global.modelRouting` identically everywhere. Its honest limit is
|
|
272
273
|
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
|
-
|
|
274
|
+
non-Anthropic model, because subagent dispatch belongs to the host. The `free`
|
|
275
|
+
rung (`route-on --free`) is reachable only where the pipeline makes the call
|
|
276
|
+
itself: `bulk-read.sh` and the `delegate` call site, where a skill asks
|
|
277
|
+
`model-dispatch.sh delegate --default host` and calls `llm_delegate` on `free`.
|
|
278
|
+
The toolkit and its CLI are the same on all three hosts, so the free chain
|
|
279
|
+
behaves identically everywhere. `route-status` prints the limit on every run.
|
|
280
|
+
Contract: `features/free-models.md`.
|
|
276
281
|
|
|
277
282
|
## 3. Frontmatter Transform Rules (Claude ↔ Copilot)
|
|
278
283
|
|
|
@@ -303,6 +303,17 @@ other outcome (`redirected`, `rejected`, `not-accepted`, `failed`, `skipped`),
|
|
|
303
303
|
with the status code or reason and the step that fixes it. The emitter runs
|
|
304
304
|
detached from phase-tracker with its output discarded, so this record is the
|
|
305
305
|
only surface on which a reporting path that stopped working shows up.
|
|
306
|
+
### free-models
|
|
307
|
+
|
|
308
|
+
Can `route-on --free` actually leave Claude. SKIP while routing or its free
|
|
309
|
+
chain is off. Otherwise it runs the toolkit's `multi-agent-toolkit-llm
|
|
310
|
+
providers` through `lib/toolkit-llm.sh`: WARN when the CLI cannot run, or when
|
|
311
|
+
no provider is both configured and allowed by the data policy
|
|
312
|
+
(`modelRouting.free.allowTraining`), because every free task then escalates to
|
|
313
|
+
Claude while the user believes it does not. `--probe` also sends each
|
|
314
|
+
configured provider one tiny request and WARNs when every usable one fails it
|
|
315
|
+
(a revoked key, an exhausted quota). `--explain` lists each provider with its
|
|
316
|
+
key source, never a value.
|
|
306
317
|
### xcode-skills
|
|
307
318
|
|
|
308
319
|
Is Apple's `xcode-integration` plugin, which Xcode 27.1 and later materialize
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Free models: the `free` rung and the `delegate` call site
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Turning it on](#turning-it-on)
|
|
5
|
+
- [The two call sites](#the-two-call-sites)
|
|
6
|
+
- [What `delegate` may hand over](#what-delegate-may-hand-over)
|
|
7
|
+
- [The contract with the answer](#the-contract-with-the-answer)
|
|
8
|
+
- [Unattended runs](#unattended-runs)
|
|
9
|
+
- [Requirements](#requirements)
|
|
10
|
+
<!-- /toc -->
|
|
11
|
+
|
|
12
|
+
Layer 2 of model routing. With `/multi-agent:route-on --free`, simple text work
|
|
13
|
+
that the pipeline does itself goes to the multi-agent-toolkit's verified
|
|
14
|
+
free-model chain instead of a Claude rung. Decisions, plans, code, reviews,
|
|
15
|
+
triage and security judgements never do.
|
|
16
|
+
|
|
17
|
+
Off by default. Nothing here changes a run until routing and its free chain are
|
|
18
|
+
both on.
|
|
19
|
+
|
|
20
|
+
## Turning it on
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bash "$HOME/.claude/lib/route-state.sh" on --free # Groq and a local Ollama only
|
|
24
|
+
bash "$HOME/.claude/lib/route-state.sh" on --free --allow-training # also Mistral, Gemini, OpenRouter free, Z.AI
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`--free` sets `modelRouting.free.enabled`, adds `bulk-read` and `delegate` to
|
|
28
|
+
`scope`, and lists which providers are usable. One free key is enough to start:
|
|
29
|
+
Groq's (<https://console.groq.com/keys>), stored with `/multi-agent:setup` or
|
|
30
|
+
`credential-store.sh set llm-groq -`, or exported as `GROQ_API_KEY` for the
|
|
31
|
+
server. `/multi-agent:route-status` reports the last seven days: tasks answered
|
|
32
|
+
free and verified, tasks handed back to Claude, and the tokens kept off Claude.
|
|
33
|
+
`/multi-agent:doctor` warns when the switch is on and no provider is usable;
|
|
34
|
+
`--probe` sends each provider one tiny request.
|
|
35
|
+
|
|
36
|
+
**Data policy.** `allowTraining: false` (the default) keeps text to providers
|
|
37
|
+
whose free tier does not train on prompts (Groq) and a local Ollama.
|
|
38
|
+
`--allow-training` adds free tiers that may train on prompts or whose data use
|
|
39
|
+
is not stated. Corporate source, tickets and logs belong under the default.
|
|
40
|
+
|
|
41
|
+
## The two call sites
|
|
42
|
+
|
|
43
|
+
| Call site | Who calls | What goes free | Fallback |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `bulk-read` | `bulk-read.sh`, when the read gate diverts a large file | the line-numbered summary of one file, validated against `schemas/bulk-read-output.schema.json` and the file's line count | `claude -p --model <bulkRead.model or haiku>` |
|
|
46
|
+
| `delegate` | a skill or phase, through the `llm_delegate` MCP tool | one of the simple tasks below | the session does the task itself |
|
|
47
|
+
|
|
48
|
+
Both ask the router first, so the policy has one implementation:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bash "$HOME/.claude/lib/model-dispatch.sh" delegate --default host # prints `free` or `host`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`free` means call `llm_delegate`; anything else means do the task in-session as
|
|
55
|
+
before. A subagent is dispatched by the host, so the router never answers `free`
|
|
56
|
+
for one.
|
|
57
|
+
|
|
58
|
+
## What `delegate` may hand over
|
|
59
|
+
|
|
60
|
+
Only tasks whose answer can be checked against their own source:
|
|
61
|
+
|
|
62
|
+
| Task | `llm_delegate` | Development | Analysis |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| Summarize a long text | `task: summarize` | build and test logs, crash logs, a long file the plan only needs the shape of | Confluence pages, Swagger descriptions, Jira descriptions and comment threads, `generic-doc` links fetched through `web_scrape`, research documents |
|
|
65
|
+
| Pull named fields out of a text | `task: extract` with a `schema` | ticket keys, endpoints, error codes, failing test names from a log | acceptance criteria, endpoints and field lists, dates and owners from a document |
|
|
66
|
+
| Pick one label | `task: classify` with `labels` | commit type from a diff summary, a log line's severity | issue type, affected platform, a comment's intent |
|
|
67
|
+
| Translate a draft | `task: translate` | localization drafts for review (the resources repo stays the contract) | a source document into the output language |
|
|
68
|
+
| Write a draft from a source | `task: draft` | a commit body or PR description draft from the diff summary, then the humanizer | a section draft the analysis author then edits |
|
|
69
|
+
|
|
70
|
+
In analysis Phase 1, a fetched `confluence`, `document`, `generic-doc` or
|
|
71
|
+
`jira` text longer than about 8,000 characters is summarized before it is
|
|
72
|
+
reasoned over: ask the router once per run, then `extract` with a schema for
|
|
73
|
+
the fields a section needs (acceptance criteria, endpoints, field lists, dates,
|
|
74
|
+
owners) and `summarize` for the rest. The verified answer is the working copy
|
|
75
|
+
for drafting; the raw fetch stays in its evidence bucket and Section 21 still
|
|
76
|
+
cites the source. `binding: true` standards are never delegated: their wording
|
|
77
|
+
is the requirement.
|
|
78
|
+
|
|
79
|
+
Never handed over: deciding what to build, the plan, any code or diff, review
|
|
80
|
+
findings and triage, security or licence judgements, anything written to a
|
|
81
|
+
ticket, PR or channel without the session reading it first.
|
|
82
|
+
|
|
83
|
+
## The contract with the answer
|
|
84
|
+
|
|
85
|
+
- A verified answer is still a third-party model's text. It comes back fenced
|
|
86
|
+
as untrusted; the session reads it as data and checks it against the plan
|
|
87
|
+
like any other input.
|
|
88
|
+
- `ESCALATE_TO_CLAUDE` is not an error. It lists a reason per provider; the
|
|
89
|
+
session does the task itself and the run continues. Nothing is retried in a
|
|
90
|
+
loop and no weaker answer is substituted.
|
|
91
|
+
- The toolkit scrubs key-shaped strings before anything leaves the machine.
|
|
92
|
+
Text that must not leave the machine at all (credentials, personal data,
|
|
93
|
+
customer records) is not delegated, whatever the policy says.
|
|
94
|
+
|
|
95
|
+
## Unattended runs
|
|
96
|
+
|
|
97
|
+
`llm_delegate` runs in the MCP server, outside the run's OS sandbox, and reads
|
|
98
|
+
provider keys from the credential store; agent-guard allows it only while
|
|
99
|
+
`route-on --free` is on. `bulk-read.sh` runs inside the sandbox, which cannot
|
|
100
|
+
read the Keychain: it uses the free chain only when the provider key is in the
|
|
101
|
+
run's environment, keeps the toolkit's quota and usage files under
|
|
102
|
+
`~/.multi-agent-unattended/toolkit-index`, and reads on haiku otherwise. The
|
|
103
|
+
installer adds the allowed provider hosts to the sandbox network list
|
|
104
|
+
(`lib/unattended-profile-check.mjs` `prefsHosts`) when the free chain is on;
|
|
105
|
+
re-run `install --unattended` after turning it on.
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
`@mmerterden/multi-agent-toolkit-mcp` ≥ v3.21.0 (`llm_delegate`,
|
|
110
|
+
`llm_providers`, the `multi-agent-toolkit-llm` CLI). `lib/toolkit-llm.sh` finds
|
|
111
|
+
the CLI through `MULTI_AGENT_TOOLKIT_LLM`, then `PATH`, then `npx`.
|
|
@@ -199,8 +199,9 @@ per-phase `model` field already carries the override).
|
|
|
199
199
|
## Routing at dispatch
|
|
200
200
|
|
|
201
201
|
`$HOME/.claude/lib/model-dispatch.sh` turns `prefs.global.modelRouting` into a
|
|
202
|
-
rung.
|
|
203
|
-
`skills/shared/core/multi-agent/SKILL.md`,
|
|
202
|
+
rung. Three call sites ask it: the subagent dispatch contract in
|
|
203
|
+
`skills/shared/core/multi-agent/SKILL.md`, `bulk-read.sh`, and the `delegate`
|
|
204
|
+
call site for simple text tasks (`features/free-models.md`).
|
|
204
205
|
|
|
205
206
|
Routing is consulted only when the phase named no model. A per-dispatch
|
|
206
207
|
`PHASE_MODEL_OVERRIDE` is a decision the phase made for a reason - Phase 3 sets
|
|
@@ -212,17 +213,22 @@ Two limits are enforced by the script rather than written here and hoped for:
|
|
|
212
213
|
- **A rule may not turn the fable rung on.** With `modelFallback.fableEnabled`
|
|
213
214
|
false, a rule preferring `fable` falls past it to the next rung. Otherwise
|
|
214
215
|
`/multi-agent:model off` would have a second door.
|
|
215
|
-
- **
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
216
|
+
- **A subagent stays inside the Anthropic ladder.** A subagent is dispatched by
|
|
217
|
+
the host, so `free` and every other non-Anthropic rung is refused at the
|
|
218
|
+
subagent site and the rule's next rung answers, with the reason on stderr.
|
|
219
|
+
- **`free` only where the pipeline makes the call, and only when switched on.**
|
|
220
|
+
The `free` rung (provider `free-chain`) answers at `bulk-read` and `delegate`
|
|
221
|
+
while `modelRouting.free.enabled` is true (`route-on --free`), ahead of the
|
|
222
|
+
matched rule's rungs. Its callers keep their own default as the fallback,
|
|
223
|
+
because the toolkit answers `ESCALATE_TO_CLAUDE` whenever no free answer
|
|
224
|
+
survives verification. Any other external rung (the `gpt-*` rows) is refused
|
|
225
|
+
everywhere: no call site speaks it.
|
|
226
|
+
|
|
227
|
+
Strategies change the order of the candidate rungs, never the set: `task-fit`
|
|
228
|
+
tries the cheapest first by `cost-table.json` price, and `cost-ceiling` does the
|
|
229
|
+
same once `cost-budget-check.mjs` reports the run at the warn threshold of
|
|
230
|
+
`budgetCeilingUsd`. `recordDecisions: false` is honoured; `true` (the default)
|
|
231
|
+
logs each decision as `model_routing.decision`.
|
|
226
232
|
|
|
227
233
|
The script never fails and never returns an empty rung: a missing preferences
|
|
228
234
|
file, a missing `jq`, unparseable JSON, an out-of-scope call site and a rung
|
|
@@ -231,8 +237,8 @@ router that can die would turn every call site into a place the run can die, for
|
|
|
231
237
|
a feature that ships disabled.
|
|
232
238
|
|
|
233
239
|
`cost-table.json` rows carry a `provider`, which is what tells the two layers
|
|
234
|
-
apart. `smoke-model-dispatch.sh` drives the script and also asserts that the
|
|
235
|
-
|
|
240
|
+
apart. `smoke-model-dispatch.sh` drives the script and also asserts that the call
|
|
241
|
+
sites invoke it: a correct router nothing calls is the same outage with
|
|
236
242
|
better internals.
|
|
237
243
|
|
|
238
244
|
## Non-goals
|
|
@@ -188,7 +188,7 @@ Under the variable it also blocks:
|
|
|
188
188
|
| Publisher scripts | `jira-publish.sh`, `post-pr-review.sh`, `update-issue-progress.sh`, `analysis-jira-write.sh`, `jira-attach.sh`, `md2confluence-v3.py create/update`, `multi-repo-pipeline.sh push/pr`, `keychain-save.sh`, `credential-store.sh get/set/delete`, `keychain.py get/set/delete`, `gate-ledger.mjs append`, `phone-devices.mjs add/revoke/launch` | the same, a credential never lands in the transcript, gates record verdicts through their own in-process calls (`park` stays allowed: it only stops the run, so there is nothing to forge), and a run never enrols a device that could then command this machine |
|
|
189
189
|
| Git redirection | `git remote add/set-url/rename/remove`; the configuration, environment and subcommands above | each changes what a later command runs or where the runner's push goes |
|
|
190
190
|
| Keychain | `security find-*`, `dump-keychain`, `export`, `add-*password`, `delete-*password`, also behind leading flags (`security -q find-generic-password`) | a run needs no credential it did not get from a fetcher |
|
|
191
|
-
| Network | `curl`/`wget`/`WebFetch`/the toolkit's `web_*` tools to a host outside the allowlist, or to a URL built at run time; `web_eval` outright; every step of `agent_run_steps` judged as if called directly (a nested `agent_*` step or an unreadable step list refused); `ios_open_url` / `android_open_url`, `xcrun simctl openurl` and `open` with a web URL outside the allowlist (an app's own deep-link scheme passes; `open` of anything but an allowed URL is refused); the `research_*` tools outright | an injected "fetch this script" is the first step of most attacks, and a browser opened on a crafted URL sends data out in the query |
|
|
191
|
+
| Network | `curl`/`wget`/`WebFetch`/the toolkit's `web_*` tools to a host outside the allowlist, or to a URL built at run time; `web_eval` outright; every step of `agent_run_steps` judged as if called directly (a nested `agent_*` step or an unreadable step list refused); `ios_open_url` / `android_open_url`, `xcrun simctl openurl` and `open` with a web URL outside the allowlist (an app's own deep-link scheme passes; `open` of anything but an allowed URL is refused); the `research_*` tools outright; `llm_delegate` unless `route-on --free` is on, with its `path` checked like any write target | an injected "fetch this script" is the first step of most attacks, and a browser opened on a crafted URL sends data out in the query |
|
|
192
192
|
| Packages | `npm/pnpm/yarn/bun install|add <pkg>`, `npm install-test/it <pkg>`, `update`, global installs (`yarn global` too), `npx` / `npm exec` / `npm x` of a binary not already in `node_modules/.bin` (with no TTY npm installs it without asking), `npx --yes/--package/--call`, `pnpm dlx`, `pnpx`, `yarn dlx`, `bunx`, `bun x`, `uvx`, `uv tool run/install`, `pipx run/install`, `pip install <pkg>`, `uv/poetry add`, `gem install`, `bundle add`, `brew install`, `cargo add/install`, `go get`, `go install pkg@v`, `swift package add-dependency`, `pod update`; `npm ci`, `pod install` and `pip install -r` once the run changed the manifest they restore from | slopsquatting: a model asked to add a dependency can name one that does not exist yet, and someone can register it |
|
|
193
193
|
| Host changes | `crontab` (except `-l`), `launchctl` (except the read verbs), `defaults` writes, `altool`, `notarytool` | a job or preference outlives the run and runs as the operator; an upload is outward |
|
|
194
194
|
| Manifests | Edit, Write or a shell write to `Package.swift`, `Package.resolved`, `package.json`, lockfiles, `Podfile(.lock)`, `build.gradle(.kts)`, `settings.gradle(.kts)`, `libs.versions.toml`, `requirements*.txt`, `pyproject.toml`, `go.mod`/`go.sum`, `Cargo.toml`/`Cargo.lock`, `Gemfile(.lock)` | the same decision, made through the file instead of the command |
|
|
@@ -512,7 +512,9 @@ every process it starts, whatever the command line looks like.
|
|
|
512
512
|
`~/.netrc`, `~/.git-credentials` and `~/.docker/config.json`.
|
|
513
513
|
- `network.strictAllowlist: true` with `allowedDomains` limited to the package
|
|
514
514
|
registries and toolchains the stack adapters need (npm, PyPI, Maven/Google/
|
|
515
|
-
Gradle, CocoaPods, Go, crates, RubyGems, GitHub) plus the prefs hosts
|
|
515
|
+
Gradle, CocoaPods, Go, crates, RubyGems, GitHub) plus the prefs hosts, and,
|
|
516
|
+
while `route-on --free` is on, the free-model hosts its data policy allows
|
|
517
|
+
(`api.groq.com` alone by default; `features/free-models.md`).
|
|
516
518
|
`enableWeakerNetworkIsolation: true` lets Go tools such as `gh` reach the
|
|
517
519
|
macOS trust service to verify TLS; `allowLocalBinding` and
|
|
518
520
|
`allowMachLookup: ["com.apple.coresimulator.*"]` let a dev server and the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Setup Step 3e - free-model keys
|
|
2
|
+
|
|
3
|
+
Runs inside setup Step 3, only when `prefs.global.modelRouting.free.enabled` is
|
|
4
|
+
true (`/multi-agent:route-on --free`). These are the keys the toolkit's
|
|
5
|
+
free-model chain reads; `llm_delegate` and `bulk-read.sh` look for them under
|
|
6
|
+
fixed names, so there is no `keychainMapping` entry.
|
|
7
|
+
|
|
8
|
+
| Key | Provider | Where to get it | Asked |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| `llm-groq` | Groq (free tier does not train on prompts) | https://console.groq.com/keys | always first |
|
|
11
|
+
| `llm-mistral` | Mistral | https://console.mistral.ai/api-keys | only with `modelRouting.free.allowTraining` |
|
|
12
|
+
| `llm-gemini` | Google Gemini | https://aistudio.google.com/apikey | only with `allowTraining` |
|
|
13
|
+
| `llm-openrouter` | OpenRouter, free models only | https://openrouter.ai/keys | only with `allowTraining` |
|
|
14
|
+
| `llm-zai` | Z.AI GLM Flash | https://z.ai/manage-apikey/apikey-list | only with `allowTraining` |
|
|
15
|
+
|
|
16
|
+
Each key is one Token Save Flow pass (`token-save-flow.md`) with the clipboard
|
|
17
|
+
save, `pbpaste | credential-store.sh set "<key>" -`, and no author or host step.
|
|
18
|
+
Skip writes nothing and moves to the next key. After the last pass, run
|
|
19
|
+
`route-state.sh status` so the user sees each saved provider reported ready.
|
|
20
|
+
|
|
21
|
+
A local Ollama needs no key: the user sets `OLLAMA_MODEL` for the server. The
|
|
22
|
+
full contract: `$HOME/.claude/multi-agent-refs/features/free-models.md`.
|
|
@@ -599,10 +599,19 @@
|
|
|
599
599
|
"uniqueItems": true,
|
|
600
600
|
"items": {
|
|
601
601
|
"type": "string",
|
|
602
|
-
"enum": ["subagent", "bulk-read", "research"]
|
|
602
|
+
"enum": ["subagent", "bulk-read", "delegate", "research"]
|
|
603
603
|
},
|
|
604
604
|
"description": "No host-session member, deliberately: rewriting the host's base URL would route the user's whole session, not just this pipeline's calls."
|
|
605
605
|
},
|
|
606
|
+
"free": {
|
|
607
|
+
"type": "object",
|
|
608
|
+
"additionalProperties": false,
|
|
609
|
+
"description": "Layer 2: the verified free-model chain at bulk-read and delegate. Contract: route-config.schema.json.",
|
|
610
|
+
"properties": {
|
|
611
|
+
"enabled": { "type": "boolean", "default": false },
|
|
612
|
+
"allowTraining": { "type": "boolean", "default": false }
|
|
613
|
+
}
|
|
614
|
+
},
|
|
606
615
|
"rules": {
|
|
607
616
|
"type": "array",
|
|
608
617
|
"default": [],
|