@techierathore/techieflow 1.0.3 → 1.0.4
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/.claude/commands/TechieFlow/agents/analyst.md +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +2 -2
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +2 -2
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +57 -1
- package/.tfcore/telemetry/tf-metrics.sh +416 -19
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-build-list.py +23 -3
- package/.tfcore/utils/tf-build-list.sh +3 -2
- package/.tfcore/utils/tf-doc-check.py +106 -4
- package/.tfcore/utils/tf-emit.sh +353 -16
- package/.tfcore/utils/tf-fix-close.sh +34 -2
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +93 -6
- package/.tfcore/utils/tf-misses-md.py +14 -17
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-phase.sh +18 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +6 -1
- package/.tfcore/utils/tf-status-facts.py +61 -12
- package/.tfcore/utils/tf-verify-screens.mjs +85 -13
- package/.tfcore/utils/tf-verify-screens.sh +12 -3
- package/.tfcore/utils/tf-verify-tests.sh +60 -17
- package/.tfcore/utils/tf-verify-verdict.py +11 -2
- package/README.md +15 -5
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-model-pick.sh — which model should this tier run on RIGHT NOW, and how is it paid for.
|
|
3
|
+
#
|
|
4
|
+
# Two questions the framework could not answer before 2026-09-10, both found by the owner
|
|
5
|
+
# (MISS-TechieFlow-20260910-01 and -02):
|
|
6
|
+
#
|
|
7
|
+
# 1. The tier model's usage limit is reached. What now? -> `pick` walks the tier's
|
|
8
|
+
# `fallbacks:` chain and returns the first model that is not on cooldown. tf-goal.sh puts
|
|
9
|
+
# the limited model on cooldown until its stated reset; tf-routing-bind.sh asks the same
|
|
10
|
+
# question when it writes the harness bindings, so the sub-agents move with the main agent.
|
|
11
|
+
#
|
|
12
|
+
# 2. What does a cost number from this model MEAN? -> `billing` says whether the model is
|
|
13
|
+
# paid for by a flat-fee subscription (marginal dollars are zero, count tokens), by a
|
|
14
|
+
# metered API key (the dollars are real money), or by nothing at all because it runs on
|
|
15
|
+
# this machine. tf-emit.sh stamps the answer on every run record so a report can never sum
|
|
16
|
+
# a subscription's zero into a money figure.
|
|
17
|
+
#
|
|
18
|
+
# USAGE
|
|
19
|
+
# tf-model-pick.sh pick <tier> [harness] -> the model id to use now ("inherit" if none)
|
|
20
|
+
# tf-model-pick.sh chain <tier> [harness] -> the whole ordered chain, one per line,
|
|
21
|
+
# each marked ok | cooldown until <time>
|
|
22
|
+
# tf-model-pick.sh cooldown <harness> <model> <until> ["<why>"]
|
|
23
|
+
# -> park a model. <until> is an epoch, or +<n>
|
|
24
|
+
# minutes, or "probe" for the no-reset-time
|
|
25
|
+
# case (parked for TF_MODEL_PROBE_MIN, 60m).
|
|
26
|
+
# tf-model-pick.sh clear [harness] [model] -> unpark; no arguments clears everything
|
|
27
|
+
# tf-model-pick.sh status -> what every tier resolves to, and why
|
|
28
|
+
# tf-model-pick.sh billing <model> [harness] -> mode<TAB>source<TAB>detail
|
|
29
|
+
# tf-model-pick.sh rate <model> [harness] -> input<TAB>output<TAB>cache_read<TAB>cache_write
|
|
30
|
+
# <TAB>source, in US$ per million tokens; exit 1
|
|
31
|
+
# with no output when nothing prices the model
|
|
32
|
+
# tf-model-pick.sh was-limited <harness> <model> <epoch>
|
|
33
|
+
# -> exit 0 if that model was on cooldown then
|
|
34
|
+
#
|
|
35
|
+
# WHERE THE COOLDOWN LIVES. Inside the project, at `.tfcore/.session/model-cooldown.json`, because
|
|
36
|
+
# that is where TechieFlow itself lives: it is installed per project through npm/npx and never at
|
|
37
|
+
# machine level, and the owner has to be able to change it for one project without touching
|
|
38
|
+
# another (owner, 2026-09-10). `cooldown_file:` in routing.yaml moves it — an absolute path, or a
|
|
39
|
+
# path relative to the project root — and TF_MODEL_HEALTH_FILE overrides both for a single run.
|
|
40
|
+
#
|
|
41
|
+
# The cost of keeping it per project, said plainly: a usage limit belongs to an account, so two
|
|
42
|
+
# projects each discover the same limit for themselves, one wasted cycle apiece. Point them at one
|
|
43
|
+
# shared `cooldown_file:` if you would rather they learned it from each other.
|
|
44
|
+
#
|
|
45
|
+
# Expired entries are kept for 30 days so tf-emit.sh can answer "was this run a fallback?" after
|
|
46
|
+
# the fact, then pruned.
|
|
47
|
+
#
|
|
48
|
+
# Fail-soft everywhere: an unreadable file, a missing catalog, a provider nobody recognises — the
|
|
49
|
+
# answer degrades to the tier model and to billing mode "unknown". Routing is declared and
|
|
50
|
+
# observed, never enforced; this script keeps that property.
|
|
51
|
+
|
|
52
|
+
set +e
|
|
53
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
54
|
+
ROOT="$(bash "$SELF_DIR/tf-harness.sh" root)"
|
|
55
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-model-pick: python3 required" >&2; exit 0; }
|
|
56
|
+
|
|
57
|
+
TF_MP_ROOT="$ROOT" python3 - "$@" <<'PY'
|
|
58
|
+
import json, os, sys, time, datetime
|
|
59
|
+
|
|
60
|
+
root = os.environ.get("TF_MP_ROOT") or os.getcwd()
|
|
61
|
+
argv = sys.argv[1:]
|
|
62
|
+
cmd = argv[0] if argv else "status"
|
|
63
|
+
|
|
64
|
+
KEEP_DAYS = 30
|
|
65
|
+
PROBE_MIN = int(os.environ.get("TF_MODEL_PROBE_MIN") or 60)
|
|
66
|
+
LOCAL_PROVIDERS = ("lmstudio", "ollama", "llamacpp", "llama-cpp", "vllm", "localhost", "local")
|
|
67
|
+
# A flat monthly fee whose usage is metered IN DOLLARS against a per-model allowance. OpenCode Go
|
|
68
|
+
# is $10/month with a $15–$60 monthly limit per model and 5-hour / weekly / monthly thresholds
|
|
69
|
+
# (https://opencode.ai/docs/go), so the dollars it reports are ALLOWANCE CONSUMED, not an invoice
|
|
70
|
+
# — and they are the very numbers that produce the limit the fallback chain exists for. An API
|
|
71
|
+
# key alone cannot tell you this; the provider name can, and `billing:` overrides it either way.
|
|
72
|
+
PLAN_PROVIDERS = ("opencode-go",)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def die(msg, code=2):
|
|
76
|
+
sys.stderr.write("tf-model-pick: %s\n" % msg)
|
|
77
|
+
raise SystemExit(code)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def now():
|
|
81
|
+
return int(time.time())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def iso(ep):
|
|
85
|
+
try:
|
|
86
|
+
return datetime.datetime.fromtimestamp(
|
|
87
|
+
int(ep), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
88
|
+
except Exception:
|
|
89
|
+
return str(ep)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --- routing.yaml (the same flat two-space format every other reader uses) --
|
|
93
|
+
def routing():
|
|
94
|
+
cfg = {"enabled": False, "tiers": {}, "fallbacks": {}, "billing": {}, "phases": {},
|
|
95
|
+
"cooldown_file": ""}
|
|
96
|
+
sect = tier = None
|
|
97
|
+
try:
|
|
98
|
+
for line in open(os.path.join(root, ".tfcore", "routing.yaml"), encoding="utf-8"):
|
|
99
|
+
line = line.rstrip("\n")
|
|
100
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
101
|
+
continue
|
|
102
|
+
if not line.startswith(" "):
|
|
103
|
+
key, _, val = line.partition(":")
|
|
104
|
+
sect, tier = key.strip(), None
|
|
105
|
+
if sect == "enabled":
|
|
106
|
+
cfg["enabled"] = val.strip() == "true"
|
|
107
|
+
elif sect == "cooldown_file":
|
|
108
|
+
cfg["cooldown_file"] = val.strip()
|
|
109
|
+
elif sect in ("tiers", "fallbacks"):
|
|
110
|
+
if line.rstrip().endswith(":") and not line.startswith(" "):
|
|
111
|
+
tier = line.strip()[:-1]
|
|
112
|
+
cfg[sect].setdefault(tier, {})
|
|
113
|
+
elif tier and line.startswith(" "):
|
|
114
|
+
k, _, v = line.strip().partition(":")
|
|
115
|
+
cfg[sect][tier][k.strip()] = v.strip()
|
|
116
|
+
elif sect in ("billing", "phases"):
|
|
117
|
+
k, _, v = line.strip().partition(":")
|
|
118
|
+
cfg[sect][k.strip()] = v.strip()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return cfg
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
CFG = routing()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def health_file():
|
|
128
|
+
"""Project first: TechieFlow is installed per project, so its state lives per project too."""
|
|
129
|
+
env = os.environ.get("TF_MODEL_HEALTH_FILE")
|
|
130
|
+
if env:
|
|
131
|
+
return env
|
|
132
|
+
declared = CFG.get("cooldown_file") or ""
|
|
133
|
+
if declared:
|
|
134
|
+
return declared if os.path.isabs(declared) else os.path.join(root, declared)
|
|
135
|
+
return os.path.join(root, ".tfcore", ".session", "model-cooldown.json")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
HEALTH = health_file()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def hkey(harness):
|
|
142
|
+
"""routing.yaml writes the harness column as claude|opencode; callers may say claude-code."""
|
|
143
|
+
return "opencode" if str(harness).startswith("opencode") else "claude"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def norm(model, harness):
|
|
147
|
+
"""The key two names for the same model share.
|
|
148
|
+
|
|
149
|
+
routing.yaml writes Claude models as the aliases a person types — opus, sonnet, haiku — while
|
|
150
|
+
a transcript reports the full id it resolved to, `claude-sonnet-5`. Parking one and looking up
|
|
151
|
+
the other would silently never match, so the family word is the key on Claude. OpenCode ids
|
|
152
|
+
are already exact and are left alone."""
|
|
153
|
+
m = (model or "").strip().lower()
|
|
154
|
+
if hkey(harness) != "claude":
|
|
155
|
+
return m
|
|
156
|
+
for fam in ("opus", "sonnet", "haiku", "fable"):
|
|
157
|
+
if fam in m:
|
|
158
|
+
return fam
|
|
159
|
+
return m
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def chain_for(tier, harness):
|
|
163
|
+
k = hkey(harness)
|
|
164
|
+
out = []
|
|
165
|
+
first = (CFG["tiers"].get(tier) or {}).get(k)
|
|
166
|
+
if first:
|
|
167
|
+
out.append(first)
|
|
168
|
+
for m in (CFG["fallbacks"].get(tier) or {}).get(k, "").split(","):
|
|
169
|
+
m = m.strip()
|
|
170
|
+
if m and m not in out:
|
|
171
|
+
out.append(m)
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --- the cooldown file -----------------------------------------------------
|
|
176
|
+
def load():
|
|
177
|
+
try:
|
|
178
|
+
with open(HEALTH, encoding="utf-8") as fh:
|
|
179
|
+
d = json.load(fh)
|
|
180
|
+
if isinstance(d, dict) and isinstance(d.get("cooldown"), list):
|
|
181
|
+
return d
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
return {"cooldown": []}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def save(d):
|
|
188
|
+
cut = now() - KEEP_DAYS * 86400
|
|
189
|
+
d["cooldown"] = [e for e in d["cooldown"] if int(e.get("until_epoch") or 0) > cut]
|
|
190
|
+
try:
|
|
191
|
+
os.makedirs(os.path.dirname(HEALTH), exist_ok=True)
|
|
192
|
+
tmp = HEALTH + ".tmp"
|
|
193
|
+
with open(tmp, "w", encoding="utf-8", newline="\n") as fh:
|
|
194
|
+
json.dump(d, fh, indent=1)
|
|
195
|
+
os.replace(tmp, HEALTH)
|
|
196
|
+
return True
|
|
197
|
+
except Exception as e:
|
|
198
|
+
sys.stderr.write("tf-model-pick: could not write %s (%s) — cooldown not recorded\n" % (HEALTH, e))
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def parked(d, harness, model, at=None):
|
|
203
|
+
"""The live cooldown entry for this model at time `at`, or None."""
|
|
204
|
+
at = now() if at is None else int(at)
|
|
205
|
+
k = hkey(harness)
|
|
206
|
+
want = norm(model, harness)
|
|
207
|
+
for e in d["cooldown"]:
|
|
208
|
+
if hkey(e.get("harness")) != k or norm(e.get("model"), e.get("harness")) != want:
|
|
209
|
+
continue
|
|
210
|
+
if int(e.get("since_epoch") or 0) <= at < int(e.get("until_epoch") or 0):
|
|
211
|
+
return e
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# --- billing mode ----------------------------------------------------------
|
|
216
|
+
def opencode_auth():
|
|
217
|
+
p = os.environ.get("OPENCODE_AUTH") or os.path.join(
|
|
218
|
+
os.path.expanduser("~"), ".local", "share", "opencode", "auth.json")
|
|
219
|
+
try:
|
|
220
|
+
with open(p, encoding="utf-8") as fh:
|
|
221
|
+
return json.load(fh)
|
|
222
|
+
except Exception:
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def opencode_catalog():
|
|
227
|
+
p = os.environ.get("OPENCODE_MODELS") or os.path.join(
|
|
228
|
+
os.path.expanduser("~"), ".cache", "opencode", "models.json")
|
|
229
|
+
try:
|
|
230
|
+
with open(p, encoding="utf-8") as fh:
|
|
231
|
+
return json.load(fh)
|
|
232
|
+
except Exception:
|
|
233
|
+
return {}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def billing(model, harness):
|
|
237
|
+
"""-> (mode, source, detail). Mode is subscription | metered | local | unknown."""
|
|
238
|
+
if hkey(harness) == "claude":
|
|
239
|
+
declared = (CFG["billing"].get("claude") or "subscription").strip()
|
|
240
|
+
if declared == "auto":
|
|
241
|
+
if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN"):
|
|
242
|
+
return ("metered", "env", "ANTHROPIC_API_KEY is set")
|
|
243
|
+
return ("subscription", "auto", "no API key in the environment")
|
|
244
|
+
if declared in ("subscription", "metered", "local"):
|
|
245
|
+
return (declared, "routing.yaml", "billing.claude")
|
|
246
|
+
return ("unknown", "routing.yaml", "billing.claude is %r" % declared)
|
|
247
|
+
|
|
248
|
+
declared = (CFG["billing"].get("opencode") or "auto").strip()
|
|
249
|
+
if declared in ("subscription", "metered", "local"):
|
|
250
|
+
return (declared, "routing.yaml", "billing.opencode")
|
|
251
|
+
provider = (model or "").split("/")[0]
|
|
252
|
+
if not provider:
|
|
253
|
+
return ("unknown", "none", "no model id")
|
|
254
|
+
# A named provider in `billing:` beats every guess below. This is the escape hatch for a
|
|
255
|
+
# local runtime called something the framework has never heard of, and for a key whose
|
|
256
|
+
# billing the owner knows and OpenCode's auth file does not say.
|
|
257
|
+
override = (CFG["billing"].get(provider) or "").strip()
|
|
258
|
+
if override in ("subscription", "plan", "metered", "local"):
|
|
259
|
+
return (override, "routing.yaml", "billing.%s" % provider)
|
|
260
|
+
if provider in PLAN_PROVIDERS:
|
|
261
|
+
return ("plan", "known-plan", "%s is a monthly plan with a per-model allowance" % provider)
|
|
262
|
+
auth = opencode_auth().get(provider)
|
|
263
|
+
if isinstance(auth, dict):
|
|
264
|
+
t = (auth.get("type") or "").lower()
|
|
265
|
+
if t == "oauth":
|
|
266
|
+
return ("subscription", "opencode-auth", "%s signed in with OAuth" % provider)
|
|
267
|
+
if t in ("api", "apikey", "api_key", "wellknown"):
|
|
268
|
+
return ("metered", "opencode-auth", "%s uses an API key" % provider)
|
|
269
|
+
return ("unknown", "opencode-auth", "%s auth type %r" % (provider, t))
|
|
270
|
+
if provider.lower().startswith(LOCAL_PROVIDERS):
|
|
271
|
+
return ("local", "provider-name", "%s runs on this machine" % provider)
|
|
272
|
+
cat = opencode_catalog().get(provider)
|
|
273
|
+
if isinstance(cat, dict):
|
|
274
|
+
# In the catalog but not signed in: it is a hosted, billable provider this machine has
|
|
275
|
+
# simply not authenticated. Saying "metered" would invent a bill; say so instead.
|
|
276
|
+
return ("unknown", "models.dev", "%s is a hosted provider with no credential here" % provider)
|
|
277
|
+
return ("unknown", "none", "provider %s is in neither auth.json nor the catalog" % provider)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# --- the rate card ---------------------------------------------------------
|
|
281
|
+
# What a model would cost at list price, per million tokens. This is a PRICE LIST, not a bill: it
|
|
282
|
+
# is what makes a subscription run comparable with a metered one (owner, 2026-09-10), and it is
|
|
283
|
+
# kept in its own field so that nothing ever overwrites a figure a provider actually measured.
|
|
284
|
+
#
|
|
285
|
+
# Two sources, project first:
|
|
286
|
+
# .tfcore/rate-card.json {"<model id>": {"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}}
|
|
287
|
+
# for a negotiated rate, or a model the catalog has never heard of.
|
|
288
|
+
# models.dev the catalog OpenCode already keeps at ~/.cache/opencode/models.json.
|
|
289
|
+
#
|
|
290
|
+
# Two simplifications, stated rather than hidden: the BASE rate is used, so a provider's
|
|
291
|
+
# long-context surcharge (`context_over_200k`) is not applied — nothing records per-message
|
|
292
|
+
# context length, and guessing it would be worse than the small understatement; and a rate the
|
|
293
|
+
# catalog does not carry produces no number at all rather than a zero.
|
|
294
|
+
def project_rate_card():
|
|
295
|
+
try:
|
|
296
|
+
with open(os.path.join(root, ".tfcore", "rate-card.json"), encoding="utf-8") as fh:
|
|
297
|
+
d = json.load(fh)
|
|
298
|
+
return d if isinstance(d, dict) else {}
|
|
299
|
+
except Exception:
|
|
300
|
+
return {}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def rate(model, harness):
|
|
304
|
+
"""-> (input, output, cache_read, cache_write, source) per 1M tokens, or None."""
|
|
305
|
+
m = (model or "").strip()
|
|
306
|
+
if not m or m.startswith("<"): # "<synthetic>" and friends are not models
|
|
307
|
+
return None
|
|
308
|
+
own = project_rate_card()
|
|
309
|
+
for key in (m, m.split("/")[-1]):
|
|
310
|
+
r = own.get(key)
|
|
311
|
+
if isinstance(r, dict):
|
|
312
|
+
return (float(r.get("input") or 0), float(r.get("output") or 0),
|
|
313
|
+
float(r.get("cache_read") or 0), float(r.get("cache_write") or 0),
|
|
314
|
+
".tfcore/rate-card.json")
|
|
315
|
+
cat = opencode_catalog()
|
|
316
|
+
provider, _, bare = m.partition("/")
|
|
317
|
+
entries = []
|
|
318
|
+
if bare and provider in cat:
|
|
319
|
+
entries = [(provider, cat[provider].get("models", {}).get(bare))]
|
|
320
|
+
else:
|
|
321
|
+
# A bare id — every Claude Code model is one, and an older OpenCode record can be. Look
|
|
322
|
+
# in anthropic first, because that is what a Claude transcript reports; then in the
|
|
323
|
+
# providers this machine is actually signed in to, because those are the ones that could
|
|
324
|
+
# have served it; then everything else alphabetically, so the same id always prices the
|
|
325
|
+
# same way. Two providers list `gpt-5.6-sol` at different rates, and picking the one you
|
|
326
|
+
# are signed in to is the difference between $4 and $5 per million input tokens.
|
|
327
|
+
signed_in = [k for k in sorted(opencode_auth()) if k in cat and k != "anthropic"]
|
|
328
|
+
rest = sorted(k for k in cat if k != "anthropic" and k not in signed_in)
|
|
329
|
+
order = (["anthropic"] if "anthropic" in cat else []) + signed_in + rest
|
|
330
|
+
entries = [(p, cat[p].get("models", {}).get(m)) for p in order]
|
|
331
|
+
for provider, entry in entries:
|
|
332
|
+
if not isinstance(entry, dict):
|
|
333
|
+
continue
|
|
334
|
+
c = entry.get("cost")
|
|
335
|
+
if not isinstance(c, dict):
|
|
336
|
+
continue
|
|
337
|
+
return (float(c.get("input") or 0), float(c.get("output") or 0),
|
|
338
|
+
float(c.get("cache_read") or 0), float(c.get("cache_write") or 0),
|
|
339
|
+
"models.dev/" + provider)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# --- commands --------------------------------------------------------------
|
|
344
|
+
if cmd == "pick":
|
|
345
|
+
if len(argv) < 2:
|
|
346
|
+
die("usage: tf-model-pick.sh pick <tier> [harness]")
|
|
347
|
+
tier, harness = argv[1], (argv[2] if len(argv) > 2 else "claude")
|
|
348
|
+
d = load()
|
|
349
|
+
for m in chain_for(tier, harness):
|
|
350
|
+
if not parked(d, harness, m):
|
|
351
|
+
print(m)
|
|
352
|
+
raise SystemExit(0)
|
|
353
|
+
print("inherit") # every model in the chain is limited — the caller waits
|
|
354
|
+
raise SystemExit(0)
|
|
355
|
+
|
|
356
|
+
if cmd == "chain":
|
|
357
|
+
if len(argv) < 2:
|
|
358
|
+
die("usage: tf-model-pick.sh chain <tier> [harness]")
|
|
359
|
+
tier, harness = argv[1], (argv[2] if len(argv) > 2 else "claude")
|
|
360
|
+
d = load()
|
|
361
|
+
ch = chain_for(tier, harness)
|
|
362
|
+
if not ch:
|
|
363
|
+
print("(no model declared for tier %s on %s)" % (tier, harness))
|
|
364
|
+
raise SystemExit(0)
|
|
365
|
+
for i, m in enumerate(ch):
|
|
366
|
+
e = parked(d, harness, m)
|
|
367
|
+
mark = "cooldown until %s (%s)" % (iso(e["until_epoch"]), e.get("why") or "limited") if e else "ok"
|
|
368
|
+
print("%d\t%s\t%s%s" % (i + 1, m, mark, "" if i else " <- tier model"))
|
|
369
|
+
raise SystemExit(0)
|
|
370
|
+
|
|
371
|
+
if cmd == "cooldown":
|
|
372
|
+
if len(argv) < 4:
|
|
373
|
+
die("usage: tf-model-pick.sh cooldown <harness> <model> <epoch|+minutes|probe> [\"why\"]")
|
|
374
|
+
harness, model, until = argv[1], argv[2], argv[3]
|
|
375
|
+
why = argv[4] if len(argv) > 4 else "usage limit"
|
|
376
|
+
if until == "probe":
|
|
377
|
+
until_ep = now() + PROBE_MIN * 60
|
|
378
|
+
elif until.startswith("+"):
|
|
379
|
+
until_ep = now() + int(float(until[1:])) * 60
|
|
380
|
+
else:
|
|
381
|
+
try:
|
|
382
|
+
until_ep = int(float(until))
|
|
383
|
+
except Exception:
|
|
384
|
+
die("<until> must be an epoch, +<minutes> or probe")
|
|
385
|
+
if until_ep <= now():
|
|
386
|
+
until_ep = now() + 60
|
|
387
|
+
d = load()
|
|
388
|
+
d["cooldown"] = [e for e in d["cooldown"]
|
|
389
|
+
if not (hkey(e.get("harness")) == hkey(harness)
|
|
390
|
+
and norm(e.get("model"), e.get("harness")) == norm(model, harness)
|
|
391
|
+
and int(e.get("until_epoch") or 0) > now())]
|
|
392
|
+
d["cooldown"].append({"harness": hkey(harness), "model": model,
|
|
393
|
+
"since_epoch": now(), "since": iso(now()),
|
|
394
|
+
"until_epoch": until_ep, "until": iso(until_ep),
|
|
395
|
+
"why": why, "app": os.path.basename(root)})
|
|
396
|
+
save(d)
|
|
397
|
+
print("cooldown: %s/%s until %s (%s)" % (hkey(harness), model, iso(until_ep), why))
|
|
398
|
+
raise SystemExit(0)
|
|
399
|
+
|
|
400
|
+
if cmd == "clear":
|
|
401
|
+
d = load()
|
|
402
|
+
before = len(d["cooldown"])
|
|
403
|
+
if len(argv) == 1:
|
|
404
|
+
d["cooldown"] = []
|
|
405
|
+
else:
|
|
406
|
+
harness = argv[1]
|
|
407
|
+
model = argv[2] if len(argv) > 2 else None
|
|
408
|
+
d["cooldown"] = [e for e in d["cooldown"]
|
|
409
|
+
if not (hkey(e.get("harness")) == hkey(harness)
|
|
410
|
+
and (model is None
|
|
411
|
+
or norm(e.get("model"), e.get("harness")) == norm(model, harness))
|
|
412
|
+
and int(e.get("until_epoch") or 0) > now())]
|
|
413
|
+
save(d)
|
|
414
|
+
print("cleared %d cooldown entr(y/ies)" % (before - len(d["cooldown"])))
|
|
415
|
+
raise SystemExit(0)
|
|
416
|
+
|
|
417
|
+
if cmd == "billing":
|
|
418
|
+
if len(argv) < 2:
|
|
419
|
+
die("usage: tf-model-pick.sh billing <model> [harness]")
|
|
420
|
+
model = argv[1]
|
|
421
|
+
harness = argv[2] if len(argv) > 2 else ("opencode" if "/" in model else "claude")
|
|
422
|
+
print("%s\t%s\t%s" % billing(model, harness))
|
|
423
|
+
raise SystemExit(0)
|
|
424
|
+
|
|
425
|
+
if cmd == "rate":
|
|
426
|
+
if len(argv) < 2:
|
|
427
|
+
die("usage: tf-model-pick.sh rate <model> [harness]")
|
|
428
|
+
model = argv[1]
|
|
429
|
+
harness = argv[2] if len(argv) > 2 else ("opencode" if "/" in model else "claude")
|
|
430
|
+
r = rate(model, harness)
|
|
431
|
+
if not r:
|
|
432
|
+
raise SystemExit(1)
|
|
433
|
+
print("%g\t%g\t%g\t%g\t%s" % r)
|
|
434
|
+
raise SystemExit(0)
|
|
435
|
+
|
|
436
|
+
if cmd == "was-limited":
|
|
437
|
+
if len(argv) < 4:
|
|
438
|
+
die("usage: tf-model-pick.sh was-limited <harness> <model> <epoch>")
|
|
439
|
+
raise SystemExit(0 if parked(load(), argv[1], argv[2], argv[3]) else 1)
|
|
440
|
+
|
|
441
|
+
if cmd == "status":
|
|
442
|
+
d = load()
|
|
443
|
+
live = [e for e in d["cooldown"] if int(e.get("until_epoch") or 0) > now()]
|
|
444
|
+
print("Cooldown file: %s" % HEALTH)
|
|
445
|
+
print("Routing: %s" % ("ON" if CFG["enabled"] else "OFF"))
|
|
446
|
+
print()
|
|
447
|
+
for harness in ("claude", "opencode"):
|
|
448
|
+
print("%s:" % harness)
|
|
449
|
+
for tier in ("frontier", "standard", "economy"):
|
|
450
|
+
ch = chain_for(tier, harness)
|
|
451
|
+
if not ch:
|
|
452
|
+
continue
|
|
453
|
+
use = next((m for m in ch if not parked(d, harness, m)), None)
|
|
454
|
+
mode = billing(use, harness)[0] if use else "-"
|
|
455
|
+
skipped = [m for m in ch if parked(d, harness, m)]
|
|
456
|
+
note = " (skipping %s)" % ", ".join(skipped) if skipped else ""
|
|
457
|
+
print(" %-9s -> %-34s [%s]%s" % (tier, use or "inherit (all limited — wait)", mode, note))
|
|
458
|
+
print()
|
|
459
|
+
if live:
|
|
460
|
+
print("On cooldown now:")
|
|
461
|
+
for e in sorted(live, key=lambda x: x["until_epoch"]):
|
|
462
|
+
print(" %-9s %-34s until %s (%s, from %s)"
|
|
463
|
+
% (e.get("harness"), e.get("model"), e.get("until"), e.get("why"), e.get("app")))
|
|
464
|
+
else:
|
|
465
|
+
print("On cooldown now: nothing.")
|
|
466
|
+
print()
|
|
467
|
+
print("Park a model by hand: bash .tfcore/utils/tf-model-pick.sh cooldown opencode <model> +180 \"monthly limit\"")
|
|
468
|
+
print("Unpark: bash .tfcore/utils/tf-model-pick.sh clear opencode <model>")
|
|
469
|
+
raise SystemExit(0)
|
|
470
|
+
|
|
471
|
+
die("unknown command %r — run with no arguments for the usage block" % cmd)
|
|
472
|
+
PY
|
|
473
|
+
rc=$?
|
|
474
|
+
if [[ $rc -eq 2 ]]; then sed -n '17,32p' "$0" | sed 's/^# \{0,1\}//' >&2; fi
|
|
475
|
+
exit $rc
|
|
@@ -51,6 +51,24 @@ case "${1:-}" in
|
|
|
51
51
|
( cd "$ROOT" && bash .tfcore/utils/tf-doc-check.sh --root "$ROOT" --baseline-write "${docs[@]}" 2>/dev/null | tail -1 >&2 ) || true
|
|
52
52
|
fi
|
|
53
53
|
fi
|
|
54
|
+
# The framework's own scripts, against THIS project's real files, before the command
|
|
55
|
+
# does anything (2026-09-09). Nine framework defects reached TfLens by being met
|
|
56
|
+
# mid-build, and each cost a switch to the framework repo and back. Step 0 is the one
|
|
57
|
+
# place every command already passes through, so the check lives here and no task file
|
|
58
|
+
# has to mention it -- including commands added later.
|
|
59
|
+
#
|
|
60
|
+
# It NEVER blocks: a framework defect is reported and the command carries on, exactly
|
|
61
|
+
# as a library gap does. Set TF_SKIP_SELFCHECK=1 to skip it (the self-tests do).
|
|
62
|
+
if [[ "${TF_SKIP_SELFCHECK:-0}" != "1" && -f "$ROOT/.tfcore/utils/tf-selfcheck.sh" ]]; then
|
|
63
|
+
sc="$(cd "$ROOT" && bash .tfcore/utils/tf-selfcheck.sh 2>/dev/null)" || true
|
|
64
|
+
if grep -q '^FAIL' <<<"$sc"; then
|
|
65
|
+
echo "tf-phase: the framework is broken on this project — log these in the feedback file and carry on:" >&2
|
|
66
|
+
grep '^FAIL' <<<"$sc" >&2
|
|
67
|
+
echo "tf-phase: entries to paste: bash .tfcore/utils/tf-selfcheck.sh --report" >&2
|
|
68
|
+
else
|
|
69
|
+
echo "tf-phase: framework self-check clean" >&2
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
54
72
|
;;
|
|
55
73
|
goal)
|
|
56
74
|
# written by tf-goal.sh at the start of a run's first cycle; claimed by the first `start`
|
|
@@ -479,6 +479,22 @@ def is_requirements_checklist(base, raw):
|
|
|
479
479
|
or "SINGLE SOURCE OF TRUTH" in head)
|
|
480
480
|
|
|
481
481
|
|
|
482
|
+
def is_miss_list(base, raw):
|
|
483
|
+
"""Is this THE derived miss log, or an ordinary document about misses?
|
|
484
|
+
|
|
485
|
+
`docs/{AppName}-Misses.md` is rebuilt from `misses.jsonl` on every write and is
|
|
486
|
+
read by agents and by the owner in markdown. Rendering it produced an HTML copy
|
|
487
|
+
nobody opened, re-rendered on every miss, in every project — 20 of them across
|
|
488
|
+
the estate before this refusal existed (owner, 2026-09-08).
|
|
489
|
+
|
|
490
|
+
Identify the document, do not guess from the suffix, exactly as the checklist
|
|
491
|
+
rule does: the name must match AND the content must carry the generator's own
|
|
492
|
+
Source row. A hand-written note called `Release-Misses.md` has neither."""
|
|
493
|
+
if not re.search(r"-Misses\.md$", base, re.I):
|
|
494
|
+
return False
|
|
495
|
+
return "Rewritten by `tf-misses-md.sh`" in raw[:2000]
|
|
496
|
+
|
|
497
|
+
|
|
482
498
|
NEXT_CMD_H2 = re.compile(r"^##\s+Next command to run\s*$", re.I)
|
|
483
499
|
|
|
484
500
|
|
|
@@ -541,6 +557,10 @@ def render(md_path, css, head_js, body_js, out_dir=None):
|
|
|
541
557
|
raise Refused("%s is the requirements checklist — checklists are AI-agent "
|
|
542
558
|
"working documents and are NEVER rendered to HTML "
|
|
543
559
|
"(html-render-shell §0)." % base)
|
|
560
|
+
if is_miss_list(base, raw_probe):
|
|
561
|
+
raise Refused("%s is the derived miss log — it is read in markdown by agents "
|
|
562
|
+
"and by the owner, and is NEVER rendered to HTML "
|
|
563
|
+
"(html-render-shell §0)." % base)
|
|
544
564
|
|
|
545
565
|
raw = raw_probe
|
|
546
566
|
lines = raw.split("\n")
|
|
@@ -41,6 +41,63 @@ root = sys.argv[1]
|
|
|
41
41
|
ryaml = os.path.join(root, ".tfcore", "routing.yaml")
|
|
42
42
|
manifest_path = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
|
|
43
43
|
|
|
44
|
+
# --- migration: a project's routing.yaml is never overwritten -------------
|
|
45
|
+
# `update-framework.sh` leaves a project's own routing.yaml alone on purpose, so a block added to
|
|
46
|
+
# the framework default after the project was scaffolded would never reach it. Both delivery
|
|
47
|
+
# routes run THIS script, so the migration lives here: a missing block is APPENDED, with the
|
|
48
|
+
# Claude aliases filled in (opus/sonnet/haiku exist on every account) and the OpenCode lines left
|
|
49
|
+
# empty, because guessing a model id for a provider this machine may not have would produce a
|
|
50
|
+
# fallback that fails at the moment it is needed. Existing lines are never touched, and a file
|
|
51
|
+
# that already has the block is left exactly as it is.
|
|
52
|
+
MIGRATIONS = [
|
|
53
|
+
("fallbacks:", """
|
|
54
|
+
# Which models this tier drops to while its own is limited, in order, comma-separated.
|
|
55
|
+
# Added by tf-routing-bind.sh. The opencode lines are OpenCode Go models
|
|
56
|
+
# (https://opencode.ai/docs/go); change them if this project uses a different provider —
|
|
57
|
+
# `opencode models` prints what this machine can reach.
|
|
58
|
+
# The "limited until <time>" record is kept in this project, at
|
|
59
|
+
# .tfcore/.session/model-cooldown.json. Uncomment to move it (relative to the project root, or
|
|
60
|
+
# absolute); several projects sharing one file learn about a limit from each other.
|
|
61
|
+
# cooldown_file: .tfcore/.session/model-cooldown.json
|
|
62
|
+
# Full explanation: docs/TechieFlow-Routing-Guide.md §9.
|
|
63
|
+
fallbacks:
|
|
64
|
+
frontier:
|
|
65
|
+
claude: opus, haiku
|
|
66
|
+
opencode: opencode-go/glm-5.3, opencode-go/kimi-k3, openai/gpt-5.6-sol
|
|
67
|
+
standard:
|
|
68
|
+
claude: haiku
|
|
69
|
+
opencode: opencode-go/mimo-v2.5, opencode-go/qwen3.8-max, openai/gpt-5.6-luna
|
|
70
|
+
economy:
|
|
71
|
+
claude: sonnet
|
|
72
|
+
opencode: opencode-go/deepseek-v4-flash, opencode-go/glm-5.3-flash, openai/gpt-5.6-luna
|
|
73
|
+
"""),
|
|
74
|
+
("billing:", """
|
|
75
|
+
# How each harness is paid for, so a cost figure knows what it means (SCHEMA.md §2.5b).
|
|
76
|
+
# subscription = a flat fee, marginal dollars are zero; metered = an API key, real money;
|
|
77
|
+
# local = no bill; auto = work it out per model from OpenCode's own sign-in file.
|
|
78
|
+
# Add a provider by name here to override the guess, e.g. " myserver: local".
|
|
79
|
+
billing:
|
|
80
|
+
claude: subscription
|
|
81
|
+
opencode: auto
|
|
82
|
+
"""),
|
|
83
|
+
]
|
|
84
|
+
try:
|
|
85
|
+
_txt = open(ryaml, encoding="utf-8").read()
|
|
86
|
+
_added = []
|
|
87
|
+
for key, block in MIGRATIONS:
|
|
88
|
+
if not re.search(r"^%s\s*$" % re.escape(key), _txt, re.M):
|
|
89
|
+
if not _txt.endswith("\n"):
|
|
90
|
+
_txt += "\n"
|
|
91
|
+
_txt += block
|
|
92
|
+
_added.append(key.rstrip(":"))
|
|
93
|
+
if _added:
|
|
94
|
+
with open(ryaml, "w", encoding="utf-8", newline="\n") as _fh:
|
|
95
|
+
_fh.write(_txt)
|
|
96
|
+
print("tf-routing-bind: added %s to .tfcore/routing.yaml (nothing existing was changed)"
|
|
97
|
+
% " and ".join(_added))
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
44
101
|
# --- parse routing.yaml (flat format — see its header) --------------------
|
|
45
102
|
cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}}
|
|
46
103
|
sect = tier = None
|
|
@@ -67,8 +124,37 @@ try:
|
|
|
67
124
|
except FileNotFoundError:
|
|
68
125
|
pass # no routing.yaml -> treat as disabled
|
|
69
126
|
|
|
127
|
+
# A tier resolves to its own model UNLESS that model is on cooldown — a usage limit recorded by
|
|
128
|
+
# tf-goal.sh in the per-machine file tf-model-pick.sh owns. Binding the fallback here is what stops
|
|
129
|
+
# the sub-agents failing behind a main agent that already moved (Routing-Guide §8, 2026-09-06).
|
|
130
|
+
_PICK = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
131
|
+
_picked = {}
|
|
132
|
+
|
|
133
|
+
|
|
70
134
|
def model_for(t, harness):
|
|
71
|
-
|
|
135
|
+
declared = cfg["tiers"].get(t, {}).get(harness)
|
|
136
|
+
if not declared or t == "inherit":
|
|
137
|
+
return declared
|
|
138
|
+
if (t, harness) not in _picked:
|
|
139
|
+
got = declared
|
|
140
|
+
if os.path.isfile(_PICK):
|
|
141
|
+
try:
|
|
142
|
+
import subprocess
|
|
143
|
+
out = subprocess.run(["bash", _PICK, "pick", t, harness],
|
|
144
|
+
capture_output=True, text=True, timeout=20)
|
|
145
|
+
cand = (out.stdout or "").strip().splitlines()
|
|
146
|
+
cand = cand[-1].strip() if cand else ""
|
|
147
|
+
if cand and cand != "inherit":
|
|
148
|
+
got = cand
|
|
149
|
+
elif cand == "inherit":
|
|
150
|
+
got = None # every model in the chain is limited: bind nothing, let it wait
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
if got != declared:
|
|
154
|
+
print("tf-routing-bind: tier %s (%s) is on %s — %s is on cooldown"
|
|
155
|
+
% (t, harness, got or "no model: the whole chain is limited", declared))
|
|
156
|
+
_picked[(t, harness)] = got
|
|
157
|
+
return _picked[(t, harness)]
|
|
72
158
|
|
|
73
159
|
# --- remove previously generated artifacts (manifest-driven) --------------
|
|
74
160
|
removed = 0
|
|
@@ -210,6 +210,13 @@ sub = ["%s=%s" % (k, v) for k, v in sorted(cfg["subagents"].items())]
|
|
|
210
210
|
if sub:
|
|
211
211
|
print("Subagents: " + ", ".join(sub))
|
|
212
212
|
print()
|
|
213
|
+
print("When a model is limited (docs/TechieFlow-Routing-Guide.md §9):")
|
|
214
|
+
print(" what each tier would run right now, and what is parked:")
|
|
215
|
+
print(" bash .tfcore/utils/tf-model-pick.sh status")
|
|
216
|
+
print(" the order a tier falls back in: ... chain <tier> <claude|opencode>")
|
|
217
|
+
print(" unpark by hand: ... clear <harness> [model]")
|
|
218
|
+
print(" The supervisor parks a model by itself on a usage limit; tf-goal.sh --no-fallback stops it.")
|
|
219
|
+
print()
|
|
213
220
|
print("Escalation (ADVISORY — you apply it when you launch; nothing switches a model mid-run):")
|
|
214
221
|
if cfg["escalation"]:
|
|
215
222
|
for ph in sorted(cfg["escalation"]):
|