@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
|
@@ -74,7 +74,7 @@ DRY = os.environ["TF_DRY"] == "1"
|
|
|
74
74
|
# ones this audit exists to catch. The file system is the fact.
|
|
75
75
|
STACKS = [
|
|
76
76
|
("dotnet", (".csproj", ".fsproj", ".vbproj", ".sln", ".slnx"),
|
|
77
|
-
["bin/", "obj/", "*.user", "TestResults/"]),
|
|
77
|
+
["bin/", "obj/", "*.user", "TestResults/", ".vs/"]),
|
|
78
78
|
("node", ("package.json",), ["dist/", "build/", ".next/", ".nuxt/", ".turbo/"]),
|
|
79
79
|
("python", ("pyproject.toml", "setup.py", "requirements.txt"),
|
|
80
80
|
["__pycache__/", "*.py[cod]", ".venv/", "venv/", ".pytest_cache/", "*.egg-info/"]),
|
|
@@ -83,6 +83,14 @@ STACKS = [
|
|
|
83
83
|
("java", ("pom.xml", "build.gradle", "build.gradle.kts"), ["target/", "build/", ".gradle/"]),
|
|
84
84
|
]
|
|
85
85
|
|
|
86
|
+
# Per-developer IDE state, wanted whatever the stack is. It is not build output, but it is
|
|
87
|
+
# the same harm and this tool is the only thing looking: TfLens tracked three .bin files
|
|
88
|
+
# under .vs/ProjectEvaluation/, one carrying 246 absolute paths out of one developer's
|
|
89
|
+
# machine, while the audit ran on that repo every week and said nothing. `.vscode/` is NOT
|
|
90
|
+
# here -- a team often commits it deliberately, and a rule that fights the owner gets
|
|
91
|
+
# switched off. tests/regression/run.sh tf_014.
|
|
92
|
+
IDE_STATE = [".vs/", ".idea/"]
|
|
93
|
+
|
|
86
94
|
# `package.json` alone does NOT make a repo a Node project, and this exception is
|
|
87
95
|
# load-bearing rather than fussy: verify-phase §1 runs `npm init -y` in EVERY repo
|
|
88
96
|
# to provision Playwright, so a bare package.json sits in essentially every .NET
|
|
@@ -114,14 +122,19 @@ def _is_real_node(root, files):
|
|
|
114
122
|
# `npm init -y` writes exactly one script: a `test` stub that echoes and exits 1.
|
|
115
123
|
return any(k in scripts for k in ("build", "dist", "bundle", "compile"))
|
|
116
124
|
|
|
125
|
+
# A NAME list, never a blanket dot-rule. `not d.startswith(".")` used to prune every
|
|
126
|
+
# dot-directory from the walk -- .vs/, .idea/, .gradle/, .terraform/, .pytest_cache/ --
|
|
127
|
+
# which is close to a complete list of the per-developer state a gitignore audit is FOR.
|
|
128
|
+
# The tool cannot report a folder it never visits, and its silence read exactly like a pass.
|
|
117
129
|
SKIP_DIRS = {".git", "node_modules", ".tfcore", "bin", "obj", "target", "dist",
|
|
118
|
-
"build", ".venv", "venv", "__pycache__", ".next"
|
|
130
|
+
"build", ".venv", "venv", "__pycache__", ".next", ".claude", ".opencode",
|
|
131
|
+
".vscode", ".github", ".husky", ".yarn"}
|
|
119
132
|
|
|
120
133
|
|
|
121
134
|
def detect():
|
|
122
135
|
found = set()
|
|
123
136
|
for root, dirs, files in os.walk(REPO):
|
|
124
|
-
dirs[:] = [d for d in dirs if d not in SKIP_DIRS
|
|
137
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
125
138
|
if root.count(os.sep) - REPO.count(os.sep) > 3:
|
|
126
139
|
dirs[:] = []
|
|
127
140
|
for name, markers, _ in STACKS:
|
|
@@ -154,6 +167,24 @@ def covered(rule, lines):
|
|
|
154
167
|
return False
|
|
155
168
|
|
|
156
169
|
|
|
170
|
+
def child_only_rules(rule, lines):
|
|
171
|
+
"""Rules that ignore something INSIDE `rule` while `rule` itself stays open.
|
|
172
|
+
|
|
173
|
+
This shape is more dangerous than no rule at all, because it looks deliberate:
|
|
174
|
+
TfLens carried `/.vs/App.slnx`, which covers the solution-named subfolder and
|
|
175
|
+
leaves `.vs/ProjectEvaluation/` tracked. The next folder the IDE creates is not
|
|
176
|
+
covered either, and nobody re-reads a line that already mentions the directory."""
|
|
177
|
+
core = rule.strip("/*").rstrip("/")
|
|
178
|
+
out = []
|
|
179
|
+
for l in lines:
|
|
180
|
+
if not l or l.startswith("#"):
|
|
181
|
+
continue
|
|
182
|
+
c = l.lstrip("!").strip().replace("**/", "").strip("/")
|
|
183
|
+
if c != core and (c.startswith(core + "/") or c.startswith(core + "\\")):
|
|
184
|
+
out.append(l.strip())
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
157
188
|
def tracked_paths():
|
|
158
189
|
"""Read .git/index directly. Returns the set of tracked paths, or None when the
|
|
159
190
|
index cannot be read (a bare repo, a worktree file, a version we do not know) —
|
|
@@ -210,8 +241,18 @@ wanted = []
|
|
|
210
241
|
for name, _, rules in STACKS:
|
|
211
242
|
if name in stacks:
|
|
212
243
|
wanted += [(name, r) for r in rules]
|
|
244
|
+
if stacks:
|
|
245
|
+
wanted += [("ide", r) for r in IDE_STATE if not any(r == w for _n, w in wanted)]
|
|
213
246
|
|
|
214
247
|
missing = [(s, r) for s, r in wanted if not covered(r, lines)]
|
|
248
|
+
# A rule naming a child of a directory that is itself unignored: report it separately,
|
|
249
|
+
# because "MISSING .vs/" reads like nobody thought about it when in fact somebody did
|
|
250
|
+
# and got it half right, which is the harder thing to spot in a review.
|
|
251
|
+
half_done = []
|
|
252
|
+
for _s, r in missing:
|
|
253
|
+
kids = child_only_rules(r, lines)
|
|
254
|
+
if kids:
|
|
255
|
+
half_done.append((r, kids))
|
|
215
256
|
|
|
216
257
|
tracked = tracked_paths()
|
|
217
258
|
offenders = {}
|
|
@@ -244,6 +285,12 @@ else:
|
|
|
244
285
|
print(" ⚠ MISSING build-output rules: %s" % names)
|
|
245
286
|
print(" add them with: bash .tfcore/utils/tf-gitignore-audit.sh . --fix")
|
|
246
287
|
|
|
288
|
+
for r, kids in half_done:
|
|
289
|
+
rc |= 1
|
|
290
|
+
print(" ⚠ %s is not ignored, but %s covers a child of it — the parent is open"
|
|
291
|
+
% (r, ", ".join(kids)))
|
|
292
|
+
print(" a rule on a child looks deliberate and leaves every sibling tracked")
|
|
293
|
+
|
|
247
294
|
if tracked is None:
|
|
248
295
|
print(" tracked-file check skipped (no readable .git/index) — rules alone do not")
|
|
249
296
|
print(" prove build output is untracked; re-run where the index is readable")
|
package/.tfcore/utils/tf-goal.sh
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
# tf-goal.sh — unattended GOAL runner / supervisor for TechieFlow (rule: .tfcore/tasks/_yolo-mode.md)
|
|
3
3
|
#
|
|
4
4
|
# Runs a goal headless in YOLO mode and keeps it running until the agent itself
|
|
5
|
-
# declares the goal done — surviving the subscription 5-hour / weekly usage
|
|
6
|
-
#
|
|
7
|
-
# SESSION), crashes, and "I stopped early to ask you something"
|
|
5
|
+
# declares the goal done — surviving the subscription 5-hour / weekly usage limits (moves to the
|
|
6
|
+
# next model in the tier's fallback chain, or sleeps until the stated reset + a buffer when there
|
|
7
|
+
# is none, then RESUMES THE SAME SESSION), crashes, and "I stopped early to ask you something".
|
|
8
8
|
#
|
|
9
9
|
# bash .tfcore/utils/tf-goal.sh [options] <app-dir> "<goal text>"
|
|
10
10
|
# bash .tfcore/utils/tf-goal.sh [options] <app-dir> @goal.md
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
# Options
|
|
13
13
|
# --harness claude|opencode default: claude
|
|
14
14
|
# --model <id> claude: --model; opencode: -m
|
|
15
|
+
# --tier <tier> which routing tier this run belongs to, for the fallback chain
|
|
16
|
+
# (default standard) — .tfcore/routing.yaml `fallbacks:`
|
|
17
|
+
# --no-fallback never switch model on a usage limit; sleep until the reset instead
|
|
18
|
+
# (the behaviour before 2026-09-10)
|
|
15
19
|
# --buffer-min <n> minutes added after a stated limit-reset time (default 15)
|
|
16
20
|
# --probe-min <n> limit hit but NO reset time parseable → fire a one-turn probe every n
|
|
17
21
|
# minutes until the API answers again, then resume (default 15)
|
|
@@ -41,6 +45,14 @@
|
|
|
41
45
|
# goal.log everything the harness printed, all cycles, timestamped
|
|
42
46
|
# goal-done.json the agent's completion sentinel (tf-yolo.sh done [complete|blocked])
|
|
43
47
|
#
|
|
48
|
+
# WHEN A MODEL RUNS OUT (added 2026-09-10, MISS-TechieFlow-20260910-01). A usage limit used to mean
|
|
49
|
+
# one thing: sleep until the reset. It now means: park that model in the per-machine cooldown file
|
|
50
|
+
# (tf-model-pick.sh), ask the tier for the next model in its `fallbacks:` chain, re-bind so the
|
|
51
|
+
# sub-agents move too, and start the next cycle straight away on that model. Only when the whole
|
|
52
|
+
# chain is parked does the supervisor sleep, exactly as before. If the fallback is limited as well
|
|
53
|
+
# the next cycle parks that one too, so an account-wide limit — every Claude model at once — costs
|
|
54
|
+
# one wasted cycle and then behaves as it always did. `--no-fallback` turns all of it off.
|
|
55
|
+
#
|
|
44
56
|
# How the agent is told to finish: the prompt preamble (below) instructs it to run
|
|
45
57
|
# `bash .tfcore/utils/tf-yolo.sh done complete "<summary>"` when the goal is met,
|
|
46
58
|
# or `... done blocked "<why>"` when only the owner can unblock it. A cycle that
|
|
@@ -59,11 +71,14 @@ set -u
|
|
|
59
71
|
|
|
60
72
|
HARNESS="claude"; MODEL=""; BUFFER_MIN=15; DEFAULT_WAIT_MIN=60; MAX_CYCLES=60; IDLE_RETRY=30
|
|
61
73
|
PROBE_MIN=15; PROBE_MAX_H=8; STALL_MIN=15
|
|
74
|
+
TIER="standard"; FALLBACK=1
|
|
62
75
|
RESUME=0; FRESH=0; DRY=0
|
|
63
76
|
while [[ $# -gt 0 ]]; do
|
|
64
77
|
case "$1" in
|
|
65
78
|
--harness) HARNESS="$2"; shift 2 ;;
|
|
66
79
|
--model) MODEL="$2"; shift 2 ;;
|
|
80
|
+
--tier) TIER="$2"; shift 2 ;;
|
|
81
|
+
--no-fallback) FALLBACK=0; shift ;;
|
|
67
82
|
--buffer-min) BUFFER_MIN="$2"; shift 2 ;;
|
|
68
83
|
--default-wait-min) DEFAULT_WAIT_MIN="$2"; shift 2 ;;
|
|
69
84
|
--probe-min) PROBE_MIN="$2"; shift 2 ;;
|
|
@@ -74,7 +89,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
74
89
|
--resume) RESUME=1; shift ;;
|
|
75
90
|
--fresh) FRESH=1; shift ;;
|
|
76
91
|
--dry-run) DRY=1; shift ;;
|
|
77
|
-
-h|--help) sed -n '2,
|
|
92
|
+
-h|--help) sed -n '2,48p' "$0"; exit 0 ;;
|
|
78
93
|
--) shift; break ;;
|
|
79
94
|
-*) echo "unknown option $1" >&2; exit 2 ;;
|
|
80
95
|
*) break ;;
|
|
@@ -88,11 +103,15 @@ fi
|
|
|
88
103
|
APP_DIR="$(cd "$APP_DIR" 2>/dev/null && pwd)" || { echo "no such dir: $1" >&2; exit 2; }
|
|
89
104
|
[[ -d "$APP_DIR/.tfcore" ]] || { echo "$APP_DIR has no .tfcore/ — scaffold it first" >&2; exit 2; }
|
|
90
105
|
case "$HARNESS" in claude|opencode) ;; *) echo "--harness must be claude|opencode" >&2; exit 2 ;; esac
|
|
106
|
+
case "$TIER" in frontier|standard|economy) ;; *) echo "--tier must be frontier|standard|economy" >&2; exit 2 ;; esac
|
|
91
107
|
command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; exit 2; }
|
|
92
108
|
|
|
93
109
|
STATE_DIR="$APP_DIR/.tfcore/.session"; mkdir -p "$STATE_DIR"
|
|
94
110
|
STATE="$STATE_DIR/goal.json"; LOG="$STATE_DIR/goal.log"; DONE="$STATE_DIR/goal-done.json"
|
|
95
111
|
YOLO_SH="$APP_DIR/.tfcore/utils/tf-yolo.sh"
|
|
112
|
+
PICK_SH="$APP_DIR/.tfcore/utils/tf-model-pick.sh"
|
|
113
|
+
BIND_SH="$APP_DIR/.tfcore/utils/tf-routing-bind.sh"
|
|
114
|
+
[[ -f "$PICK_SH" ]] || FALLBACK=0 # a project that predates tf-model-pick.sh keeps the old behaviour
|
|
96
115
|
|
|
97
116
|
if [[ "$GOAL_ARG" == @* ]]; then
|
|
98
117
|
GOAL_FILE="${GOAL_ARG#@}"; [[ -f "$GOAL_FILE" ]] || { echo "goal file not found: $GOAL_FILE" >&2; exit 2; }
|
|
@@ -126,6 +145,10 @@ if [[ $RESUME -eq 1 ]]; then
|
|
|
126
145
|
[[ -f "$STATE" ]] || { echo "--resume: no $STATE" >&2; exit 2; }
|
|
127
146
|
[[ -z "$GOAL" ]] && GOAL="$(state_get goal)"
|
|
128
147
|
HARNESS="$(state_get harness)"; HARNESS="${HARNESS:-claude}"
|
|
148
|
+
# A resume keeps the model a fallback moved to, and the tier its chain came from; an explicit
|
|
149
|
+
# --model on the resume command still wins.
|
|
150
|
+
_t="$(state_get tier)"; [[ -n "$_t" ]] && TIER="$_t"
|
|
151
|
+
_m="$(state_get model)"; [[ -n "$_m" && -z "$MODEL" ]] && MODEL="$_m"
|
|
129
152
|
CYCLE="$(state_get cycle)"; CYCLE="${CYCLE:-0}"
|
|
130
153
|
SESSION_ID="$(state_get session_id)"
|
|
131
154
|
STALLS="$(state_get stalls)"; STALLS="${STALLS:-0}"
|
|
@@ -144,7 +167,7 @@ else
|
|
|
144
167
|
fi
|
|
145
168
|
CYCLE=0; SESSION_ID=""; STALLS=0
|
|
146
169
|
if [[ $DRY -eq 0 ]]; then # a dry run touches nothing: no state, no flag, no log line
|
|
147
|
-
state_set goal "$GOAL" harness "$HARNESS" cycle 0 session_id "" stalls 0 started "$(ts)" last_reason "start"
|
|
170
|
+
state_set goal "$GOAL" harness "$HARNESS" tier "$TIER" model "$MODEL" cycle 0 session_id "" stalls 0 started "$(ts)" last_reason "start"
|
|
148
171
|
rm -f "$DONE"
|
|
149
172
|
fi
|
|
150
173
|
fi
|
|
@@ -346,6 +369,60 @@ print(sid)
|
|
|
346
369
|
PY
|
|
347
370
|
}
|
|
348
371
|
|
|
372
|
+
extract_model() { # the model the harness reported for this cycle, from its own output
|
|
373
|
+
python3 - "$1" <<'PY' 2>/dev/null
|
|
374
|
+
import sys, json, re
|
|
375
|
+
m = ""
|
|
376
|
+
try:
|
|
377
|
+
for line in open(sys.argv[1], errors="replace"):
|
|
378
|
+
line = line.strip()
|
|
379
|
+
if not line.startswith("{") or '"model"' not in line:
|
|
380
|
+
continue
|
|
381
|
+
try: d = json.loads(line)
|
|
382
|
+
except Exception: continue
|
|
383
|
+
if not isinstance(d, dict): continue
|
|
384
|
+
if isinstance(d.get("model"), str) and d["model"]:
|
|
385
|
+
m = d["model"]
|
|
386
|
+
msg = d.get("message")
|
|
387
|
+
if isinstance(msg, dict) and isinstance(msg.get("model"), str) and msg["model"]:
|
|
388
|
+
m = msg["model"]
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
print(m)
|
|
392
|
+
PY
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# Park the model this cycle ran on and move to the next one in its tier. Prints nothing and
|
|
396
|
+
# returns 1 when there is nowhere to go — the caller then sleeps, which is what it always did.
|
|
397
|
+
# $1 = epoch the limit is stated to reset at (or empty when it could not be parsed)
|
|
398
|
+
# $2 = why, for the cooldown record
|
|
399
|
+
try_fallback() {
|
|
400
|
+
[[ $FALLBACK -eq 1 ]] || return 1
|
|
401
|
+
local until="${1:-}" why="${2:-usage limit}" cur next
|
|
402
|
+
cur="$MODEL"
|
|
403
|
+
[[ -z "$cur" ]] && cur="$(extract_model "$OUT")"
|
|
404
|
+
if [[ -z "$cur" ]]; then
|
|
405
|
+
cur="$(TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" pick "$TIER" "$HARNESS" 2>/dev/null | tail -1)"
|
|
406
|
+
[[ "$cur" == inherit ]] && cur=""
|
|
407
|
+
fi
|
|
408
|
+
if [[ -z "$cur" ]]; then
|
|
409
|
+
log "no fallback: nothing here says which model this cycle ran on (no --model, no model id in the output, no routing answer for tier $TIER)"
|
|
410
|
+
return 1
|
|
411
|
+
fi
|
|
412
|
+
[[ -z "$until" ]] && until="probe"
|
|
413
|
+
TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" cooldown "$HARNESS" "$cur" "$until" "$why" >>"$LOG" 2>&1
|
|
414
|
+
next="$(TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" pick "$TIER" "$HARNESS" 2>/dev/null | tail -1)"
|
|
415
|
+
if [[ -z "$next" || "$next" == "inherit" ]]; then
|
|
416
|
+
log "no fallback left: every model in the $TIER chain is on cooldown — waiting for the reset instead"
|
|
417
|
+
return 1
|
|
418
|
+
fi
|
|
419
|
+
MODEL="$next"
|
|
420
|
+
state_set model "$MODEL" fallback_from "$cur" last_reason "fallback:$cur->$MODEL"
|
|
421
|
+
log "FALLBACK: $cur is limited ($why) — the next cycle runs on $MODEL (tier $TIER). Re-binding so the sub-agents move too."
|
|
422
|
+
[[ -f "$BIND_SH" ]] && bash "$BIND_SH" "$APP_DIR" 2>&1 | sed 's/^/ /' | tee -a "$LOG" >&2
|
|
423
|
+
return 0
|
|
424
|
+
}
|
|
425
|
+
|
|
349
426
|
# Fallback when a limit message carries no parseable reset time: fire a one-turn
|
|
350
427
|
# probe every PROBE_MIN minutes until it comes back clean (exit 0 AND no limit
|
|
351
428
|
# wording in its output). Gives up after PROBE_MAX_H hours and resumes anyway.
|
|
@@ -507,7 +584,14 @@ while :; do
|
|
|
507
584
|
CLASS=IDLE; DETAIL="stalled ${STALL_MIN}m"; HOW=""
|
|
508
585
|
PERR="$(opencode_log_error "$CYCLE_START")"
|
|
509
586
|
if [[ -n "$PERR" ]] && grep -qiE "usage limit|monthly|balance|quota|rate limit|429|insufficient|billing|unauthorized|api key" <<<"$PERR"; then
|
|
510
|
-
log "the provider refused the model while the harness printed nothing (cycle $CYCLE): $PERR
|
|
587
|
+
log "the provider refused the model while the harness printed nothing (cycle $CYCLE): $PERR"
|
|
588
|
+
# A monthly limit or an empty balance states no reset time, so the model is parked for the
|
|
589
|
+
# probe window and the run continues on the next model in the tier. Only when the chain is
|
|
590
|
+
# exhausted does this stay what it was before 2026-09-10: the owner's problem, exit 5.
|
|
591
|
+
if try_fallback "" "provider refused the model: ${PERR:0:120}"; then
|
|
592
|
+
STALLS=0; state_set stalls 0; BACKOFF=120; continue
|
|
593
|
+
fi
|
|
594
|
+
log "no other model in the $TIER chain is available — the owner must act (enable balance, wait for the reset, or pick another model); stopping"
|
|
511
595
|
state_set last_reason "provider-limit" ended "$(ts)"
|
|
512
596
|
exit 5
|
|
513
597
|
fi
|
|
@@ -525,6 +609,9 @@ while :; do
|
|
|
525
609
|
fi
|
|
526
610
|
case "$CLASS" in
|
|
527
611
|
LIMIT)
|
|
612
|
+
# Another model in the same tier beats waiting, when there is one.
|
|
613
|
+
if [[ "$HOW" == probe ]]; then _FB_UNTIL=""; else _FB_UNTIL="$DETAIL"; fi
|
|
614
|
+
if try_fallback "$_FB_UNTIL" "usage limit"; then BACKOFF=120; continue; fi
|
|
528
615
|
if [[ "$HOW" == probe ]]; then
|
|
529
616
|
log "USAGE LIMIT hit (cycle $CYCLE) but no reset time could be parsed from the message — probing every ${PROBE_MIN}m until the API answers again (max ${PROBE_MAX_H}h). Tail of the message:"
|
|
530
617
|
tail -c 400 "$OUT" | tr '\n' ' ' | sed 's/^/ /' | tee -a "$LOG" >&2; echo >&2
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""tf-misses-md.py — the readable miss list, docs/<App>-Misses.md (Session 5, 2026-09-07; FR-31).
|
|
3
3
|
|
|
4
|
-
python3 .tfcore/utils/tf-misses-md.py [--root <repo>] [--app <App>] [--
|
|
4
|
+
python3 .tfcore/utils/tf-misses-md.py [--root <repo>] [--app <App>] [--quiet]
|
|
5
5
|
|
|
6
6
|
Rebuilds docs/<App>-Misses.md from docs/metrics/misses.jsonl: one row per `miss` record, newest first,
|
|
7
7
|
in three tables (open, fixed, will not fix), each row holding the miss id, the owning row, when and
|
|
8
8
|
by whom it was found, whose gap it was (the `sort` field, folded from any miss-amend) and the `what`
|
|
9
|
-
sentence.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
sentence. Markdown only — there is no HTML sibling: the miss log is read by agents and by the owner
|
|
10
|
+
in markdown, and tf-render-html.py refuses it (owner, 2026-09-08; an HTML copy nobody opened was
|
|
11
|
+
being re-rendered on every miss in every project). tf-emit.sh calls this after every write to the
|
|
12
|
+
misses stream, so the file is never older than the record; run it by hand once on a project whose
|
|
13
|
+
stream predates it. The file is derived: never edit it, edit nothing, log a miss.
|
|
12
14
|
Exit 0 always (telemetry has no veto); the reason for a skipped write is printed unless --quiet.
|
|
13
15
|
"""
|
|
14
16
|
import datetime
|
|
@@ -16,7 +18,6 @@ import glob
|
|
|
16
18
|
import json
|
|
17
19
|
import os
|
|
18
20
|
import re
|
|
19
|
-
import subprocess
|
|
20
21
|
import sys
|
|
21
22
|
|
|
22
23
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -183,24 +184,20 @@ def main(argv):
|
|
|
183
184
|
old = fh.read()
|
|
184
185
|
except FileNotFoundError:
|
|
185
186
|
pass
|
|
187
|
+
stale = out[:-3] + ".html" # left by a pre-2026-09-08 run, before the miss
|
|
188
|
+
gone = "" # log was ruled an agent document (owner)
|
|
189
|
+
if os.path.isfile(stale):
|
|
190
|
+
os.remove(stale)
|
|
191
|
+
gone = "; removed the stale HTML copy"
|
|
186
192
|
same = lambda s: re.sub(r"\| Updated \| [0-9-]+ \|", "", s) # everything but the date must match
|
|
187
|
-
if old is not None and same(old) == same(text)
|
|
193
|
+
if old is not None and same(old) == same(text):
|
|
188
194
|
if not quiet:
|
|
189
|
-
print(f"tf-misses-md: {rel} unchanged ({n} misses)")
|
|
195
|
+
print(f"tf-misses-md: {rel} unchanged ({n} misses){gone}")
|
|
190
196
|
return 0
|
|
191
197
|
with open(out, "w", encoding="utf-8", newline="\n") as fh:
|
|
192
198
|
fh.write(text)
|
|
193
|
-
html = "skipped"
|
|
194
|
-
if "--no-html" not in argv:
|
|
195
|
-
r = os.path.join(HERE, "tf-render-html.py")
|
|
196
|
-
if os.path.isfile(r):
|
|
197
|
-
try:
|
|
198
|
-
p = subprocess.run([sys.executable, r, out, "--quiet"], capture_output=True, text=True, timeout=120)
|
|
199
|
-
html = "rendered" if p.returncode == 0 else "not rendered (" + (p.stderr or p.stdout).strip().splitlines()[-1:][0][:100] + ")" if (p.stderr or p.stdout).strip() else "not rendered"
|
|
200
|
-
except Exception as e:
|
|
201
|
-
html = f"not rendered ({e})"
|
|
202
199
|
if not quiet:
|
|
203
|
-
print(f"tf-misses-md: wrote {rel} ({n} misses)
|
|
200
|
+
print(f"tf-misses-md: wrote {rel} ({n} misses){gone}")
|
|
204
201
|
except Exception as e:
|
|
205
202
|
if not quiet:
|
|
206
203
|
print(f"tf-misses-md: nothing written ({e})")
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# tf-misses-md.sh — rebuild the readable miss list docs/<App>-Misses.md
|
|
3
|
-
#
|
|
4
|
-
# stream; run it by hand once on a
|
|
2
|
+
# tf-misses-md.sh — rebuild the readable miss list docs/<App>-Misses.md from the misses
|
|
3
|
+
# stream (Session 5, 2026-09-07; FR-31). Markdown only: the miss log is never rendered to
|
|
4
|
+
# HTML. tf-emit.sh runs it after every write to that stream; run it by hand once on a
|
|
5
|
+
# project whose stream is older than this script.
|
|
5
6
|
#
|
|
6
|
-
# bash .tfcore/utils/tf-misses-md.sh [--root <repo>] [--app <App>] [--
|
|
7
|
+
# bash .tfcore/utils/tf-misses-md.sh [--root <repo>] [--app <App>] [--quiet]
|
|
7
8
|
#
|
|
8
9
|
# Exit 0 always. Details: tf-misses-md.py.
|
|
9
10
|
set -u
|