@techierathore/techieflow 1.0.2 → 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 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- 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 +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- 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/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +86 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- 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 +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- 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-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.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-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +358 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- 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
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
package/.tfcore/utils/tf-emit.sh
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
# tf-emit.sh --amend MISS-App-20260828-01 why_missed missing-checklist-item
|
|
20
20
|
# # fills a field that is still null on a miss already on
|
|
21
21
|
# # the stream (SCHEMA.md §5.5.7). Never overwrites.
|
|
22
|
+
# tf-emit.sh --void-run build-phase 2026-09-09T11:40:00Z "the start time was guessed, not measured"
|
|
23
|
+
# # marks a WRONG run record as not to be counted: both
|
|
24
|
+
# # records stay, every figure skips it and says how many
|
|
25
|
+
# # and why (SCHEMA.md §2.7). Refuses if no such run.
|
|
26
|
+
# tf-emit.sh --origin-of REQ-UI-014 # prints "<started> <cmd> <agent>" of the last build or fix
|
|
27
|
+
# # run that touched the row, nothing when there is none
|
|
22
28
|
# tf-emit.sh --where # prints the resolved docs/metrics dir
|
|
23
29
|
#
|
|
24
30
|
# STREAMS: runs | gates | sessions | commits | misses (anything else is dropped)
|
|
@@ -131,6 +137,39 @@ PY
|
|
|
131
137
|
exit 0
|
|
132
138
|
fi
|
|
133
139
|
|
|
140
|
+
# --- read helper: which run caused a row's defect ------------------------
|
|
141
|
+
# Prints "<started> <cmd> <agent>" for the most recent non-backfilled build-phase
|
|
142
|
+
# or fix-issues run whose reqs_touched holds the REQ, nothing when there is none
|
|
143
|
+
# (the caller then leaves origin_run_id out and the record is marked inferred).
|
|
144
|
+
# Used by tf-log-miss.sh and tf-triage.sh (Sitting 4c, 2026-09-06).
|
|
145
|
+
if [[ "$1" == "--origin-of" ]]; then
|
|
146
|
+
REQ="$2"
|
|
147
|
+
[[ -n "$REQ" ]] || exit 0
|
|
148
|
+
python3 - "$MET_DIR/runs.jsonl" "$REQ" 2>"$TF_ERR" <<'PY2' || true
|
|
149
|
+
import json, sys
|
|
150
|
+
path, req = sys.argv[1], sys.argv[2].upper()
|
|
151
|
+
best = None
|
|
152
|
+
try:
|
|
153
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
154
|
+
for line in fh:
|
|
155
|
+
try:
|
|
156
|
+
r = json.loads(line)
|
|
157
|
+
except Exception:
|
|
158
|
+
continue
|
|
159
|
+
if r.get("kind") != "run" or r.get("backfilled") or r.get("cmd") not in ("build-phase", "fix-issues"):
|
|
160
|
+
continue
|
|
161
|
+
if req in [str(x).upper() for x in (r.get("reqs_touched") or [])]:
|
|
162
|
+
if best is None or r.get("started", "") >= best.get("started", ""):
|
|
163
|
+
best = r
|
|
164
|
+
except FileNotFoundError:
|
|
165
|
+
pass
|
|
166
|
+
if best:
|
|
167
|
+
subs = best.get("subagents") or []
|
|
168
|
+
print(best.get("started", ""), best.get("cmd"), subs[0] if subs else "flow-master")
|
|
169
|
+
PY2
|
|
170
|
+
exit 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
134
173
|
# --- read helper: next RUN attempt number for a cmd + REQ set ------------
|
|
135
174
|
# run attempt = 1 + count of prior NON-BACKFILLED `run` records with the same
|
|
136
175
|
# cmd whose reqs_touched intersects the given REQ IDs. Same counting rule that
|
|
@@ -170,9 +209,10 @@ fi
|
|
|
170
209
|
# `miss-fix`, and a collision would silently merge two defects into one lifecycle.
|
|
171
210
|
if [[ "$1" == "--next-miss-id" ]]; then
|
|
172
211
|
python3 - "$MET_DIR/misses.jsonl" "$ROOT" 2>"$TF_ERR" <<'PY' || true
|
|
173
|
-
import datetime, glob, json, os, sys
|
|
212
|
+
import datetime, glob, json, os, re, sys
|
|
174
213
|
path, root = sys.argv[1], sys.argv[2]
|
|
175
|
-
hits = glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
214
|
+
hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
215
|
+
if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)] # only the phase-1 work list names the app
|
|
176
216
|
app = (os.path.basename(hits[0])[: -len("-Checklist.md")] if len(hits) == 1
|
|
177
217
|
else os.path.basename(os.path.abspath(root)))
|
|
178
218
|
day = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
|
|
@@ -234,6 +274,61 @@ fi
|
|
|
234
274
|
# patience rather than quality. Open = no miss-fix, or the latest miss-fix
|
|
235
275
|
# closed with a verdict_after other than "Verified". Prints "<miss_id>
|
|
236
276
|
# <miss_class>" for the most recent open miss, or nothing at all.
|
|
277
|
+
# --- list the open misses whose deficient artifact is a document ----------
|
|
278
|
+
# `*amend-docs` is the command that edits a BRD, an Architecture or a mockup, so it is
|
|
279
|
+
# the only command that can close a miss whose artifact is one of those -- and it had no
|
|
280
|
+
# way to find them. Every other close path works off a checklist row the verifier touched,
|
|
281
|
+
# which a BRD edit never is, so a document miss stayed open forever (TF-016: twelve
|
|
282
|
+
# finished TfLens items showed as outstanding for up to two weeks).
|
|
283
|
+
#
|
|
284
|
+
# tf-emit.sh --open-misses <App> [--artifact-class doc]
|
|
285
|
+
#
|
|
286
|
+
# Prints one line per open miss: "<miss_id> <artifact> <what>". Open means no miss-fix
|
|
287
|
+
# carries verdict_after Verified, the same test --open-miss applies.
|
|
288
|
+
if [[ "$1" == "--open-misses" ]]; then
|
|
289
|
+
OMAPP="${2:-}"; OMCLASS=""
|
|
290
|
+
[[ "${3:-}" == "--artifact-class" ]] && OMCLASS="${4:-}"
|
|
291
|
+
TF_OMAPP="$OMAPP" TF_OMCLASS="$OMCLASS" python3 - "$MET_DIR/misses.jsonl" 2>"$TF_ERR" <<'PY2' || true
|
|
292
|
+
import json, os, sys
|
|
293
|
+
DOC = {"brd", "architecture", "uidesign", "checklist", "devguide"}
|
|
294
|
+
app, klass = os.environ.get("TF_OMAPP") or "", os.environ.get("TF_OMCLASS") or ""
|
|
295
|
+
opened, fixes = [], {}
|
|
296
|
+
try:
|
|
297
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
298
|
+
for line in fh:
|
|
299
|
+
line = line.strip()
|
|
300
|
+
if not line:
|
|
301
|
+
continue
|
|
302
|
+
try:
|
|
303
|
+
r = json.loads(line)
|
|
304
|
+
except Exception:
|
|
305
|
+
continue
|
|
306
|
+
if r.get("kind") == "miss":
|
|
307
|
+
opened.append(r)
|
|
308
|
+
elif r.get("kind") == "miss-fix":
|
|
309
|
+
mid = r.get("miss_id")
|
|
310
|
+
if mid:
|
|
311
|
+
prev = fixes.get(mid)
|
|
312
|
+
if prev is None or (r.get("ts") or "") >= (prev.get("ts") or ""):
|
|
313
|
+
fixes[mid] = r
|
|
314
|
+
except FileNotFoundError:
|
|
315
|
+
pass
|
|
316
|
+
for r in opened:
|
|
317
|
+
if app and r.get("app") and r.get("app") != app:
|
|
318
|
+
continue
|
|
319
|
+
art = r.get("artifact") or ""
|
|
320
|
+
if klass == "doc" and art not in DOC:
|
|
321
|
+
continue
|
|
322
|
+
if klass and klass != "doc" and art != klass:
|
|
323
|
+
continue
|
|
324
|
+
fix = fixes.get(r.get("miss_id"))
|
|
325
|
+
if fix is not None and fix.get("verdict_after") == "Verified":
|
|
326
|
+
continue
|
|
327
|
+
print("%s %s %s" % (r.get("miss_id"), art or "-", (r.get("what") or "")[:110]))
|
|
328
|
+
PY2
|
|
329
|
+
exit 0
|
|
330
|
+
fi
|
|
331
|
+
|
|
237
332
|
if [[ "$1" == "--open-miss" ]]; then
|
|
238
333
|
REQ="$2"
|
|
239
334
|
[[ -n "$REQ" ]] || exit 0
|
|
@@ -304,10 +399,23 @@ AMENDABLE = {
|
|
|
304
399
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
305
400
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
306
401
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
402
|
+
# whose gap it was, the four-question sort (FR-32, Session 5): a judgement a reader can
|
|
403
|
+
# still make later, so a miss logged before the field existed can be sorted now.
|
|
404
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
307
405
|
}
|
|
308
|
-
|
|
406
|
+
# `what` is the miss's own sentence, and it is FREE TEXT -- the one field here that
|
|
407
|
+
# is not a closed vocabulary. It is allowed because the protection was never the
|
|
408
|
+
# vocabulary: an amend may only fill a field that is still null and can never
|
|
409
|
+
# overwrite one that is set (below), so free text cannot rewrite a fact, only
|
|
410
|
+
# supply one that was missing. Validated as non-empty text instead.
|
|
411
|
+
# MISS-TechieFlow-20260909-02; tests/regression/run.sh tf_018.
|
|
412
|
+
AMENDABLE_TEXT = ("what",)
|
|
413
|
+
if fld not in AMENDABLE and fld not in AMENDABLE_TEXT:
|
|
309
414
|
print("REFUSED %s is not an amendable field (SCHEMA.md §5.5.7)" % fld); raise SystemExit(0)
|
|
310
|
-
if
|
|
415
|
+
if fld in AMENDABLE_TEXT:
|
|
416
|
+
if not isinstance(val, str) or not val.strip():
|
|
417
|
+
print("REFUSED %s must be a non-empty sentence" % fld); raise SystemExit(0)
|
|
418
|
+
elif val not in AMENDABLE[fld]:
|
|
311
419
|
print("REFUSED %r is not in the closed vocabulary for %s" % (val, fld)); raise SystemExit(0)
|
|
312
420
|
|
|
313
421
|
parent, current = None, None
|
|
@@ -350,8 +458,77 @@ PY
|
|
|
350
458
|
exit 0
|
|
351
459
|
fi
|
|
352
460
|
|
|
461
|
+
# --- write helper: mark a run record as not to be counted -----------------
|
|
462
|
+
# SCHEMA.md §2.7. The same doctrine as --amend, one stream over: a run record that
|
|
463
|
+
# is WRONG cannot be edited or deleted, because the stream is append-only and the
|
|
464
|
+
# evidence that it happened is worth keeping. So the correction is another record.
|
|
465
|
+
#
|
|
466
|
+
# tf-emit.sh --void-run <cmd> <started> "<why it is wrong>"
|
|
467
|
+
#
|
|
468
|
+
# It names the run by its own `cmd` and `started` — the pair every run record
|
|
469
|
+
# carries and the key the readers already use — and every figure then skips it,
|
|
470
|
+
# with the count and the reasons printed. A void that names no run on this stream
|
|
471
|
+
# is refused: it would be a claim about nothing. It is deliberately NOT a delete:
|
|
472
|
+
# both records stay, and a reader can see what was corrected and why.
|
|
473
|
+
if [[ "$1" == "--void-run" ]]; then
|
|
474
|
+
VCMD="$2"; VSTART="$3"; VWHY="$4"
|
|
475
|
+
if [[ -z "$VCMD" || -z "$VSTART" || -z "$VWHY" ]]; then
|
|
476
|
+
echo "tf-emit: --void-run needs <cmd> <started> \"<reason>\""
|
|
477
|
+
exit 0
|
|
478
|
+
fi
|
|
479
|
+
VREC="$(python3 - "$MET_DIR/runs.jsonl" "$VCMD" "$VSTART" "$VWHY" 2>"$TF_ERR" <<'PY' || true
|
|
480
|
+
import json, sys
|
|
481
|
+
path, cmd, started, why = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
|
482
|
+
target, voided, app = None, False, None
|
|
483
|
+
try:
|
|
484
|
+
with open(path, encoding="utf-8") as fh:
|
|
485
|
+
for line in fh:
|
|
486
|
+
line = line.strip()
|
|
487
|
+
if not line:
|
|
488
|
+
continue
|
|
489
|
+
try:
|
|
490
|
+
r = json.loads(line)
|
|
491
|
+
except Exception:
|
|
492
|
+
continue
|
|
493
|
+
if r.get("cmd") == cmd and r.get("started") == started:
|
|
494
|
+
if r.get("kind") == "run-void":
|
|
495
|
+
voided = True
|
|
496
|
+
elif r.get("kind", "run") == "run":
|
|
497
|
+
target = r
|
|
498
|
+
app = r.get("app")
|
|
499
|
+
except FileNotFoundError:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
if target is None:
|
|
503
|
+
print("REFUSED no run record %s started %s on this stream" % (cmd, started)); raise SystemExit(0)
|
|
504
|
+
if voided:
|
|
505
|
+
print("REFUSED %s started %s is already voided — one void is enough" % (cmd, started)); raise SystemExit(0)
|
|
506
|
+
if not why.strip():
|
|
507
|
+
print("REFUSED a void needs a reason a reader can check"); raise SystemExit(0)
|
|
508
|
+
|
|
509
|
+
print("OK " + json.dumps({"kind": "run-void", "app": app, "cmd": cmd,
|
|
510
|
+
"started": started, "reason": why.strip()},
|
|
511
|
+
separators=(",", ":"), ensure_ascii=False))
|
|
512
|
+
PY
|
|
513
|
+
)"
|
|
514
|
+
case "$VREC" in
|
|
515
|
+
"OK "*) printf '%s' "${VREC#OK }" | bash "${BASH_SOURCE[0]}" runs
|
|
516
|
+
echo "tf-emit: voided $VCMD started $VSTART — $VWHY"
|
|
517
|
+
echo " both records stay on the stream; every figure now skips the run and says so" ;;
|
|
518
|
+
"REFUSED "*) echo "tf-emit: void refused — ${VREC#REFUSED }" ;;
|
|
519
|
+
*) echo "tf-emit: void could not be evaluated — nothing written" ;;
|
|
520
|
+
esac
|
|
521
|
+
exit 0
|
|
522
|
+
fi
|
|
523
|
+
|
|
353
524
|
# --- append path ----------------------------------------------------------
|
|
354
525
|
STREAM="$1"
|
|
526
|
+
# --allow-overlap: this run really did happen while another was still open (a second machine
|
|
527
|
+
# appending to the same merge=union stream is the only case that has ever come up). Without it a
|
|
528
|
+
# run whose `started` precedes the previous run's `ended` is refused — see _overlap_ok below.
|
|
529
|
+
TF_ALLOW_OVERLAP=0
|
|
530
|
+
for _a in "$@"; do [[ "$_a" == "--allow-overlap" ]] && TF_ALLOW_OVERLAP=1; done
|
|
531
|
+
export TF_ALLOW_OVERLAP
|
|
355
532
|
case "$STREAM" in
|
|
356
533
|
runs|gates|sessions|commits|misses) ;;
|
|
357
534
|
*) _warn "unknown stream '${STREAM:-<none>}' — event dropped"; exit 0 ;;
|
|
@@ -360,7 +537,7 @@ esac
|
|
|
360
537
|
# The program is captured into a variable rather than fed on stdin — a heredoc
|
|
361
538
|
# on `python3 -` would REPLACE the caller's piped JSON and silently drop every event.
|
|
362
539
|
TF_PROG="$(cat <<'PY'
|
|
363
|
-
import json, os, sys, datetime, glob, re
|
|
540
|
+
import json, os, sys, datetime, glob, re, subprocess, time
|
|
364
541
|
|
|
365
542
|
met_dir, stream, root = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
366
543
|
dbg = os.environ.get("TF_METRICS_DEBUG")
|
|
@@ -456,11 +633,8 @@ def _detect_harness():
|
|
|
456
633
|
# the OTHER harness inherits that harness's markers (e.g. OpenCode started
|
|
457
634
|
# from a Claude Code bash still carries CLAUDE_PROJECT_DIR).
|
|
458
635
|
tf = os.environ.get("TF_HARNESS")
|
|
459
|
-
if tf in ("claude-code", "opencode"
|
|
636
|
+
if tf in ("claude-code", "opencode"):
|
|
460
637
|
return tf
|
|
461
|
-
for k in ("CODEX_THREAD_ID", "CODEX_SESSION_ID"):
|
|
462
|
-
if os.environ.get(k):
|
|
463
|
-
return "codex"
|
|
464
638
|
for k in ("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID",
|
|
465
639
|
"CLAUDE_PROJECT_DIR"):
|
|
466
640
|
if os.environ.get(k):
|
|
@@ -488,8 +662,6 @@ def _detect_harness():
|
|
|
488
662
|
name, ppid = os.path.basename(parts[0]), int(parts[1])
|
|
489
663
|
if name and "opencode" in name.lower():
|
|
490
664
|
return "opencode"
|
|
491
|
-
if name and "codex" in name.lower():
|
|
492
|
-
return "codex"
|
|
493
665
|
if name and "claude" in name.lower():
|
|
494
666
|
return "claude-code"
|
|
495
667
|
pid = ppid
|
|
@@ -505,7 +677,11 @@ except Exception:
|
|
|
505
677
|
# app name — inferred from the one checklist, else the repo directory name
|
|
506
678
|
def _app_name():
|
|
507
679
|
try:
|
|
508
|
-
|
|
680
|
+
# The Deployment Checklist and a Large project's phase-n checklists share the suffix;
|
|
681
|
+
# only <App>-Checklist.md names the app, else a copy such as TfLens-oc was recorded
|
|
682
|
+
# under its folder name (MISS-TechieFlow-20260906-04).
|
|
683
|
+
hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
684
|
+
if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)]
|
|
509
685
|
if len(hits) == 1:
|
|
510
686
|
return os.path.basename(hits[0])[: -len("-Checklist.md")]
|
|
511
687
|
except Exception:
|
|
@@ -524,6 +700,80 @@ def enrich(rec):
|
|
|
524
700
|
DECISIONS.md 2026-08-20)."""
|
|
525
701
|
rec.setdefault("v", 1)
|
|
526
702
|
rec.setdefault("ts", NOW)
|
|
703
|
+
# A run record says whether YOLO was on (D-12, FR-35; Sitting 4c, 2026-09-06): the task
|
|
704
|
+
# never has to remember it. TF_YOLO=1 (the supervisor) or a live .tfcore/.session/yolo.json.
|
|
705
|
+
if rec.get("kind") == "run" and "yolo" not in rec:
|
|
706
|
+
on = os.environ.get("TF_YOLO") == "1"
|
|
707
|
+
if not on:
|
|
708
|
+
try:
|
|
709
|
+
fl = os.path.join(root, ".tfcore", ".session", "yolo.json")
|
|
710
|
+
on = os.path.isfile(fl) and (time.time() - os.path.getmtime(fl)) < 24 * 3600
|
|
711
|
+
except Exception:
|
|
712
|
+
on = False
|
|
713
|
+
rec["yolo"] = bool(on)
|
|
714
|
+
# A run record without `started` takes it from the command marker written by
|
|
715
|
+
# tf-phase.sh start (step 0 of every task), when the marker names the same command.
|
|
716
|
+
if rec.get("kind") == "run" and not rec.get("started"):
|
|
717
|
+
try:
|
|
718
|
+
with open(os.path.join(root, ".tfcore", ".session", "phase.json"), encoding="utf-8") as fh:
|
|
719
|
+
mk = json.load(fh) or {}
|
|
720
|
+
if mk.get("started") and (not rec.get("cmd") or mk.get("cmd") == rec.get("cmd")):
|
|
721
|
+
rec["started"] = mk["started"]
|
|
722
|
+
rec.setdefault("ended", NOW)
|
|
723
|
+
sys.stdout.write("tf-emit: started taken from the command marker (%s)\n" % mk["started"])
|
|
724
|
+
except Exception:
|
|
725
|
+
pass
|
|
726
|
+
# A run's `ended` is when the record is written, never a guess: an ended in the
|
|
727
|
+
# future (more than a minute past now) or before started is replaced with now and
|
|
728
|
+
# duration_s recomputed (MISS-TechieFlow-20260905-11: a day-1 run wrote 17:05 at 16:40).
|
|
729
|
+
# The same rule the other way round: a run record with NO `ended` was silently
|
|
730
|
+
# accepted and could never be costed — the effort-per-phase figure just lost it
|
|
731
|
+
# (MISS-TechieFlow-20260907-09, the session-6 record itself). `ended` is when the
|
|
732
|
+
# record is written, so an absent one is filled in, never left empty.
|
|
733
|
+
if rec.get("kind") == "run" and not isinstance(rec.get("ended"), str):
|
|
734
|
+
rec["ended"] = NOW
|
|
735
|
+
if rec.get("kind") == "run" and isinstance(rec.get("ended"), str):
|
|
736
|
+
t_end, t_now = _iso_ms(rec["ended"]), _iso_ms(NOW)
|
|
737
|
+
t_start = _iso_ms(rec["started"]) if isinstance(rec.get("started"), str) else None
|
|
738
|
+
# Two different faults, and they must not share a remedy.
|
|
739
|
+
#
|
|
740
|
+
# (a) `ended` a little in the future: `started` is still trustworthy, so clamp to
|
|
741
|
+
# now and re-derive (MISS-TechieFlow-20260905-11, a day-1 run wrote 17:05 at 16:40).
|
|
742
|
+
# (b) `ended` BEFORE `started`: the record contradicts itself and there is no way to
|
|
743
|
+
# tell which of the two is wrong, so it carries NO duration at all. Deriving one
|
|
744
|
+
# from the surviving timestamp invents a figure -- a TechieBlog record started
|
|
745
|
+
# 2026-08-22 would have been "measured" at 1,515,038 seconds on the day it was
|
|
746
|
+
# re-emitted. An honest gap beats a plausible number: SCHEMA §2.5.
|
|
747
|
+
if t_end is not None and t_now is not None and t_start is not None and t_end < t_start:
|
|
748
|
+
sys.stdout.write("tf-emit: ended %s precedes started %s; the record contradicts "
|
|
749
|
+
"itself, so no duration is stored\n" % (rec["ended"], rec["started"]))
|
|
750
|
+
rec["ended"] = NOW
|
|
751
|
+
rec.pop("duration_s", None)
|
|
752
|
+
rec["duration_unmeasured"] = "ended-before-started"
|
|
753
|
+
elif t_end is not None and t_now is not None and t_end > t_now + 60_000:
|
|
754
|
+
sys.stdout.write("tf-emit: ended %s is not a measurement (it lies in the future); "
|
|
755
|
+
"set to now, %s\n" % (rec["ended"], NOW))
|
|
756
|
+
rec["ended"] = NOW
|
|
757
|
+
if t_start is not None and "duration_s" in rec:
|
|
758
|
+
rec["duration_s"] = max(0, (t_now - t_start) // 1000)
|
|
759
|
+
elif t_end is not None and t_start is not None and isinstance(rec.get("duration_s"), (int, float)):
|
|
760
|
+
# A supplied duration that bears no relation to its own timestamps. Thirteen of
|
|
761
|
+
# TechieBlog's fourteen impossible records hid here, storing a plausible round
|
|
762
|
+
# number (3600, 2700, 1800, 900, 600) that nothing downstream could question --
|
|
763
|
+
# only the ONE record that stored the negative arithmetic was ever detectable.
|
|
764
|
+
span = (t_end - t_start) // 1000
|
|
765
|
+
if abs(span - rec["duration_s"]) > 1:
|
|
766
|
+
sys.stdout.write("tf-emit: duration_s %s disagrees with started/ended (%ss); "
|
|
767
|
+
"using the timestamps\n" % (rec["duration_s"], span))
|
|
768
|
+
rec["duration_s"] = span
|
|
769
|
+
# A run record that omits duration_s gets it from started and ended (the devguide and
|
|
770
|
+
# refresh-status records of 2026-09-06 had none; MISS-TechieFlow-20260906-10).
|
|
771
|
+
if rec.get("kind") == "run" and "duration_s" not in rec and rec.get("started") and rec.get("ended") \
|
|
772
|
+
and not rec.get("duration_unmeasured"):
|
|
773
|
+
try:
|
|
774
|
+
rec["duration_s"] = max(0, (_iso_ms(rec["ended"]) - _iso_ms(rec["started"])) // 1000)
|
|
775
|
+
except Exception:
|
|
776
|
+
pass
|
|
527
777
|
if "project_type" not in rec:
|
|
528
778
|
if PTYPE:
|
|
529
779
|
rec["project_type"] = PTYPE
|
|
@@ -531,7 +781,10 @@ def enrich(rec):
|
|
|
531
781
|
# Default, but NEVER silently — the report labels these unclassified.
|
|
532
782
|
rec["project_type"] = "app"
|
|
533
783
|
rec["project_type_inferred"] = True
|
|
534
|
-
|
|
784
|
+
if HARNESS:
|
|
785
|
+
rec["harness"] = HARNESS # detected, never declared (SCHEMA.md §1)
|
|
786
|
+
else:
|
|
787
|
+
rec.setdefault("harness", None)
|
|
535
788
|
rec.setdefault("app", APP)
|
|
536
789
|
return rec
|
|
537
790
|
|
|
@@ -574,6 +827,52 @@ def _routing():
|
|
|
574
827
|
|
|
575
828
|
ROUTING = _routing()
|
|
576
829
|
|
|
830
|
+
# --- how a model is paid for (SCHEMA.md §2.5b, added 2026-09-10) -----------
|
|
831
|
+
# One cost column cannot mean three things at once. A run on a flat-fee subscription records
|
|
832
|
+
# zero marginal dollars, a run on a metered API key records real money, and a run on a local
|
|
833
|
+
# model records zero because there is no bill — and until this field existed a reader could
|
|
834
|
+
# not tell those three zeroes apart, so every "what did it cost" figure quietly averaged real
|
|
835
|
+
# spending with free runs. The answer is resolved by tf-model-pick.sh, which reads OpenCode's
|
|
836
|
+
# own auth.json and model catalog, so it is looked up and never declared by an agent — the
|
|
837
|
+
# same rule as `harness` (§1) and `origin_model` (§5.5.1).
|
|
838
|
+
_BILLING_CACHE = {}
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _billing(model, harness):
|
|
842
|
+
key = (model, harness)
|
|
843
|
+
if key not in _BILLING_CACHE:
|
|
844
|
+
mode, source = "unknown", "none"
|
|
845
|
+
pick = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
846
|
+
if model and os.path.isfile(pick):
|
|
847
|
+
try:
|
|
848
|
+
out = subprocess.run(["bash", pick, "billing", model, harness or "claude"],
|
|
849
|
+
capture_output=True, text=True, timeout=20,
|
|
850
|
+
env=dict(os.environ, TF_PROJECT_DIR=root))
|
|
851
|
+
parts = (out.stdout or "").strip().split("\t")
|
|
852
|
+
if parts and parts[0] in ("subscription", "plan", "metered", "local", "unknown"):
|
|
853
|
+
mode = parts[0]
|
|
854
|
+
source = parts[1] if len(parts) > 1 else "tf-model-pick"
|
|
855
|
+
except Exception:
|
|
856
|
+
pass
|
|
857
|
+
_BILLING_CACHE[key] = (mode, source)
|
|
858
|
+
return _BILLING_CACHE[key]
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _was_limited(model, harness, at_epoch):
|
|
862
|
+
"""Did this run start while the tier's own model was parked? Then it is a deliberate
|
|
863
|
+
fallback, not routing drift, and `routed:false` should not be read as one."""
|
|
864
|
+
pick = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
865
|
+
if not model or not os.path.isfile(pick) or at_epoch is None:
|
|
866
|
+
return False
|
|
867
|
+
try:
|
|
868
|
+
out = subprocess.run(["bash", pick, "was-limited", harness or "claude", model, str(int(at_epoch))],
|
|
869
|
+
capture_output=True, text=True, timeout=20,
|
|
870
|
+
env=dict(os.environ, TF_PROJECT_DIR=root))
|
|
871
|
+
return out.returncode == 0
|
|
872
|
+
except Exception:
|
|
873
|
+
return False
|
|
874
|
+
|
|
875
|
+
|
|
577
876
|
def _iso_ms(s):
|
|
578
877
|
try:
|
|
579
878
|
return int(datetime.datetime.strptime(
|
|
@@ -747,6 +1046,97 @@ def _prior_runs():
|
|
|
747
1046
|
pass
|
|
748
1047
|
return _PRIOR_RUNS
|
|
749
1048
|
|
|
1049
|
+
# --- a run may not start before the last one finished (FR-72, 2026-09-10) --
|
|
1050
|
+
# `--void-run` (§2.7) gave a way to CORRECT a run record written with a start time nobody
|
|
1051
|
+
# measured. Nothing refused one, so the same mistake happened twice more the day the correction
|
|
1052
|
+
# mechanism shipped — two records whose `started` preceded the previous record's `ended`, one by
|
|
1053
|
+
# 42 minutes and one by nearly three hours, each silently adding that overlap to every total
|
|
1054
|
+
# (MISS-TechieFlow-20260910-04, sorted `ignored`: it was written down and not followed).
|
|
1055
|
+
#
|
|
1056
|
+
# A stream is append-only, so the only place to stop it is before the write. The rule: for one
|
|
1057
|
+
# app, a live `run` record may not begin before the last live `run` record ended. Voided and
|
|
1058
|
+
# backfilled records are ignored, which is what makes the void-then-re-emit flow work. Records
|
|
1059
|
+
# with no `started` are untouched.
|
|
1060
|
+
#
|
|
1061
|
+
# The one legitimate overlap is two machines appending to the same merge=union stream, so
|
|
1062
|
+
# `--allow-overlap` exists and the refusal message names it. It is deliberately a flag and not a
|
|
1063
|
+
# fallback: the mistake this refuses is easy to make and invisible afterwards.
|
|
1064
|
+
_LAST_END = None
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _last_live_end(app):
|
|
1068
|
+
global _LAST_END
|
|
1069
|
+
if _LAST_END is None:
|
|
1070
|
+
_LAST_END = {}
|
|
1071
|
+
voided = set()
|
|
1072
|
+
rows = []
|
|
1073
|
+
try:
|
|
1074
|
+
with open(os.path.join(met_dir, "runs.jsonl"), encoding="utf-8") as fh:
|
|
1075
|
+
for line in fh:
|
|
1076
|
+
line = line.strip()
|
|
1077
|
+
if not line:
|
|
1078
|
+
continue
|
|
1079
|
+
try:
|
|
1080
|
+
r = json.loads(line)
|
|
1081
|
+
except Exception:
|
|
1082
|
+
continue
|
|
1083
|
+
if r.get("kind") == "run-void":
|
|
1084
|
+
voided.add((r.get("app"), r.get("cmd"), r.get("started")))
|
|
1085
|
+
elif r.get("kind", "run") == "run" and not r.get("backfilled"):
|
|
1086
|
+
rows.append(r)
|
|
1087
|
+
except Exception:
|
|
1088
|
+
pass
|
|
1089
|
+
for r in rows:
|
|
1090
|
+
if (r.get("app"), r.get("cmd"), r.get("started")) in voided:
|
|
1091
|
+
continue
|
|
1092
|
+
# `ended` ONLY. `ts` is when the record was WRITTEN, not when the run finished, so
|
|
1093
|
+
# falling back to it would make every historical record emitted today look like an
|
|
1094
|
+
# overlap with the record written a second ago. A run with no `ended` has no window
|
|
1095
|
+
# and constrains nothing.
|
|
1096
|
+
#
|
|
1097
|
+
# And only a record whose window is COHERENT: a self-contradicting one (ended before
|
|
1098
|
+
# started) has its `ended` replaced with the write time by the check above, and
|
|
1099
|
+
# marked by a null `duration_s`. That placeholder is not a measurement, so it must
|
|
1100
|
+
# not become a boundary — it would block every later record for the same app.
|
|
1101
|
+
if not isinstance(r.get("duration_s"), (int, float)):
|
|
1102
|
+
continue
|
|
1103
|
+
end = r.get("ended")
|
|
1104
|
+
if not end:
|
|
1105
|
+
continue
|
|
1106
|
+
a = r.get("app")
|
|
1107
|
+
if a not in _LAST_END or end > _LAST_END[a][0]:
|
|
1108
|
+
_LAST_END[a] = (end, r.get("cmd"), r.get("started"))
|
|
1109
|
+
return _LAST_END.get(app)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _overlap_ok(rec):
|
|
1113
|
+
"""False (and the reason printed) when this run would start before the last one ended."""
|
|
1114
|
+
if stream != "runs" or rec.get("kind", "run") != "run" or rec.get("backfilled"):
|
|
1115
|
+
return True
|
|
1116
|
+
if os.environ.get("TF_ALLOW_OVERLAP") == "1":
|
|
1117
|
+
return True
|
|
1118
|
+
started = rec.get("started")
|
|
1119
|
+
if not started:
|
|
1120
|
+
return True
|
|
1121
|
+
prev = _last_live_end(rec.get("app"))
|
|
1122
|
+
if not prev:
|
|
1123
|
+
return True
|
|
1124
|
+
a, b = _iso_ms(started), _iso_ms(prev[0])
|
|
1125
|
+
if a is not None and b is not None:
|
|
1126
|
+
if a >= b:
|
|
1127
|
+
return True
|
|
1128
|
+
elif started >= prev[0]: # unparseable timestamp: fall back to the string order
|
|
1129
|
+
return True
|
|
1130
|
+
sys.stdout.write(
|
|
1131
|
+
"tf-emit: REFUSED — this run starts at %s, before the previous run for %s ended at %s "
|
|
1132
|
+
"(%s started %s). The two would overlap and every total over them would be that much too "
|
|
1133
|
+
"big. Use the previous run's `ended` as this one's `started`, or pass --allow-overlap if "
|
|
1134
|
+
"they really did run at the same time (a second machine on the same stream). "
|
|
1135
|
+
"Nothing was appended.\n"
|
|
1136
|
+
% (started, rec.get("app"), prev[0], prev[1], prev[2]))
|
|
1137
|
+
return False
|
|
1138
|
+
|
|
1139
|
+
|
|
750
1140
|
def stamp_attempt(rec):
|
|
751
1141
|
if stream != "runs" or "attempt" in rec or rec.get("backfilled"):
|
|
752
1142
|
return rec
|
|
@@ -771,7 +1161,7 @@ def enrich_run(rec):
|
|
|
771
1161
|
tier = ROUTING["phases"].get(cmd)
|
|
772
1162
|
if tier and tier != "inherit":
|
|
773
1163
|
rec["tier"] = tier
|
|
774
|
-
key = {"claude-code": "claude", "opencode": "opencode"
|
|
1164
|
+
key = {"claude-code": "claude", "opencode": "opencode"}.get(HARNESS)
|
|
775
1165
|
tm = ROUTING["tiers"].get(tier, {}).get(key) if key else None
|
|
776
1166
|
if tm:
|
|
777
1167
|
rec["tier_model"] = tm
|
|
@@ -812,8 +1202,21 @@ def enrich_run(rec):
|
|
|
812
1202
|
rec["tokens_out_subagents"] = win.get("sub_out", 0)
|
|
813
1203
|
if rec.get("tier_model") and rec.get("model"):
|
|
814
1204
|
rec["routed"] = rec["model"] == rec["tier_model"]
|
|
1205
|
+
# What a cost figure from this window MEANS. Several models in one window may
|
|
1206
|
+
# be paid for differently — a fallback from a subscription to a metered key is
|
|
1207
|
+
# exactly that case — so a mixed window says so rather than picking a winner.
|
|
1208
|
+
modes = {_billing(m, HARNESS)[0] for m in models} or {_billing(rec.get("model"), HARNESS)[0]}
|
|
1209
|
+
rec["billing_mode"] = modes.pop() if len(modes) == 1 else "mixed"
|
|
1210
|
+
rec["cost_source"] = "opencode-db" if HARNESS == "opencode" else "none"
|
|
1211
|
+
# `routed:false` because the tier model was out of quota is a different fact from
|
|
1212
|
+
# `routed:false` because something drifted. Only the first names a model here.
|
|
1213
|
+
tm = rec.get("tier_model")
|
|
1214
|
+
if tm and rec.get("model") and rec["model"] != tm and _was_limited(tm, HARNESS, t0 / 1000.0):
|
|
1215
|
+
rec["fallback_from"] = tm
|
|
815
1216
|
else:
|
|
816
1217
|
rec["tokens_scope"] = "none"
|
|
1218
|
+
rec["billing_mode"] = _billing(rec.get("tier_model"), HARNESS)[0]
|
|
1219
|
+
rec["cost_source"] = "none"
|
|
817
1220
|
except Exception as e:
|
|
818
1221
|
warn("run enrichment failed (%s) — record kept unenriched" % e)
|
|
819
1222
|
return rec
|
|
@@ -881,7 +1284,7 @@ def _runs_by_start():
|
|
|
881
1284
|
return _RUNS_BY_START
|
|
882
1285
|
|
|
883
1286
|
_COST_FIELDS = ("tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
884
|
-
"cost_usd", "tokens_scope", "model")
|
|
1287
|
+
"cost_usd", "tokens_scope", "model", "billing_mode", "cost_source")
|
|
885
1288
|
|
|
886
1289
|
# Kept identical to the --amend branch above. Two doors, ONE enforcement: an
|
|
887
1290
|
# agent that hand-writes a miss-amend record onto the stream faces the same
|
|
@@ -891,14 +1294,23 @@ _AMENDABLE = {
|
|
|
891
1294
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
892
1295
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
893
1296
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
1297
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
894
1298
|
}
|
|
1299
|
+
_AMENDABLE_TEXT = ("what",) # free text, guarded by the never-overwrite rule (see --amend)
|
|
895
1300
|
|
|
896
1301
|
def _amend_ok(rec):
|
|
897
1302
|
"""True when this miss-amend may be appended. Returning False DROPS it —
|
|
898
1303
|
an invalid amend writes nothing rather than landing as a record the reader
|
|
899
1304
|
then has to defend itself against."""
|
|
900
1305
|
mid, fld, val = rec.get("miss_id"), rec.get("field"), rec.get("value")
|
|
901
|
-
if
|
|
1306
|
+
if not mid:
|
|
1307
|
+
warn("amend refused — no miss_id")
|
|
1308
|
+
return False
|
|
1309
|
+
if fld in _AMENDABLE_TEXT:
|
|
1310
|
+
if not isinstance(val, str) or not val.strip():
|
|
1311
|
+
warn("amend refused — %s must be a non-empty sentence" % fld)
|
|
1312
|
+
return False
|
|
1313
|
+
elif fld not in _AMENDABLE or val not in _AMENDABLE[fld]:
|
|
902
1314
|
warn("amend refused — %r/%r outside the §5.5.7 allowlist" % (fld, val))
|
|
903
1315
|
return False
|
|
904
1316
|
parent, current = None, None
|
|
@@ -952,6 +1364,14 @@ _MISS_ENUMS = {
|
|
|
952
1364
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
953
1365
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
954
1366
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
1367
|
+
# whose gap (FR-32, Session 5 2026-09-07): spec = the app's spec did not say it (fix the
|
|
1368
|
+
# checklist line) · unsaid = the framework never said it (a requirement line plus a check) ·
|
|
1369
|
+
# weak-check = a check existed and did not catch it (fix the check) · ignored = it was written
|
|
1370
|
+
# and not followed (a hook, or delete the rule)
|
|
1371
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
1372
|
+
}
|
|
1373
|
+
_REVIEW_ENUMS = {
|
|
1374
|
+
"phase": ("day1-review", "build-review", "verify-review", "handoff-review"),
|
|
955
1375
|
}
|
|
956
1376
|
_FIX_ENUMS = {
|
|
957
1377
|
"verdict_after": ("Verified", "Needs re-verify", "FAIL", "deferred", "wont-fix"),
|
|
@@ -975,6 +1395,81 @@ def _enums_ok(rec, table):
|
|
|
975
1395
|
return True
|
|
976
1396
|
|
|
977
1397
|
|
|
1398
|
+
# --- shape check (added 2026-09-05, reset Sitting 4a) ------------------------
|
|
1399
|
+
# A record may carry only the fields SCHEMA.md defines for its kind (plus the
|
|
1400
|
+
# ones this script injects). A `miss` must carry a miss_id from --next-miss-id.
|
|
1401
|
+
# Anything else is REFUSED on stdout and never appended, the same choice as
|
|
1402
|
+
# _enums_ok(): a field nobody defined is a field no report can read.
|
|
1403
|
+
_COMMON = {"v", "ts", "kind", "app", "project_type", "project_type_inferred", "harness", "backfilled"}
|
|
1404
|
+
_ALLOWED = {
|
|
1405
|
+
("runs", "run"): {"cmd", "mode", "yolo", "started", "ended", "duration_s", "reqs_touched", "reqs_count",
|
|
1406
|
+
"subagents", "files_written", "build_result", "tier", "tier_model", "model", "models",
|
|
1407
|
+
"routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
1408
|
+
"cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
|
|
1409
|
+
"model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts",
|
|
1410
|
+
"billing_mode", "cost_source", "fallback_from"},
|
|
1411
|
+
# A correction, not a deletion: it names a run record that is wrong by the pair every
|
|
1412
|
+
# run carries (`cmd` + `started`) and says why, so every figure can skip it while the
|
|
1413
|
+
# record itself stays where it is. Written by --void-run, which refuses one that names
|
|
1414
|
+
# no run. SCHEMA.md §2.7; tests/regression/run.sh tf_void.
|
|
1415
|
+
("runs", "run-void"): {"cmd", "started", "reason"},
|
|
1416
|
+
("gates", "gate"): {"run_id", "req_id", "req_class", "attempt", "verdict", "gate", "gates_run",
|
|
1417
|
+
"failure_class", "prior_verdict", "proof", "escaped", "phase", "result", "detail", "at",
|
|
1418
|
+
"tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1419
|
+
"tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
|
|
1420
|
+
"subagent_runs", "tokens_out_subagents", "session_id",
|
|
1421
|
+
"billing_mode", "cost_source", "fallback_from"},
|
|
1422
|
+
("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
|
|
1423
|
+
"cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
|
|
1424
|
+
("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
|
|
1425
|
+
("misses", "miss"): {"miss_id", "req_id", "req_class", "miss_class", "artifact", "severity", "origin_phase",
|
|
1426
|
+
"origin_agent", "origin_run_id", "origin_model", "origin_harness", "origin_confidence",
|
|
1427
|
+
"why_missed", "found_by", "found_phase", "found_gate", "found_run_id", "failure_class",
|
|
1428
|
+
"what", "sort"},
|
|
1429
|
+
("misses", "miss-fix"): {"miss_id", "req_id", "fix_run_id", "fix_cmd", "fix_attempt", "verdict_after",
|
|
1430
|
+
"reopened", "cost_attribution", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1431
|
+
"tokens_cache_write", "cost_usd", "tokens_scope", "model",
|
|
1432
|
+
"billing_mode", "cost_source"},
|
|
1433
|
+
("misses", "miss-amend"): {"miss_id", "field", "value"},
|
|
1434
|
+
# an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
|
|
1435
|
+
# given, what producing the reviewed output cost, what applying the corrections cost. The two
|
|
1436
|
+
# costs are copied from the runs named by reviewed_run_id and correction_run_id (their `started`).
|
|
1437
|
+
("misses", "review"): {"phase", "reviewed_run_id", "correction_run_id", "corrections", "what",
|
|
1438
|
+
"tokens_produce", "tokens_correct", "cost_produce_usd", "cost_correct_usd",
|
|
1439
|
+
"model_produce", "model_correct"},
|
|
1440
|
+
}
|
|
1441
|
+
# Fields the emitter derives for runs/gates: a caller's value is discarded.
|
|
1442
|
+
_DERIVED = ("harness", "tier", "tier_model", "model", "models", "model_tokens_out", "routed",
|
|
1443
|
+
"tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write", "cost_usd",
|
|
1444
|
+
"tokens_scope", "subagent_runs", "tokens_out_subagents",
|
|
1445
|
+
"billing_mode", "cost_source", "fallback_from")
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def _shape_ok(rec):
|
|
1449
|
+
kind = rec.get("kind")
|
|
1450
|
+
if not isinstance(rec, dict) or not kind:
|
|
1451
|
+
sys.stdout.write("tf-emit: REFUSED — record has no \"kind\" (SCHEMA.md §1). Nothing was appended.\n")
|
|
1452
|
+
return False
|
|
1453
|
+
allowed = _ALLOWED.get((stream, kind))
|
|
1454
|
+
if allowed is None:
|
|
1455
|
+
sys.stdout.write("tf-emit: REFUSED — kind %r is not a record kind of the %s stream. Nothing was appended.\n" % (kind, stream))
|
|
1456
|
+
return False
|
|
1457
|
+
unknown = sorted(set(rec) - allowed - _COMMON)
|
|
1458
|
+
if unknown:
|
|
1459
|
+
sys.stdout.write("tf-emit: REFUSED — field(s) not in SCHEMA.md for a %s record: %s. Add the field to the schema first, or drop it. Nothing was appended.\n" % (kind, ", ".join(unknown)))
|
|
1460
|
+
return False
|
|
1461
|
+
if kind == "run" and rec.get("build_result") not in (None, "pass", "fail", "not-run"):
|
|
1462
|
+
sys.stdout.write("tf-emit: REFUSED — build_result must be pass, fail or not-run (SCHEMA.md §2); got %r. Put the detail in the checklist Remarks or Known blockers, not here. Nothing was appended.\n" % (rec.get("build_result"),))
|
|
1463
|
+
return False
|
|
1464
|
+
if kind == "miss" and not rec.get("miss_id"):
|
|
1465
|
+
sys.stdout.write("tf-emit: REFUSED — a miss record needs a miss_id; get one with: bash .tfcore/utils/tf-emit.sh --next-miss-id. Nothing was appended.\n")
|
|
1466
|
+
return False
|
|
1467
|
+
if stream in ("runs", "gates") and not rec.get("backfilled"):
|
|
1468
|
+
for f in _DERIVED:
|
|
1469
|
+
rec.pop(f, None)
|
|
1470
|
+
return True
|
|
1471
|
+
|
|
1472
|
+
|
|
978
1473
|
def enrich_miss(rec):
|
|
979
1474
|
if stream != "misses":
|
|
980
1475
|
return rec
|
|
@@ -984,6 +1479,21 @@ def enrich_miss(rec):
|
|
|
984
1479
|
return None
|
|
985
1480
|
if rec.get("kind") == "miss-fix" and not _enums_ok(rec, _FIX_ENUMS):
|
|
986
1481
|
return None
|
|
1482
|
+
if rec.get("kind") == "review":
|
|
1483
|
+
if not _enums_ok(rec, _REVIEW_ENUMS):
|
|
1484
|
+
return None
|
|
1485
|
+
if not isinstance(rec.get("corrections"), int) or rec["corrections"] < 0:
|
|
1486
|
+
sys.stdout.write("tf-emit: REFUSED — a review record needs \"corrections\", a whole number of corrections the owner gave. Nothing was appended.\n")
|
|
1487
|
+
return None
|
|
1488
|
+
for f in ("tokens_produce", "tokens_correct", "cost_produce_usd", "cost_correct_usd", "model_produce", "model_correct"):
|
|
1489
|
+
rec.pop(f, None) # derived, never a caller's figure
|
|
1490
|
+
runs = _runs_by_start()
|
|
1491
|
+
for side, key in (("produce", "reviewed_run_id"), ("correct", "correction_run_id")):
|
|
1492
|
+
src = runs.get(rec.get(key)) if rec.get(key) else None
|
|
1493
|
+
rec["tokens_" + side] = src.get("tokens_out") if src else None
|
|
1494
|
+
rec["cost_" + side + "_usd"] = src.get("cost_usd") if src else None
|
|
1495
|
+
rec["model_" + side] = src.get("model") if src else None
|
|
1496
|
+
return rec
|
|
987
1497
|
try:
|
|
988
1498
|
kind = rec.get("kind")
|
|
989
1499
|
runs = _runs_by_start()
|
|
@@ -1003,9 +1513,11 @@ def enrich_miss(rec):
|
|
|
1003
1513
|
rec["origin_harness"] = None
|
|
1004
1514
|
elif kind == "miss-fix":
|
|
1005
1515
|
src = runs.get(rec.get("fix_run_id")) if rec.get("fix_run_id") else None
|
|
1516
|
+
for f in _COST_FIELDS:
|
|
1517
|
+
rec.pop(f, None) # a caller's cost figure is never kept
|
|
1006
1518
|
if src:
|
|
1007
1519
|
for f in _COST_FIELDS:
|
|
1008
|
-
if f in src
|
|
1520
|
+
if f in src:
|
|
1009
1521
|
rec[f] = src[f]
|
|
1010
1522
|
if "cost_attribution" not in rec:
|
|
1011
1523
|
touched = src.get("reqs_touched") or []
|
|
@@ -1048,7 +1560,14 @@ def enrich_miss(rec):
|
|
|
1048
1560
|
# .gitattributes block pins to LF. These files are LF on every platform.
|
|
1049
1561
|
lines = []
|
|
1050
1562
|
for rec in records:
|
|
1051
|
-
|
|
1563
|
+
if not _shape_ok(rec):
|
|
1564
|
+
continue
|
|
1565
|
+
# enrich() first, because it is what fills `app` when the caller left it out — the overlap
|
|
1566
|
+
# rule is per app and would otherwise compare a named run against an unnamed one.
|
|
1567
|
+
base = enrich(rec)
|
|
1568
|
+
if not _overlap_ok(base):
|
|
1569
|
+
continue
|
|
1570
|
+
out = enrich_miss(enrich_run(stamp_attempt(base)))
|
|
1052
1571
|
if out is None: # a refused miss-amend (§5.5.7) — dropped, never appended
|
|
1053
1572
|
continue
|
|
1054
1573
|
line = json.dumps(out, separators=(",", ":"), ensure_ascii=False)
|
|
@@ -1072,4 +1591,12 @@ PY
|
|
|
1072
1591
|
)"
|
|
1073
1592
|
python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
|
|
1074
1593
|
|
|
1594
|
+
# The readable miss list beside the record (FR-31, Session 5 2026-09-07): after any write to the
|
|
1595
|
+
# misses stream, docs/<App>-Misses.md is rebuilt from the stream, so the file is never older than
|
|
1596
|
+
# the record. Markdown only — the miss log is an agent document and is never rendered to HTML
|
|
1597
|
+
# (owner, 2026-09-08). Best effort, like everything else here.
|
|
1598
|
+
if [[ "$STREAM" == "misses" ]]; then
|
|
1599
|
+
python3 "$(dirname "${BASH_SOURCE[0]}")/tf-misses-md.py" --root "$ROOT" --quiet 2>"$TF_ERR" || true
|
|
1600
|
+
fi
|
|
1601
|
+
|
|
1075
1602
|
exit 0
|