@techierathore/techieflow 1.0.3 → 1.1.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/.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 +27 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- 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 +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +181 -9
- 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/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +27 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- 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 +3 -3
- package/.tfcore/tasks/day1-greenfield.md +3 -3
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +7 -3
- package/.tfcore/tasks/handoff-phase.md +20 -18
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +61 -1
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +435 -21
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- 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/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- 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-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +99 -11
- package/.tfcore/utils/tf-build-list.sh +5 -3
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +242 -10
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +413 -19
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +113 -31
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +105 -18
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +24 -21
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +33 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +80 -2
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +356 -0
- package/.tfcore/utils/tf-split-brd.py +17 -2
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +133 -44
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +254 -43
- package/.tfcore/utils/tf-verify-screens.sh +18 -4
- package/.tfcore/utils/tf-verify-tests.sh +272 -28
- package/.tfcore/utils/tf-verify-verdict.py +59 -11
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +19 -5
- package/docs/TechieFlow-Installation.md +5 -3
- 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
|
@@ -3,21 +3,34 @@
|
|
|
3
3
|
#
|
|
4
4
|
# bash .tfcore/utils/tf-fix-close.sh <App> [--started <ISO>] [--reqs REQ-UI-009,REQ-FN-014]
|
|
5
5
|
# [--subagents trblazeui,builder] [--build pass|fail|not-run] [--files N]
|
|
6
|
+
# bash .tfcore/utils/tf-fix-close.sh <App> --misses MISS-App-20260901-06,MISS-App-20260830-03 \
|
|
7
|
+
# --fix-cmd amend-docs [--verdict Verified]
|
|
8
|
+
#
|
|
9
|
+
# The second form closes misses DIRECTLY, by id, with no checklist row and no verify ledger.
|
|
10
|
+
# It exists because a miss whose deficient artifact is a document is fixed by *amend-docs
|
|
11
|
+
# editing the BRD -- never by the verifier touching a row -- so the row-driven form above can
|
|
12
|
+
# never reach it, and the miss stayed open forever (TF-016). It writes NO run record: the
|
|
13
|
+
# amending command emits its own through the status gate. `fix_cmd: "amend-docs"` was already
|
|
14
|
+
# legal in SCHEMA §5.5.2; only this door was missing.
|
|
6
15
|
#
|
|
7
16
|
# Emits, in this order, through tf-emit.sh only:
|
|
8
17
|
# 1. the fix-issues run record (cmd fix-issues, mode fix, the rows touched) — first, because the
|
|
9
|
-
# miss-fix records below carry no numbers of their own: the emitter copies this run's token window
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
18
|
+
# miss-fix records below carry no numbers of their own: the emitter copies this run's token window.
|
|
19
|
+
# A run the fix chained (the inline verify) keeps its own record, so the fix is recorded in the gaps
|
|
20
|
+
# around it; the first gap carries the rows and the fix's start (TF-052).
|
|
21
|
+
# 2. one miss-fix per row that has an open miss, with verdict_after taken from every verify the fix
|
|
22
|
+
# chained: the gate records written since the fix started, then the ledger docs/.last-verify.json
|
|
23
|
+
# when it belongs to this fix (PASS → Verified; FAIL → FAIL; any other failing verdict → Needs
|
|
24
|
+
# re-verify). A row no verify graded gets no miss-fix and is listed. A row with no open miss is listed,
|
|
13
25
|
# not invented: a fix on a defect nobody logged is a triage gap, so open it first with tf-triage.sh
|
|
14
26
|
# demote or tf-log-miss.sh, then re-run this.
|
|
15
|
-
# --reqs defaults to every row
|
|
27
|
+
# --reqs defaults to every row those verifies graded. The start defaults to the fix's marker, or its
|
|
28
|
+
# "outer" entry when a chained verify replaced it. Telemetry never blocks: refusals are printed, exit 0.
|
|
16
29
|
set -u
|
|
17
30
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
31
|
[[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
|
|
19
32
|
APP="$1"; shift
|
|
20
|
-
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""
|
|
33
|
+
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""; MISSES=""; FIXCMD="fix-issues"; VERDICT=""
|
|
21
34
|
while [[ $# -gt 0 ]]; do
|
|
22
35
|
case "$1" in
|
|
23
36
|
--started) STARTED="${2:-}"; shift 2 ;;
|
|
@@ -25,11 +38,20 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
38
|
--subagents) SUBS="${2:-}"; shift 2 ;;
|
|
26
39
|
--build) BUILD="${2:-}"; shift 2 ;;
|
|
27
40
|
--files) FILES="${2:-}"; shift 2 ;;
|
|
41
|
+
--misses) MISSES="${2:-}"; shift 2 ;;
|
|
42
|
+
--fix-cmd) FIXCMD="${2:-}"; shift 2 ;;
|
|
43
|
+
--verdict) VERDICT="${2:-}"; shift 2 ;;
|
|
28
44
|
*) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
|
|
29
45
|
esac
|
|
30
46
|
done
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
# the fix's start: its own marker, or the "outer" one when a verify it chained has replaced the marker
|
|
48
|
+
[[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | python3 -c "import json,sys
|
|
49
|
+
try: m=json.load(sys.stdin)
|
|
50
|
+
except Exception: m={}
|
|
51
|
+
o=m.get('outer') or {}
|
|
52
|
+
print(m.get('started','') if m.get('cmd')!='verify-phase' or not o.get('started') else o['started'])" 2>/dev/null)"
|
|
53
|
+
TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES" \
|
|
54
|
+
TF_MISSES="$MISSES" TF_FIXCMD="$FIXCMD" TF_VERDICT="$VERDICT" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
|
|
33
55
|
import datetime, json, os, subprocess
|
|
34
56
|
EMIT = os.environ["TF_EMIT"]; app = os.environ["TF_APP"]; started = os.environ["TF_STARTED"]
|
|
35
57
|
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -39,45 +61,105 @@ def emit(stream, rec):
|
|
|
39
61
|
msg = (p.stdout + p.stderr).strip(); ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
|
|
40
62
|
if not ok: print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
|
|
41
63
|
return ok
|
|
64
|
+
# --- close by miss id (the *amend-docs door) -------------------------------------------
|
|
65
|
+
direct = [m.strip() for m in (os.environ.get("TF_MISSES") or "").split(",") if m.strip()]
|
|
66
|
+
if direct:
|
|
67
|
+
fix_cmd = os.environ.get("TF_FIXCMD") or "amend-docs"
|
|
68
|
+
verdict = os.environ.get("TF_VERDICT") or "Verified"
|
|
69
|
+
closed = 0
|
|
70
|
+
for mid in direct:
|
|
71
|
+
fa = q("--next-fix-attempt", mid) or "1"
|
|
72
|
+
rec = {"kind": "miss-fix", "miss_id": mid, "fix_cmd": fix_cmd,
|
|
73
|
+
"fix_attempt": int(fa) if fa.isdigit() else 1,
|
|
74
|
+
"verdict_after": verdict, "reopened": False}
|
|
75
|
+
if started:
|
|
76
|
+
rec["fix_run_id"] = started
|
|
77
|
+
if emit("misses", rec):
|
|
78
|
+
closed += 1
|
|
79
|
+
print("%s: %d of %d miss-fix record(s) written (no run record — the amending command emits its own)"
|
|
80
|
+
% (fix_cmd, closed, len(direct)))
|
|
81
|
+
raise SystemExit(0)
|
|
82
|
+
|
|
83
|
+
met = q("--where") or "docs/metrics"
|
|
84
|
+
def jsonl(name):
|
|
85
|
+
try:
|
|
86
|
+
for line in open(os.path.join(met, name), encoding="utf-8", errors="replace"):
|
|
87
|
+
try: yield json.loads(line)
|
|
88
|
+
except Exception: pass
|
|
89
|
+
except OSError:
|
|
90
|
+
return
|
|
91
|
+
# The verdicts: every verify this fix chained, not only the last. docs/.last-verify.json holds one scoped
|
|
92
|
+
# verify, so a fix that verified a Phase 3 scope and then a --phase 1 scope read the Phase 3 rows as absent
|
|
93
|
+
# and wrote Needs re-verify on their misses (TF-052). The gate records each verify wrote since the fix
|
|
94
|
+
# started come first; the ledger fills in only when it belongs to this fix.
|
|
95
|
+
rows = {} # rid -> checklist status
|
|
96
|
+
if started:
|
|
97
|
+
for g in jsonl("gates.jsonl"):
|
|
98
|
+
if g.get("kind") == "gate" and g.get("app") in (None, app) and not g.get("backfilled") and g.get("gate") != "escaped" \
|
|
99
|
+
and (g.get("run_id") or "") >= started and g.get("req_id"):
|
|
100
|
+
rows[g["req_id"].upper()] = g.get("verdict")
|
|
42
101
|
ledger = {}
|
|
43
102
|
try:
|
|
44
103
|
ledger = json.load(open(os.path.join("docs", ".last-verify.json"), encoding="utf-8"))
|
|
45
104
|
except Exception:
|
|
46
105
|
pass
|
|
47
|
-
|
|
106
|
+
if not started or not ledger.get("run_id") or ledger["run_id"] >= started:
|
|
107
|
+
row_runs = ledger.get("row_runs") # a ledger kept row by row also holds older runs' rows (Lekhak TF-007)
|
|
108
|
+
for rid, v in (ledger.get("rows") or {}).items():
|
|
109
|
+
if started and isinstance(row_runs, dict) and rid not in (ledger.get("this_run") or []) \
|
|
110
|
+
and (row_runs.get(rid) or "") < started:
|
|
111
|
+
continue
|
|
112
|
+
# verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
|
|
113
|
+
# (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
|
|
114
|
+
rows.setdefault(rid.upper(), "Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
|
|
48
115
|
reqs = [r.strip().upper() for r in os.environ["TF_REQS"].split(",") if r.strip()] or sorted(rows)
|
|
49
116
|
subs = [s.strip() for s in os.environ["TF_SUBS"].split(",") if s.strip()]
|
|
50
117
|
files = os.environ["TF_FILES"]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
118
|
+
# The fix's own time only: a run it chained (the inline verify) has its own record, so the fix is recorded
|
|
119
|
+
# in the gaps around it, the first gap keyed on the fix's start with the rows (its miss-fixes name that start).
|
|
120
|
+
inner, voided = [], set()
|
|
121
|
+
for r in jsonl("runs.jsonl"):
|
|
122
|
+
if r.get("kind") == "run-void":
|
|
123
|
+
voided.add((r.get("cmd"), r.get("started")))
|
|
124
|
+
elif r.get("kind", "run") == "run" and r.get("app") == app and r.get("cmd") != "fix-issues" and not r.get("backfilled") \
|
|
125
|
+
and started and (r.get("started") or "") >= started and (r.get("ended") or "") and r["ended"] <= now:
|
|
126
|
+
inner.append((r["started"], r["ended"], r.get("cmd")))
|
|
127
|
+
inner = sorted(w for w in inner if (w[2], w[0]) not in voided)
|
|
128
|
+
gaps, t = [], started
|
|
129
|
+
for s, e, _c in inner:
|
|
130
|
+
if t and s > t: gaps.append((t, s))
|
|
131
|
+
t = max(t, e) if t else e
|
|
132
|
+
if not started or not gaps or t < now: gaps.append((t, now))
|
|
133
|
+
existing = {r.get("started") for r in jsonl("runs.jsonl") if r.get("kind", "run") == "run" and r.get("cmd") == "fix-issues"}
|
|
134
|
+
wrote = had = 0
|
|
135
|
+
for i, (s, e) in enumerate(gaps):
|
|
136
|
+
first = i == 0
|
|
137
|
+
run = {"kind": "run", "app": app, "cmd": "fix-issues", "mode": "fix", "ended": e, "reqs_touched": reqs if first else [],
|
|
138
|
+
"reqs_count": len(reqs) if first else 0, "subagents": subs if first else [],
|
|
139
|
+
"files_written": (int(files) if files.isdigit() else len(reqs)) if first else 0, "build_result": os.environ["TF_BUILD"]}
|
|
140
|
+
if s: run["started"] = s
|
|
141
|
+
# one run record per start: a close called twice must not count the fix twice
|
|
142
|
+
if s and s in existing:
|
|
143
|
+
had += 1; continue
|
|
144
|
+
wrote += emit("runs", run)
|
|
145
|
+
if inner:
|
|
146
|
+
print(f"fix-issues: recorded around {len(inner)} chained run(s) ({', '.join(sorted({c for _s, _e, c in inner}))}) in {len(gaps)} segment(s)")
|
|
147
|
+
closed, none, unverified = 0, [], []
|
|
66
148
|
for rid in reqs:
|
|
149
|
+
if rid not in rows:
|
|
150
|
+
unverified.append(rid); continue
|
|
67
151
|
opened = q("--open-miss", rid)
|
|
68
152
|
if not opened:
|
|
69
153
|
none.append(rid); continue
|
|
70
154
|
mid = opened.split()[0]
|
|
71
155
|
fa = q("--next-fix-attempt", mid) or "1"
|
|
72
|
-
v = rows.get(rid)
|
|
73
|
-
# verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
|
|
74
|
-
# (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
|
|
75
|
-
after = ("Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
|
|
76
156
|
rec = {"kind": "miss-fix", "miss_id": mid, "req_id": rid, "fix_cmd": "fix-issues", "fix_attempt": int(fa) if fa.isdigit() else 1,
|
|
77
|
-
"verdict_after":
|
|
157
|
+
"verdict_after": rows[rid], "reopened": False}
|
|
78
158
|
if started: rec["fix_run_id"] = started
|
|
79
159
|
if emit("misses", rec): closed += 1
|
|
80
|
-
|
|
81
|
-
|
|
160
|
+
state = "already there" if had == len(gaps) else "written" if wrote + had == len(gaps) else "not written"
|
|
161
|
+
print(f"fix-issues: run record {state}; {closed} miss-fix record(s) with the verifier's verdict"
|
|
162
|
+
+ (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else "")
|
|
163
|
+
+ (f"; no verify graded {', '.join(unverified)} during this fix, so its miss stays open with no miss-fix" if unverified else ""))
|
|
82
164
|
PY
|
|
83
165
|
exit 0
|
|
@@ -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
|
|
@@ -56,14 +68,19 @@
|
|
|
56
68
|
# Override command lines with TF_GOAL_CLAUDE_FLAGS / TF_GOAL_OPENCODE_FLAGS /
|
|
57
69
|
|
|
58
70
|
set -u
|
|
71
|
+
# GNU-only commands (setsid, stat -c, date -d) go through the shim, so a stock Mac runs this too.
|
|
72
|
+
source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh"
|
|
59
73
|
|
|
60
74
|
HARNESS="claude"; MODEL=""; BUFFER_MIN=15; DEFAULT_WAIT_MIN=60; MAX_CYCLES=60; IDLE_RETRY=30
|
|
61
75
|
PROBE_MIN=15; PROBE_MAX_H=8; STALL_MIN=15
|
|
76
|
+
TIER="standard"; FALLBACK=1
|
|
62
77
|
RESUME=0; FRESH=0; DRY=0
|
|
63
78
|
while [[ $# -gt 0 ]]; do
|
|
64
79
|
case "$1" in
|
|
65
80
|
--harness) HARNESS="$2"; shift 2 ;;
|
|
66
81
|
--model) MODEL="$2"; shift 2 ;;
|
|
82
|
+
--tier) TIER="$2"; shift 2 ;;
|
|
83
|
+
--no-fallback) FALLBACK=0; shift ;;
|
|
67
84
|
--buffer-min) BUFFER_MIN="$2"; shift 2 ;;
|
|
68
85
|
--default-wait-min) DEFAULT_WAIT_MIN="$2"; shift 2 ;;
|
|
69
86
|
--probe-min) PROBE_MIN="$2"; shift 2 ;;
|
|
@@ -74,7 +91,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
74
91
|
--resume) RESUME=1; shift ;;
|
|
75
92
|
--fresh) FRESH=1; shift ;;
|
|
76
93
|
--dry-run) DRY=1; shift ;;
|
|
77
|
-
-h|--help) sed -n '2,
|
|
94
|
+
-h|--help) sed -n '2,48p' "$0"; exit 0 ;;
|
|
78
95
|
--) shift; break ;;
|
|
79
96
|
-*) echo "unknown option $1" >&2; exit 2 ;;
|
|
80
97
|
*) break ;;
|
|
@@ -88,11 +105,15 @@ fi
|
|
|
88
105
|
APP_DIR="$(cd "$APP_DIR" 2>/dev/null && pwd)" || { echo "no such dir: $1" >&2; exit 2; }
|
|
89
106
|
[[ -d "$APP_DIR/.tfcore" ]] || { echo "$APP_DIR has no .tfcore/ — scaffold it first" >&2; exit 2; }
|
|
90
107
|
case "$HARNESS" in claude|opencode) ;; *) echo "--harness must be claude|opencode" >&2; exit 2 ;; esac
|
|
108
|
+
case "$TIER" in frontier|standard|economy) ;; *) echo "--tier must be frontier|standard|economy" >&2; exit 2 ;; esac
|
|
91
109
|
command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; exit 2; }
|
|
92
110
|
|
|
93
111
|
STATE_DIR="$APP_DIR/.tfcore/.session"; mkdir -p "$STATE_DIR"
|
|
94
112
|
STATE="$STATE_DIR/goal.json"; LOG="$STATE_DIR/goal.log"; DONE="$STATE_DIR/goal-done.json"
|
|
95
113
|
YOLO_SH="$APP_DIR/.tfcore/utils/tf-yolo.sh"
|
|
114
|
+
PICK_SH="$APP_DIR/.tfcore/utils/tf-model-pick.sh"
|
|
115
|
+
BIND_SH="$APP_DIR/.tfcore/utils/tf-routing-bind.sh"
|
|
116
|
+
[[ -f "$PICK_SH" ]] || FALLBACK=0 # a project that predates tf-model-pick.sh keeps the old behaviour
|
|
96
117
|
|
|
97
118
|
if [[ "$GOAL_ARG" == @* ]]; then
|
|
98
119
|
GOAL_FILE="${GOAL_ARG#@}"; [[ -f "$GOAL_FILE" ]] || { echo "goal file not found: $GOAL_FILE" >&2; exit 2; }
|
|
@@ -126,6 +147,10 @@ if [[ $RESUME -eq 1 ]]; then
|
|
|
126
147
|
[[ -f "$STATE" ]] || { echo "--resume: no $STATE" >&2; exit 2; }
|
|
127
148
|
[[ -z "$GOAL" ]] && GOAL="$(state_get goal)"
|
|
128
149
|
HARNESS="$(state_get harness)"; HARNESS="${HARNESS:-claude}"
|
|
150
|
+
# A resume keeps the model a fallback moved to, and the tier its chain came from; an explicit
|
|
151
|
+
# --model on the resume command still wins.
|
|
152
|
+
_t="$(state_get tier)"; [[ -n "$_t" ]] && TIER="$_t"
|
|
153
|
+
_m="$(state_get model)"; [[ -n "$_m" && -z "$MODEL" ]] && MODEL="$_m"
|
|
129
154
|
CYCLE="$(state_get cycle)"; CYCLE="${CYCLE:-0}"
|
|
130
155
|
SESSION_ID="$(state_get session_id)"
|
|
131
156
|
STALLS="$(state_get stalls)"; STALLS="${STALLS:-0}"
|
|
@@ -139,12 +164,12 @@ else
|
|
|
139
164
|
_lr="$(state_get last_reason)"
|
|
140
165
|
case "$_lr" in
|
|
141
166
|
done:*|max-cycles|stopped|"") ;;
|
|
142
|
-
*) echo "tf-goal: a run looks active in $APP_DIR (goal.json last_reason=$_lr, touched $(
|
|
167
|
+
*) echo "tf-goal: a run looks active in $APP_DIR (goal.json last_reason=$_lr, touched $(tf_date_from -u "@$(tf_stat_mtime "$STATE")" +%H:%MZ)). Wait for it, or continue it with --resume." >&2; exit 2 ;;
|
|
143
168
|
esac
|
|
144
169
|
fi
|
|
145
170
|
CYCLE=0; SESSION_ID=""; STALLS=0
|
|
146
171
|
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"
|
|
172
|
+
state_set goal "$GOAL" harness "$HARNESS" tier "$TIER" model "$MODEL" cycle 0 session_id "" stalls 0 started "$(ts)" last_reason "start"
|
|
148
173
|
rm -f "$DONE"
|
|
149
174
|
fi
|
|
150
175
|
fi
|
|
@@ -346,6 +371,60 @@ print(sid)
|
|
|
346
371
|
PY
|
|
347
372
|
}
|
|
348
373
|
|
|
374
|
+
extract_model() { # the model the harness reported for this cycle, from its own output
|
|
375
|
+
python3 - "$1" <<'PY' 2>/dev/null
|
|
376
|
+
import sys, json, re
|
|
377
|
+
m = ""
|
|
378
|
+
try:
|
|
379
|
+
for line in open(sys.argv[1], errors="replace"):
|
|
380
|
+
line = line.strip()
|
|
381
|
+
if not line.startswith("{") or '"model"' not in line:
|
|
382
|
+
continue
|
|
383
|
+
try: d = json.loads(line)
|
|
384
|
+
except Exception: continue
|
|
385
|
+
if not isinstance(d, dict): continue
|
|
386
|
+
if isinstance(d.get("model"), str) and d["model"]:
|
|
387
|
+
m = d["model"]
|
|
388
|
+
msg = d.get("message")
|
|
389
|
+
if isinstance(msg, dict) and isinstance(msg.get("model"), str) and msg["model"]:
|
|
390
|
+
m = msg["model"]
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
print(m)
|
|
394
|
+
PY
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Park the model this cycle ran on and move to the next one in its tier. Prints nothing and
|
|
398
|
+
# returns 1 when there is nowhere to go — the caller then sleeps, which is what it always did.
|
|
399
|
+
# $1 = epoch the limit is stated to reset at (or empty when it could not be parsed)
|
|
400
|
+
# $2 = why, for the cooldown record
|
|
401
|
+
try_fallback() {
|
|
402
|
+
[[ $FALLBACK -eq 1 ]] || return 1
|
|
403
|
+
local until="${1:-}" why="${2:-usage limit}" cur next
|
|
404
|
+
cur="$MODEL"
|
|
405
|
+
[[ -z "$cur" ]] && cur="$(extract_model "$OUT")"
|
|
406
|
+
if [[ -z "$cur" ]]; then
|
|
407
|
+
cur="$(TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" pick "$TIER" "$HARNESS" 2>/dev/null | tail -1)"
|
|
408
|
+
[[ "$cur" == inherit ]] && cur=""
|
|
409
|
+
fi
|
|
410
|
+
if [[ -z "$cur" ]]; then
|
|
411
|
+
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)"
|
|
412
|
+
return 1
|
|
413
|
+
fi
|
|
414
|
+
[[ -z "$until" ]] && until="probe"
|
|
415
|
+
TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" cooldown "$HARNESS" "$cur" "$until" "$why" >>"$LOG" 2>&1
|
|
416
|
+
next="$(TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" pick "$TIER" "$HARNESS" 2>/dev/null | tail -1)"
|
|
417
|
+
if [[ -z "$next" || "$next" == "inherit" ]]; then
|
|
418
|
+
log "no fallback left: every model in the $TIER chain is on cooldown — waiting for the reset instead"
|
|
419
|
+
return 1
|
|
420
|
+
fi
|
|
421
|
+
MODEL="$next"
|
|
422
|
+
state_set model "$MODEL" fallback_from "$cur" last_reason "fallback:$cur->$MODEL"
|
|
423
|
+
log "FALLBACK: $cur is limited ($why) — the next cycle runs on $MODEL (tier $TIER). Re-binding so the sub-agents move too."
|
|
424
|
+
[[ -f "$BIND_SH" ]] && bash "$BIND_SH" "$APP_DIR" 2>&1 | sed 's/^/ /' | tee -a "$LOG" >&2
|
|
425
|
+
return 0
|
|
426
|
+
}
|
|
427
|
+
|
|
349
428
|
# Fallback when a limit message carries no parseable reset time: fire a one-turn
|
|
350
429
|
# probe every PROBE_MIN minutes until it comes back clean (exit 0 AND no limit
|
|
351
430
|
# wording in its output). Gives up after PROBE_MAX_H hours and resumes anyway.
|
|
@@ -359,11 +438,11 @@ probe_until_clear() {
|
|
|
359
438
|
else
|
|
360
439
|
( cd "$APP_DIR" && opencode run --auto "Reply with the single word OK." ) > "$pout" 2>&1; prc=$?
|
|
361
440
|
fi
|
|
362
|
-
if [[ $prc -eq 0 ]] && ! grep -qiE 'usage limit|hit your limit|rate[ _-]?limit|limit (has been )?(reached|exceeded)|too many requests
|
|
441
|
+
if [[ $prc -eq 0 ]] && ! grep -qiE 'usage limit|hit your limit|rate[ _-]?limit|limit (has been )?(reached|exceeded)|too many requests|(^|[^[:alnum:]_])429([^[:alnum:]_]|$)|overloaded|weekly limit|resets? (at|in)([^[:alnum:]_]|$)' "$pout"; then
|
|
363
442
|
log "probe #$n OK"; return 0
|
|
364
443
|
fi
|
|
365
444
|
log "probe #$n still limited (rc=$prc: $(head -c 120 "$pout" | tr '\n' ' ')) — next in ${PROBE_MIN}m"
|
|
366
|
-
state_set resume_at "probe #$((n+1)) at $(
|
|
445
|
+
state_set resume_at "probe #$((n+1)) at $(tf_date_from "+${PROBE_MIN} min" '+%H:%M' 2>/dev/null)"
|
|
367
446
|
if [[ $(date +%s) -ge $deadline ]]; then log "probe window (${PROBE_MAX_H}h) exhausted — resuming anyway"; return 1; fi
|
|
368
447
|
nap $(( PROBE_MIN * 60 ))
|
|
369
448
|
done
|
|
@@ -373,7 +452,7 @@ sleep_until() { # epoch
|
|
|
373
452
|
local target="$1" now left
|
|
374
453
|
while :; do
|
|
375
454
|
now=$(date +%s); left=$(( target - now )); [[ $left -le 0 ]] && break
|
|
376
|
-
state_set resume_at "$(
|
|
455
|
+
state_set resume_at "$(tf_date_from -u "@$target" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "$target")"
|
|
377
456
|
if [[ $left -gt 300 ]]; then nap 300; else nap "$left"; fi
|
|
378
457
|
done
|
|
379
458
|
state_set resume_at ""
|
|
@@ -405,16 +484,14 @@ run_cycle() { # runs "${CMD[@]}" in $APP_DIR, output to $OUT and $LOG; sets RC,
|
|
|
405
484
|
STALLED=0
|
|
406
485
|
local quiet=0 prev=-1 size
|
|
407
486
|
if [[ -n "${TF_GOAL_FAKE_CMD:-}" ]]; then CMD=(bash -c "$TF_GOAL_FAKE_CMD"); fi
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
( cd "$APP_DIR" && TF_YOLO=1 exec "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
|
|
412
|
-
fi
|
|
487
|
+
# Its own session and process group (tf_setsid: setsid(1), or python3/perl on a Mac), so
|
|
488
|
+
# kill_child can stop the whole tree.
|
|
489
|
+
( cd "$APP_DIR" && TF_YOLO=1 tf_setsid --exec "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
|
|
413
490
|
CHILD=$!
|
|
414
491
|
while kill -0 "$CHILD" 2>/dev/null; do
|
|
415
492
|
nap "$STALL_TICK"
|
|
416
493
|
kill -0 "$CHILD" 2>/dev/null || break
|
|
417
|
-
size="$(
|
|
494
|
+
size="$(tf_stat_size "$OUT" 2>/dev/null || echo 0)"
|
|
418
495
|
if [[ "$size" == "$prev" ]]; then quiet=$(( quiet + STALL_TICK )); else quiet=0; prev="$size"; fi
|
|
419
496
|
if [[ "$STALL_SEC" -gt 0 && $quiet -ge "$STALL_SEC" ]]; then
|
|
420
497
|
log "STALL: no output for $(( quiet / 60 ))m$(( quiet % 60 ))s (cycle $CYCLE, ${size} bytes) — stopping the harness, re-prompting"
|
|
@@ -492,7 +569,7 @@ while :; do
|
|
|
492
569
|
OUT="$STATE_DIR/goal-cycle-$CYCLE.out"
|
|
493
570
|
if [[ $DRY -eq 1 ]]; then echo "dry run — would launch cycle $CYCLE ($KIND):" >&2; printf ' %q' "${CMD[@]}"; echo; exit 0; fi
|
|
494
571
|
state_set cycle "$CYCLE"; CYCLE_START="$(date +%s)"
|
|
495
|
-
log "cycle $CYCLE ($KIND) → ${CMD[*]:0:6} … (prompt ${#CMD[-1]} chars)"
|
|
572
|
+
log "cycle $CYCLE ($KIND) → ${CMD[*]:0:6} … (prompt ${#CMD[${#CMD[@]}-1]} chars)"
|
|
496
573
|
|
|
497
574
|
# The run's start is now, not when the agent reaches step 0: write an unclaimed marker the
|
|
498
575
|
# first command's `tf-phase.sh start` claims (MISS-TechieFlow-20260905-20).
|
|
@@ -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
|
|
@@ -533,7 +620,7 @@ while :; do
|
|
|
533
620
|
log "probe succeeded — resuming session ${SESSION_ID:-(--continue)} after a ${BUFFER_MIN}m buffer"
|
|
534
621
|
nap $(( BUFFER_MIN * 60 ))
|
|
535
622
|
else
|
|
536
|
-
WHEN="$(
|
|
623
|
+
WHEN="$(tf_date_from "@$DETAIL" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || echo "$DETAIL")"
|
|
537
624
|
log "USAGE LIMIT hit (cycle $CYCLE) — RETRY AT $WHEN (stated reset + ${BUFFER_MIN}m buffer). Sleeping."
|
|
538
625
|
state_set last_reason "limit" resume_at "$WHEN"
|
|
539
626
|
sleep_until "$DETAIL"
|
|
@@ -48,13 +48,18 @@ _detect() {
|
|
|
48
48
|
fi
|
|
49
49
|
local k
|
|
50
50
|
for k in $(compgen -v | grep '^OPENCODE' 2>/dev/null); do printf 'opencode'; return; done
|
|
51
|
-
# /proc
|
|
51
|
+
# ancestry walk, bounded: /proc on Linux/WSL, ps where there is no /proc (macOS)
|
|
52
52
|
local pid=$PPID name ppid seen=0 stat
|
|
53
53
|
while [[ -n "$pid" && "$pid" -gt 1 && $seen -lt 12 ]]; do
|
|
54
54
|
seen=$((seen + 1))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if [[ -d /proc/self ]]; then
|
|
56
|
+
stat="$(cat /proc/$pid/stat 2>/dev/null)" || break
|
|
57
|
+
name="${stat#*(}"; name="${name%%)*}"
|
|
58
|
+
ppid="$(printf '%s' "${stat##*) }" | awk '{print $2}')"
|
|
59
|
+
else
|
|
60
|
+
ppid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')"; [[ -n "$ppid" ]] || break
|
|
61
|
+
name="$(ps -o comm= -p "$pid" 2>/dev/null)"; name="${name##*/}"
|
|
62
|
+
fi
|
|
58
63
|
case "$name" in
|
|
59
64
|
*opencode*) printf 'opencode'; return ;;
|
|
60
65
|
*claude*) printf 'claude-code'; return ;;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# tf-lock.sh — one build or browser check at a time per repository. Sourced, not run.
|
|
2
|
+
# source "$HERE/tf-lock.sh"; tf_take_lock <label> waits for the lock, then holds it until exit
|
|
3
|
+
# A directory, because mkdir is atomic on every drive this runs on. A lock whose process has gone
|
|
4
|
+
# is taken over. A script started by the holder (tf-verify-tests.sh running tf-build.sh) shares it
|
|
5
|
+
# through TF_REPO_LOCK_PID. TF_BUILD_LOCK_MAX seconds to wait (1800). (TF-043, TfLens TF-048)
|
|
6
|
+
TF_LOCK="tests/.artifacts/build/.lock"
|
|
7
|
+
tf_release_lock() { [[ "$(cut -d' ' -f1 "$TF_LOCK/owner" 2>/dev/null)" == "$$" ]] && rm -rf "$TF_LOCK"; }
|
|
8
|
+
tf_take_lock() {
|
|
9
|
+
local label="${1:-}" waited=0 max="${TF_BUILD_LOCK_MAX:-1800}" said=0 owner opid ohost
|
|
10
|
+
if [[ -n "${TF_REPO_LOCK_PID:-}" ]] && kill -0 "$TF_REPO_LOCK_PID" 2>/dev/null; then return 0; fi
|
|
11
|
+
mkdir -p "$(dirname "$TF_LOCK")"
|
|
12
|
+
while ! mkdir "$TF_LOCK" 2>/dev/null; do
|
|
13
|
+
owner="$(cat "$TF_LOCK/owner" 2>/dev/null)"; opid="$(cut -d' ' -f1 <<<"$owner")"; ohost="$(cut -d' ' -f2 <<<"$owner")"
|
|
14
|
+
if [[ -z "$owner" ]]; then
|
|
15
|
+
[[ -n "$(find "$TF_LOCK" -maxdepth 0 -mmin +1 2>/dev/null)" ]] && { rm -rf "$TF_LOCK"; continue; }
|
|
16
|
+
elif [[ "$ohost" == "$(hostname)" ]] && ! kill -0 "$opid" 2>/dev/null; then
|
|
17
|
+
rm -rf "$TF_LOCK"; continue
|
|
18
|
+
fi
|
|
19
|
+
[[ $said -eq 0 ]] && { echo "wait another build or browser check is running in this repository (${owner:-starting}); this one starts when it finishes" >&2; said=1; }
|
|
20
|
+
if [[ $waited -ge $max ]]; then
|
|
21
|
+
echo "NOT-RUN another build or browser check held this repository for $((max / 60)) minutes ($owner). If nothing is running, remove $TF_LOCK and run again. Not a code error"
|
|
22
|
+
exit 2
|
|
23
|
+
fi
|
|
24
|
+
sleep 3; waited=$((waited + 3))
|
|
25
|
+
done
|
|
26
|
+
echo "$$ $(hostname) $label $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TF_LOCK/owner"
|
|
27
|
+
export TF_REPO_LOCK_PID=$$
|
|
28
|
+
trap tf_release_lock EXIT
|
|
29
|
+
}
|