@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
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 --void-miss MISS-App-20260919-01 "logged again from an earlier triage's leftovers"
|
|
27
|
+
# # the same for a WRONG miss (SCHEMA.md §5.5.11)
|
|
22
28
|
# tf-emit.sh --origin-of REQ-UI-014 # prints "<started> <cmd> <agent>" of the last build or fix
|
|
23
29
|
# # run that touched the row, nothing when there is none
|
|
24
30
|
# tf-emit.sh --where # prints the resolved docs/metrics dir
|
|
@@ -270,13 +276,72 @@ fi
|
|
|
270
276
|
# patience rather than quality. Open = no miss-fix, or the latest miss-fix
|
|
271
277
|
# closed with a verdict_after other than "Verified". Prints "<miss_id>
|
|
272
278
|
# <miss_class>" for the most recent open miss, or nothing at all.
|
|
279
|
+
# --- list the open misses whose deficient artifact is a document ----------
|
|
280
|
+
# `*amend-docs` is the command that edits a BRD, an Architecture or a mockup, so it is
|
|
281
|
+
# the only command that can close a miss whose artifact is one of those -- and it had no
|
|
282
|
+
# way to find them. Every other close path works off a checklist row the verifier touched,
|
|
283
|
+
# which a BRD edit never is, so a document miss stayed open forever (TF-016: twelve
|
|
284
|
+
# finished TfLens items showed as outstanding for up to two weeks).
|
|
285
|
+
#
|
|
286
|
+
# tf-emit.sh --open-misses <App> [--artifact-class doc]
|
|
287
|
+
#
|
|
288
|
+
# Prints one line per open miss: "<miss_id> <artifact> <what>". Open means no miss-fix
|
|
289
|
+
# carries verdict_after Verified, the same test --open-miss applies.
|
|
290
|
+
if [[ "$1" == "--open-misses" ]]; then
|
|
291
|
+
OMAPP="${2:-}"; OMCLASS=""
|
|
292
|
+
[[ "${3:-}" == "--artifact-class" ]] && OMCLASS="${4:-}"
|
|
293
|
+
TF_OMAPP="$OMAPP" TF_OMCLASS="$OMCLASS" python3 - "$MET_DIR/misses.jsonl" 2>"$TF_ERR" <<'PY2' || true
|
|
294
|
+
import json, os, sys
|
|
295
|
+
DOC = {"brd", "architecture", "uidesign", "checklist", "devguide"}
|
|
296
|
+
app, klass = os.environ.get("TF_OMAPP") or "", os.environ.get("TF_OMCLASS") or ""
|
|
297
|
+
opened, fixes, voided = [], {}, set()
|
|
298
|
+
try:
|
|
299
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
300
|
+
for line in fh:
|
|
301
|
+
line = line.strip()
|
|
302
|
+
if not line:
|
|
303
|
+
continue
|
|
304
|
+
try:
|
|
305
|
+
r = json.loads(line)
|
|
306
|
+
except Exception:
|
|
307
|
+
continue
|
|
308
|
+
if r.get("kind") == "miss":
|
|
309
|
+
opened.append(r)
|
|
310
|
+
elif r.get("kind") == "miss-void":
|
|
311
|
+
voided.add(r.get("miss_id"))
|
|
312
|
+
elif r.get("kind") == "miss-fix":
|
|
313
|
+
mid = r.get("miss_id")
|
|
314
|
+
if mid:
|
|
315
|
+
prev = fixes.get(mid)
|
|
316
|
+
if prev is None or (r.get("ts") or "") >= (prev.get("ts") or ""):
|
|
317
|
+
fixes[mid] = r
|
|
318
|
+
except FileNotFoundError:
|
|
319
|
+
pass
|
|
320
|
+
for r in opened:
|
|
321
|
+
if r.get("miss_id") in voided:
|
|
322
|
+
continue
|
|
323
|
+
if app and r.get("app") and r.get("app") != app:
|
|
324
|
+
continue
|
|
325
|
+
art = r.get("artifact") or ""
|
|
326
|
+
if klass == "doc" and art not in DOC:
|
|
327
|
+
continue
|
|
328
|
+
if klass and klass != "doc" and art != klass:
|
|
329
|
+
continue
|
|
330
|
+
fix = fixes.get(r.get("miss_id"))
|
|
331
|
+
if fix is not None and fix.get("verdict_after") == "Verified":
|
|
332
|
+
continue
|
|
333
|
+
print("%s %s %s" % (r.get("miss_id"), art or "-", (r.get("what") or "")[:110]))
|
|
334
|
+
PY2
|
|
335
|
+
exit 0
|
|
336
|
+
fi
|
|
337
|
+
|
|
273
338
|
if [[ "$1" == "--open-miss" ]]; then
|
|
274
339
|
REQ="$2"
|
|
275
340
|
[[ -n "$REQ" ]] || exit 0
|
|
276
341
|
python3 - "$MET_DIR/misses.jsonl" "$REQ" 2>"$TF_ERR" <<'PY' || true
|
|
277
342
|
import json, sys
|
|
278
343
|
path, req = sys.argv[1], sys.argv[2]
|
|
279
|
-
opened, fixes = [], {}
|
|
344
|
+
opened, fixes, voided = [], {}, set()
|
|
280
345
|
try:
|
|
281
346
|
with open(path, encoding="utf-8") as fh:
|
|
282
347
|
for line in fh:
|
|
@@ -289,6 +354,8 @@ try:
|
|
|
289
354
|
continue
|
|
290
355
|
if r.get("kind") == "miss" and r.get("req_id") == req:
|
|
291
356
|
opened.append(r)
|
|
357
|
+
elif r.get("kind") == "miss-void":
|
|
358
|
+
voided.add(r.get("miss_id"))
|
|
292
359
|
elif r.get("kind") == "miss-fix":
|
|
293
360
|
mid = r.get("miss_id")
|
|
294
361
|
if mid:
|
|
@@ -298,6 +365,8 @@ try:
|
|
|
298
365
|
except FileNotFoundError:
|
|
299
366
|
pass
|
|
300
367
|
for r in reversed(opened): # most recent first
|
|
368
|
+
if r.get("miss_id") in voided:
|
|
369
|
+
continue
|
|
301
370
|
fix = fixes.get(r.get("miss_id"))
|
|
302
371
|
if fix is None or fix.get("verdict_after") != "Verified":
|
|
303
372
|
print("%s %s" % (r.get("miss_id"), r.get("miss_class") or "other"))
|
|
@@ -344,9 +413,19 @@ AMENDABLE = {
|
|
|
344
413
|
# still make later, so a miss logged before the field existed can be sorted now.
|
|
345
414
|
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
346
415
|
}
|
|
347
|
-
|
|
416
|
+
# `what` is the miss's own sentence, and it is FREE TEXT -- the one field here that
|
|
417
|
+
# is not a closed vocabulary. It is allowed because the protection was never the
|
|
418
|
+
# vocabulary: an amend may only fill a field that is still null and can never
|
|
419
|
+
# overwrite one that is set (below), so free text cannot rewrite a fact, only
|
|
420
|
+
# supply one that was missing. Validated as non-empty text instead.
|
|
421
|
+
# MISS-TechieFlow-20260909-02; tests/regression/run.sh tf_018.
|
|
422
|
+
AMENDABLE_TEXT = ("what",)
|
|
423
|
+
if fld not in AMENDABLE and fld not in AMENDABLE_TEXT:
|
|
348
424
|
print("REFUSED %s is not an amendable field (SCHEMA.md §5.5.7)" % fld); raise SystemExit(0)
|
|
349
|
-
if
|
|
425
|
+
if fld in AMENDABLE_TEXT:
|
|
426
|
+
if not isinstance(val, str) or not val.strip():
|
|
427
|
+
print("REFUSED %s must be a non-empty sentence" % fld); raise SystemExit(0)
|
|
428
|
+
elif val not in AMENDABLE[fld]:
|
|
350
429
|
print("REFUSED %r is not in the closed vocabulary for %s" % (val, fld)); raise SystemExit(0)
|
|
351
430
|
|
|
352
431
|
parent, current = None, None
|
|
@@ -389,8 +468,116 @@ PY
|
|
|
389
468
|
exit 0
|
|
390
469
|
fi
|
|
391
470
|
|
|
471
|
+
# --- write helper: withdraw a miss that should never have been written -----
|
|
472
|
+
# SCHEMA.md §5.5.11 — --void-run's answer for the misses stream (TrBlazeUI TF-001).
|
|
473
|
+
if [[ "$1" == "--void-miss" ]]; then
|
|
474
|
+
VMID="$2"; VWHY="$3"
|
|
475
|
+
VREC="$(python3 - "$MET_DIR/misses.jsonl" "$VMID" "$VWHY" 2>"$TF_ERR" <<'PY' || true
|
|
476
|
+
import json, sys
|
|
477
|
+
path, mid, why = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
478
|
+
found, voided = False, False
|
|
479
|
+
try:
|
|
480
|
+
for line in open(path, encoding="utf-8"):
|
|
481
|
+
try:
|
|
482
|
+
r = json.loads(line)
|
|
483
|
+
except Exception:
|
|
484
|
+
continue
|
|
485
|
+
if r.get("miss_id") == mid:
|
|
486
|
+
found = found or r.get("kind") == "miss"
|
|
487
|
+
voided = voided or r.get("kind") == "miss-void"
|
|
488
|
+
except FileNotFoundError:
|
|
489
|
+
pass
|
|
490
|
+
if not found:
|
|
491
|
+
print("REFUSED no miss record %s on this stream" % mid)
|
|
492
|
+
elif voided:
|
|
493
|
+
print("REFUSED %s is already voided — one void is enough" % mid)
|
|
494
|
+
elif not why.strip():
|
|
495
|
+
print("REFUSED a void needs a reason a reader can check")
|
|
496
|
+
else:
|
|
497
|
+
print("OK " + json.dumps({"kind": "miss-void", "miss_id": mid, "reason": why.strip()},
|
|
498
|
+
separators=(",", ":"), ensure_ascii=False))
|
|
499
|
+
PY
|
|
500
|
+
)"
|
|
501
|
+
case "$VREC" in
|
|
502
|
+
"OK "*) printf '%s' "${VREC#OK }" | bash "${BASH_SOURCE[0]}" misses
|
|
503
|
+
echo "tf-emit: voided $VMID — $VWHY" ;;
|
|
504
|
+
"REFUSED "*) echo "tf-emit: void refused — ${VREC#REFUSED }" ;;
|
|
505
|
+
*) echo "tf-emit: void could not be evaluated — nothing written" ;;
|
|
506
|
+
esac
|
|
507
|
+
exit 0
|
|
508
|
+
fi
|
|
509
|
+
|
|
510
|
+
# --- write helper: mark a run record as not to be counted -----------------
|
|
511
|
+
# SCHEMA.md §2.7. The same doctrine as --amend, one stream over: a run record that
|
|
512
|
+
# is WRONG cannot be edited or deleted, because the stream is append-only and the
|
|
513
|
+
# evidence that it happened is worth keeping. So the correction is another record.
|
|
514
|
+
#
|
|
515
|
+
# tf-emit.sh --void-run <cmd> <started> "<why it is wrong>"
|
|
516
|
+
#
|
|
517
|
+
# It names the run by its own `cmd` and `started` — the pair every run record
|
|
518
|
+
# carries and the key the readers already use — and every figure then skips it,
|
|
519
|
+
# with the count and the reasons printed. A void that names no run on this stream
|
|
520
|
+
# is refused: it would be a claim about nothing. It is deliberately NOT a delete:
|
|
521
|
+
# both records stay, and a reader can see what was corrected and why.
|
|
522
|
+
if [[ "$1" == "--void-run" ]]; then
|
|
523
|
+
VCMD="$2"; VSTART="$3"; VWHY="$4"
|
|
524
|
+
if [[ -z "$VCMD" || -z "$VSTART" || -z "$VWHY" ]]; then
|
|
525
|
+
echo "tf-emit: --void-run needs <cmd> <started> \"<reason>\""
|
|
526
|
+
exit 0
|
|
527
|
+
fi
|
|
528
|
+
VREC="$(python3 - "$MET_DIR/runs.jsonl" "$VCMD" "$VSTART" "$VWHY" 2>"$TF_ERR" <<'PY' || true
|
|
529
|
+
import json, sys
|
|
530
|
+
path, cmd, started, why = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
|
531
|
+
target, voided, app = None, False, None
|
|
532
|
+
try:
|
|
533
|
+
with open(path, encoding="utf-8") as fh:
|
|
534
|
+
for line in fh:
|
|
535
|
+
line = line.strip()
|
|
536
|
+
if not line:
|
|
537
|
+
continue
|
|
538
|
+
try:
|
|
539
|
+
r = json.loads(line)
|
|
540
|
+
except Exception:
|
|
541
|
+
continue
|
|
542
|
+
if r.get("cmd") == cmd and r.get("started") == started:
|
|
543
|
+
if r.get("kind") == "run-void":
|
|
544
|
+
voided = True
|
|
545
|
+
elif r.get("kind", "run") == "run":
|
|
546
|
+
target = r
|
|
547
|
+
app = r.get("app")
|
|
548
|
+
except FileNotFoundError:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
if target is None:
|
|
552
|
+
print("REFUSED no run record %s started %s on this stream" % (cmd, started)); raise SystemExit(0)
|
|
553
|
+
if voided:
|
|
554
|
+
print("REFUSED %s started %s is already voided — one void is enough" % (cmd, started)); raise SystemExit(0)
|
|
555
|
+
if not why.strip():
|
|
556
|
+
print("REFUSED a void needs a reason a reader can check"); raise SystemExit(0)
|
|
557
|
+
|
|
558
|
+
print("OK " + json.dumps({"kind": "run-void", "app": app, "cmd": cmd,
|
|
559
|
+
"started": started, "reason": why.strip()},
|
|
560
|
+
separators=(",", ":"), ensure_ascii=False))
|
|
561
|
+
PY
|
|
562
|
+
)"
|
|
563
|
+
case "$VREC" in
|
|
564
|
+
"OK "*) printf '%s' "${VREC#OK }" | bash "${BASH_SOURCE[0]}" runs
|
|
565
|
+
echo "tf-emit: voided $VCMD started $VSTART — $VWHY"
|
|
566
|
+
echo " both records stay on the stream; every figure now skips the run and says so" ;;
|
|
567
|
+
"REFUSED "*) echo "tf-emit: void refused — ${VREC#REFUSED }" ;;
|
|
568
|
+
*) echo "tf-emit: void could not be evaluated — nothing written" ;;
|
|
569
|
+
esac
|
|
570
|
+
exit 0
|
|
571
|
+
fi
|
|
572
|
+
|
|
392
573
|
# --- append path ----------------------------------------------------------
|
|
393
574
|
STREAM="$1"
|
|
575
|
+
# --allow-overlap: this run really did happen while another was still open (a second machine
|
|
576
|
+
# appending to the same merge=union stream is the only case that has ever come up). Without it a
|
|
577
|
+
# run whose `started` precedes the previous run's `ended` is refused — see _overlap_ok below.
|
|
578
|
+
TF_ALLOW_OVERLAP=0
|
|
579
|
+
for _a in "$@"; do [[ "$_a" == "--allow-overlap" ]] && TF_ALLOW_OVERLAP=1; done
|
|
580
|
+
export TF_ALLOW_OVERLAP
|
|
394
581
|
case "$STREAM" in
|
|
395
582
|
runs|gates|sessions|commits|misses) ;;
|
|
396
583
|
*) _warn "unknown stream '${STREAM:-<none>}' — event dropped"; exit 0 ;;
|
|
@@ -398,8 +585,8 @@ esac
|
|
|
398
585
|
|
|
399
586
|
# The program is captured into a variable rather than fed on stdin — a heredoc
|
|
400
587
|
# on `python3 -` would REPLACE the caller's piped JSON and silently drop every event.
|
|
401
|
-
|
|
402
|
-
import json, os, sys, datetime, glob, re, time
|
|
588
|
+
IFS= read -r -d '' TF_PROG <<'PY' || true # bash 3.2 misreads a here-document inside a command substitution
|
|
589
|
+
import json, os, sys, datetime, glob, re, subprocess, time
|
|
403
590
|
|
|
404
591
|
met_dir, stream, root = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
405
592
|
dbg = os.environ.get("TF_METRICS_DEBUG")
|
|
@@ -597,15 +784,41 @@ def enrich(rec):
|
|
|
597
784
|
if rec.get("kind") == "run" and isinstance(rec.get("ended"), str):
|
|
598
785
|
t_end, t_now = _iso_ms(rec["ended"]), _iso_ms(NOW)
|
|
599
786
|
t_start = _iso_ms(rec["started"]) if isinstance(rec.get("started"), str) else None
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
787
|
+
# Two different faults, and they must not share a remedy.
|
|
788
|
+
#
|
|
789
|
+
# (a) `ended` a little in the future: `started` is still trustworthy, so clamp to
|
|
790
|
+
# now and re-derive (MISS-TechieFlow-20260905-11, a day-1 run wrote 17:05 at 16:40).
|
|
791
|
+
# (b) `ended` BEFORE `started`: the record contradicts itself and there is no way to
|
|
792
|
+
# tell which of the two is wrong, so it carries NO duration at all. Deriving one
|
|
793
|
+
# from the surviving timestamp invents a figure -- a TechieBlog record started
|
|
794
|
+
# 2026-08-22 would have been "measured" at 1,515,038 seconds on the day it was
|
|
795
|
+
# re-emitted. An honest gap beats a plausible number: SCHEMA §2.5.
|
|
796
|
+
if t_end is not None and t_now is not None and t_start is not None and t_end < t_start:
|
|
797
|
+
sys.stdout.write("tf-emit: ended %s precedes started %s; the record contradicts "
|
|
798
|
+
"itself, so no duration is stored\n" % (rec["ended"], rec["started"]))
|
|
799
|
+
rec["ended"] = NOW
|
|
800
|
+
rec.pop("duration_s", None)
|
|
801
|
+
rec["duration_unmeasured"] = "ended-before-started"
|
|
802
|
+
elif t_end is not None and t_now is not None and t_end > t_now + 60_000:
|
|
803
|
+
sys.stdout.write("tf-emit: ended %s is not a measurement (it lies in the future); "
|
|
804
|
+
"set to now, %s\n" % (rec["ended"], NOW))
|
|
603
805
|
rec["ended"] = NOW
|
|
604
806
|
if t_start is not None and "duration_s" in rec:
|
|
605
807
|
rec["duration_s"] = max(0, (t_now - t_start) // 1000)
|
|
808
|
+
elif t_end is not None and t_start is not None and isinstance(rec.get("duration_s"), (int, float)):
|
|
809
|
+
# A supplied duration that bears no relation to its own timestamps. Thirteen of
|
|
810
|
+
# TechieBlog's fourteen impossible records hid here, storing a plausible round
|
|
811
|
+
# number (3600, 2700, 1800, 900, 600) that nothing downstream could question --
|
|
812
|
+
# only the ONE record that stored the negative arithmetic was ever detectable.
|
|
813
|
+
span = (t_end - t_start) // 1000
|
|
814
|
+
if abs(span - rec["duration_s"]) > 1:
|
|
815
|
+
sys.stdout.write("tf-emit: duration_s %s disagrees with started/ended (%ss); "
|
|
816
|
+
"using the timestamps\n" % (rec["duration_s"], span))
|
|
817
|
+
rec["duration_s"] = span
|
|
606
818
|
# A run record that omits duration_s gets it from started and ended (the devguide and
|
|
607
819
|
# refresh-status records of 2026-09-06 had none; MISS-TechieFlow-20260906-10).
|
|
608
|
-
if rec.get("kind") == "run" and "duration_s" not in rec and rec.get("started") and rec.get("ended")
|
|
820
|
+
if rec.get("kind") == "run" and "duration_s" not in rec and rec.get("started") and rec.get("ended") \
|
|
821
|
+
and not rec.get("duration_unmeasured"):
|
|
609
822
|
try:
|
|
610
823
|
rec["duration_s"] = max(0, (_iso_ms(rec["ended"]) - _iso_ms(rec["started"])) // 1000)
|
|
611
824
|
except Exception:
|
|
@@ -663,6 +876,52 @@ def _routing():
|
|
|
663
876
|
|
|
664
877
|
ROUTING = _routing()
|
|
665
878
|
|
|
879
|
+
# --- how a model is paid for (SCHEMA.md §2.5b, added 2026-09-10) -----------
|
|
880
|
+
# One cost column cannot mean three things at once. A run on a flat-fee subscription records
|
|
881
|
+
# zero marginal dollars, a run on a metered API key records real money, and a run on a local
|
|
882
|
+
# model records zero because there is no bill — and until this field existed a reader could
|
|
883
|
+
# not tell those three zeroes apart, so every "what did it cost" figure quietly averaged real
|
|
884
|
+
# spending with free runs. The answer is resolved by tf-model-pick.sh, which reads OpenCode's
|
|
885
|
+
# own auth.json and model catalog, so it is looked up and never declared by an agent — the
|
|
886
|
+
# same rule as `harness` (§1) and `origin_model` (§5.5.1).
|
|
887
|
+
_BILLING_CACHE = {}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _billing(model, harness):
|
|
891
|
+
key = (model, harness)
|
|
892
|
+
if key not in _BILLING_CACHE:
|
|
893
|
+
mode, source = "unknown", "none"
|
|
894
|
+
pick = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
895
|
+
if model and os.path.isfile(pick):
|
|
896
|
+
try:
|
|
897
|
+
out = subprocess.run(["bash", pick, "billing", model, harness or "claude"],
|
|
898
|
+
capture_output=True, text=True, timeout=20,
|
|
899
|
+
env=dict(os.environ, TF_PROJECT_DIR=root))
|
|
900
|
+
parts = (out.stdout or "").strip().split("\t")
|
|
901
|
+
if parts and parts[0] in ("subscription", "plan", "metered", "local", "unknown"):
|
|
902
|
+
mode = parts[0]
|
|
903
|
+
source = parts[1] if len(parts) > 1 else "tf-model-pick"
|
|
904
|
+
except Exception:
|
|
905
|
+
pass
|
|
906
|
+
_BILLING_CACHE[key] = (mode, source)
|
|
907
|
+
return _BILLING_CACHE[key]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _was_limited(model, harness, at_epoch):
|
|
911
|
+
"""Did this run start while the tier's own model was parked? Then it is a deliberate
|
|
912
|
+
fallback, not routing drift, and `routed:false` should not be read as one."""
|
|
913
|
+
pick = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
914
|
+
if not model or not os.path.isfile(pick) or at_epoch is None:
|
|
915
|
+
return False
|
|
916
|
+
try:
|
|
917
|
+
out = subprocess.run(["bash", pick, "was-limited", harness or "claude", model, str(int(at_epoch))],
|
|
918
|
+
capture_output=True, text=True, timeout=20,
|
|
919
|
+
env=dict(os.environ, TF_PROJECT_DIR=root))
|
|
920
|
+
return out.returncode == 0
|
|
921
|
+
except Exception:
|
|
922
|
+
return False
|
|
923
|
+
|
|
924
|
+
|
|
666
925
|
def _iso_ms(s):
|
|
667
926
|
try:
|
|
668
927
|
return int(datetime.datetime.strptime(
|
|
@@ -836,6 +1095,104 @@ def _prior_runs():
|
|
|
836
1095
|
pass
|
|
837
1096
|
return _PRIOR_RUNS
|
|
838
1097
|
|
|
1098
|
+
# --- a run may not start before the last one finished (FR-72, 2026-09-10) --
|
|
1099
|
+
# `--void-run` (§2.7) gave a way to CORRECT a run record written with a start time nobody
|
|
1100
|
+
# measured. Nothing refused one, so the same mistake happened twice more the day the correction
|
|
1101
|
+
# mechanism shipped — two records whose `started` preceded the previous record's `ended`, one by
|
|
1102
|
+
# 42 minutes and one by nearly three hours, each silently adding that overlap to every total
|
|
1103
|
+
# (MISS-TechieFlow-20260910-04, sorted `ignored`: it was written down and not followed).
|
|
1104
|
+
#
|
|
1105
|
+
# A stream is append-only, so the only place to stop it is before the write. The rule: for one
|
|
1106
|
+
# app, a live `run` record's window may not OVERLAP another live `run` record's window. It is the
|
|
1107
|
+
# windows that are compared, not the order of writing: a command that chains another one inside
|
|
1108
|
+
# itself -- *build-phase chaining *verify -- finishes last but did its own first segment before the
|
|
1109
|
+
# inner run started, and comparing against the newest record alone meant that segment could never be
|
|
1110
|
+
# recorded at all (TfLens TF-040, 80 minutes and five builder clusters on no record). Two windows
|
|
1111
|
+
# that merely touch at a boundary do not overlap. Voided and backfilled records are ignored, which
|
|
1112
|
+
# is what makes the void-then-re-emit flow work. Records with no `started` are untouched.
|
|
1113
|
+
#
|
|
1114
|
+
# The one legitimate overlap is two machines appending to the same merge=union stream, so
|
|
1115
|
+
# `--allow-overlap` exists and the refusal message names it. It is deliberately a flag and not a
|
|
1116
|
+
# fallback: the mistake this refuses is easy to make and invisible afterwards.
|
|
1117
|
+
_LIVE = None
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _live_windows(app):
|
|
1121
|
+
"""[(started, ended, cmd)] of every live run record for this app, oldest first."""
|
|
1122
|
+
global _LIVE
|
|
1123
|
+
if _LIVE is None:
|
|
1124
|
+
_LIVE = {}
|
|
1125
|
+
voided = set()
|
|
1126
|
+
rows = []
|
|
1127
|
+
try:
|
|
1128
|
+
with open(os.path.join(met_dir, "runs.jsonl"), encoding="utf-8") as fh:
|
|
1129
|
+
for line in fh:
|
|
1130
|
+
line = line.strip()
|
|
1131
|
+
if not line:
|
|
1132
|
+
continue
|
|
1133
|
+
try:
|
|
1134
|
+
r = json.loads(line)
|
|
1135
|
+
except Exception:
|
|
1136
|
+
continue
|
|
1137
|
+
if r.get("kind") == "run-void":
|
|
1138
|
+
voided.add((r.get("app"), r.get("cmd"), r.get("started")))
|
|
1139
|
+
elif r.get("kind", "run") == "run" and not r.get("backfilled"):
|
|
1140
|
+
rows.append(r)
|
|
1141
|
+
except Exception:
|
|
1142
|
+
pass
|
|
1143
|
+
for r in rows:
|
|
1144
|
+
if (r.get("app"), r.get("cmd"), r.get("started")) in voided:
|
|
1145
|
+
continue
|
|
1146
|
+
start = r.get("started")
|
|
1147
|
+
if not start:
|
|
1148
|
+
continue
|
|
1149
|
+
# `ended` ONLY. `ts` is when the record was WRITTEN, not when the run finished, so
|
|
1150
|
+
# falling back to it would make every historical record emitted today look like an
|
|
1151
|
+
# overlap with the record written a second ago. A run with no `ended` has no window
|
|
1152
|
+
# and constrains nothing.
|
|
1153
|
+
#
|
|
1154
|
+
# And only a record whose window is COHERENT: a self-contradicting one (ended before
|
|
1155
|
+
# started) has its `ended` replaced with the write time by the check above, and
|
|
1156
|
+
# marked by a null `duration_s`. That placeholder is not a measurement, so it must
|
|
1157
|
+
# not become a boundary — it would block every later record for the same app.
|
|
1158
|
+
if not isinstance(r.get("duration_s"), (int, float)):
|
|
1159
|
+
continue
|
|
1160
|
+
end = r.get("ended")
|
|
1161
|
+
if not end:
|
|
1162
|
+
continue
|
|
1163
|
+
_LIVE.setdefault(r.get("app"), []).append((start, end, r.get("cmd")))
|
|
1164
|
+
return _LIVE.get(app, [])
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _overlap_ok(rec):
|
|
1168
|
+
"""False (and the reason printed) when this run's window overlaps a live one's."""
|
|
1169
|
+
if stream != "runs" or rec.get("kind", "run") != "run" or rec.get("backfilled"):
|
|
1170
|
+
return True
|
|
1171
|
+
if os.environ.get("TF_ALLOW_OVERLAP") == "1":
|
|
1172
|
+
return True
|
|
1173
|
+
started, ended = rec.get("started"), rec.get("ended")
|
|
1174
|
+
if not started:
|
|
1175
|
+
return True
|
|
1176
|
+
def key(v):
|
|
1177
|
+
k = _iso_ms(v)
|
|
1178
|
+
return k if k is not None else v # unparseable: compare as strings, as before
|
|
1179
|
+
s, e = key(started), key(ended or started)
|
|
1180
|
+
for (ps, pe, pcmd) in _live_windows(rec.get("app")):
|
|
1181
|
+
a, b = key(ps), key(pe)
|
|
1182
|
+
if type(a) is not type(s) or type(b) is not type(e):
|
|
1183
|
+
continue
|
|
1184
|
+
if s < b and a < e: # windows that only touch at a boundary are fine
|
|
1185
|
+
sys.stdout.write(
|
|
1186
|
+
"tf-emit: REFUSED — this run (%s to %s) overlaps the %s run of %s already on the "
|
|
1187
|
+
"stream (%s to %s). Every total over the two would be that much too big. Record the "
|
|
1188
|
+
"part that did not overlap -- a command that chained another one records its own "
|
|
1189
|
+
"segment before the inner run started -- or pass --allow-overlap if they really did "
|
|
1190
|
+
"run at the same time (a second machine on the same stream). Nothing was appended.\n"
|
|
1191
|
+
% (started, ended or "now", pcmd, rec.get("app"), ps, pe))
|
|
1192
|
+
return False
|
|
1193
|
+
return True
|
|
1194
|
+
|
|
1195
|
+
|
|
839
1196
|
def stamp_attempt(rec):
|
|
840
1197
|
if stream != "runs" or "attempt" in rec or rec.get("backfilled"):
|
|
841
1198
|
return rec
|
|
@@ -901,8 +1258,21 @@ def enrich_run(rec):
|
|
|
901
1258
|
rec["tokens_out_subagents"] = win.get("sub_out", 0)
|
|
902
1259
|
if rec.get("tier_model") and rec.get("model"):
|
|
903
1260
|
rec["routed"] = rec["model"] == rec["tier_model"]
|
|
1261
|
+
# What a cost figure from this window MEANS. Several models in one window may
|
|
1262
|
+
# be paid for differently — a fallback from a subscription to a metered key is
|
|
1263
|
+
# exactly that case — so a mixed window says so rather than picking a winner.
|
|
1264
|
+
modes = {_billing(m, HARNESS)[0] for m in models} or {_billing(rec.get("model"), HARNESS)[0]}
|
|
1265
|
+
rec["billing_mode"] = modes.pop() if len(modes) == 1 else "mixed"
|
|
1266
|
+
rec["cost_source"] = "opencode-db" if HARNESS == "opencode" else "none"
|
|
1267
|
+
# `routed:false` because the tier model was out of quota is a different fact from
|
|
1268
|
+
# `routed:false` because something drifted. Only the first names a model here.
|
|
1269
|
+
tm = rec.get("tier_model")
|
|
1270
|
+
if tm and rec.get("model") and rec["model"] != tm and _was_limited(tm, HARNESS, t0 / 1000.0):
|
|
1271
|
+
rec["fallback_from"] = tm
|
|
904
1272
|
else:
|
|
905
1273
|
rec["tokens_scope"] = "none"
|
|
1274
|
+
rec["billing_mode"] = _billing(rec.get("tier_model"), HARNESS)[0]
|
|
1275
|
+
rec["cost_source"] = "none"
|
|
906
1276
|
except Exception as e:
|
|
907
1277
|
warn("run enrichment failed (%s) — record kept unenriched" % e)
|
|
908
1278
|
return rec
|
|
@@ -970,7 +1340,7 @@ def _runs_by_start():
|
|
|
970
1340
|
return _RUNS_BY_START
|
|
971
1341
|
|
|
972
1342
|
_COST_FIELDS = ("tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
973
|
-
"cost_usd", "tokens_scope", "model")
|
|
1343
|
+
"cost_usd", "tokens_scope", "model", "billing_mode", "cost_source")
|
|
974
1344
|
|
|
975
1345
|
# Kept identical to the --amend branch above. Two doors, ONE enforcement: an
|
|
976
1346
|
# agent that hand-writes a miss-amend record onto the stream faces the same
|
|
@@ -982,13 +1352,21 @@ _AMENDABLE = {
|
|
|
982
1352
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
983
1353
|
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
984
1354
|
}
|
|
1355
|
+
_AMENDABLE_TEXT = ("what",) # free text, guarded by the never-overwrite rule (see --amend)
|
|
985
1356
|
|
|
986
1357
|
def _amend_ok(rec):
|
|
987
1358
|
"""True when this miss-amend may be appended. Returning False DROPS it —
|
|
988
1359
|
an invalid amend writes nothing rather than landing as a record the reader
|
|
989
1360
|
then has to defend itself against."""
|
|
990
1361
|
mid, fld, val = rec.get("miss_id"), rec.get("field"), rec.get("value")
|
|
991
|
-
if
|
|
1362
|
+
if not mid:
|
|
1363
|
+
warn("amend refused — no miss_id")
|
|
1364
|
+
return False
|
|
1365
|
+
if fld in _AMENDABLE_TEXT:
|
|
1366
|
+
if not isinstance(val, str) or not val.strip():
|
|
1367
|
+
warn("amend refused — %s must be a non-empty sentence" % fld)
|
|
1368
|
+
return False
|
|
1369
|
+
elif fld not in _AMENDABLE or val not in _AMENDABLE[fld]:
|
|
992
1370
|
warn("amend refused — %r/%r outside the §5.5.7 allowlist" % (fld, val))
|
|
993
1371
|
return False
|
|
994
1372
|
parent, current = None, None
|
|
@@ -1084,12 +1462,19 @@ _ALLOWED = {
|
|
|
1084
1462
|
"subagents", "files_written", "build_result", "tier", "tier_model", "model", "models",
|
|
1085
1463
|
"routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
1086
1464
|
"cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
|
|
1087
|
-
"model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts"
|
|
1465
|
+
"model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts",
|
|
1466
|
+
"billing_mode", "cost_source", "fallback_from"},
|
|
1467
|
+
# A correction, not a deletion: it names a run record that is wrong by the pair every
|
|
1468
|
+
# run carries (`cmd` + `started`) and says why, so every figure can skip it while the
|
|
1469
|
+
# record itself stays where it is. Written by --void-run, which refuses one that names
|
|
1470
|
+
# no run. SCHEMA.md §2.7; tests/regression/run.sh tf_void.
|
|
1471
|
+
("runs", "run-void"): {"cmd", "started", "reason"},
|
|
1088
1472
|
("gates", "gate"): {"run_id", "req_id", "req_class", "attempt", "verdict", "gate", "gates_run",
|
|
1089
1473
|
"failure_class", "prior_verdict", "proof", "escaped", "phase", "result", "detail", "at",
|
|
1090
1474
|
"tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1091
1475
|
"tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
|
|
1092
|
-
"subagent_runs", "tokens_out_subagents", "session_id"
|
|
1476
|
+
"subagent_runs", "tokens_out_subagents", "session_id",
|
|
1477
|
+
"billing_mode", "cost_source", "fallback_from"},
|
|
1093
1478
|
("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
|
|
1094
1479
|
"cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
|
|
1095
1480
|
("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
|
|
@@ -1099,8 +1484,11 @@ _ALLOWED = {
|
|
|
1099
1484
|
"what", "sort"},
|
|
1100
1485
|
("misses", "miss-fix"): {"miss_id", "req_id", "fix_run_id", "fix_cmd", "fix_attempt", "verdict_after",
|
|
1101
1486
|
"reopened", "cost_attribution", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1102
|
-
"tokens_cache_write", "cost_usd", "tokens_scope", "model"
|
|
1487
|
+
"tokens_cache_write", "cost_usd", "tokens_scope", "model",
|
|
1488
|
+
"billing_mode", "cost_source"},
|
|
1103
1489
|
("misses", "miss-amend"): {"miss_id", "field", "value"},
|
|
1490
|
+
# a miss that should never have been written, withdrawn by --void-miss (SCHEMA.md §5.5.11)
|
|
1491
|
+
("misses", "miss-void"): {"miss_id", "reason"},
|
|
1104
1492
|
# an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
|
|
1105
1493
|
# given, what producing the reviewed output cost, what applying the corrections cost. The two
|
|
1106
1494
|
# costs are copied from the runs named by reviewed_run_id and correction_run_id (their `started`).
|
|
@@ -1111,7 +1499,8 @@ _ALLOWED = {
|
|
|
1111
1499
|
# Fields the emitter derives for runs/gates: a caller's value is discarded.
|
|
1112
1500
|
_DERIVED = ("harness", "tier", "tier_model", "model", "models", "model_tokens_out", "routed",
|
|
1113
1501
|
"tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write", "cost_usd",
|
|
1114
|
-
"tokens_scope", "subagent_runs", "tokens_out_subagents"
|
|
1502
|
+
"tokens_scope", "subagent_runs", "tokens_out_subagents",
|
|
1503
|
+
"billing_mode", "cost_source", "fallback_from")
|
|
1115
1504
|
|
|
1116
1505
|
|
|
1117
1506
|
def _shape_ok(rec):
|
|
@@ -1231,7 +1620,12 @@ lines = []
|
|
|
1231
1620
|
for rec in records:
|
|
1232
1621
|
if not _shape_ok(rec):
|
|
1233
1622
|
continue
|
|
1234
|
-
|
|
1623
|
+
# enrich() first, because it is what fills `app` when the caller left it out — the overlap
|
|
1624
|
+
# rule is per app and would otherwise compare a named run against an unnamed one.
|
|
1625
|
+
base = enrich(rec)
|
|
1626
|
+
if not _overlap_ok(base):
|
|
1627
|
+
continue
|
|
1628
|
+
out = enrich_miss(enrich_run(stamp_attempt(base)))
|
|
1235
1629
|
if out is None: # a refused miss-amend (§5.5.7) — dropped, never appended
|
|
1236
1630
|
continue
|
|
1237
1631
|
line = json.dumps(out, separators=(",", ":"), ensure_ascii=False)
|
|
@@ -1252,12 +1646,12 @@ except Exception as e:
|
|
|
1252
1646
|
warn("append failed (%s) — event dropped" % e)
|
|
1253
1647
|
raise SystemExit(0)
|
|
1254
1648
|
PY
|
|
1255
|
-
)"
|
|
1256
1649
|
python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
|
|
1257
1650
|
|
|
1258
1651
|
# The readable miss list beside the record (FR-31, Session 5 2026-09-07): after any write to the
|
|
1259
|
-
# misses stream, docs/<App>-Misses.md
|
|
1260
|
-
#
|
|
1652
|
+
# misses stream, docs/<App>-Misses.md is rebuilt from the stream, so the file is never older than
|
|
1653
|
+
# the record. Markdown only — the miss log is an agent document and is never rendered to HTML
|
|
1654
|
+
# (owner, 2026-09-08). Best effort, like everything else here.
|
|
1261
1655
|
if [[ "$STREAM" == "misses" ]]; then
|
|
1262
1656
|
python3 "$(dirname "${BASH_SOURCE[0]}")/tf-misses-md.py" --root "$ROOT" --quiet 2>"$TF_ERR" || true
|
|
1263
1657
|
fi
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-feedback.sh — the state of every entry in this project's upstream feedback files, and the
|
|
3
|
+
# one way to mark an entry closed once its fix has been re-checked here (2026-09-11).
|
|
4
|
+
#
|
|
5
|
+
# bash .tfcore/utils/tf-feedback.sh <App> every entry: open, fixed upstream, closed
|
|
6
|
+
# bash .tfcore/utils/tf-feedback.sh <App> --waiting fixed upstream, not yet re-checked here
|
|
7
|
+
# bash .tfcore/utils/tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"
|
|
8
|
+
#
|
|
9
|
+
# Details: tf_feedback.py. Writes nothing except the one closing line --close adds.
|
|
10
|
+
set -u
|
|
11
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-feedback: python3 is required" >&2; exit 2; }
|
|
13
|
+
exec python3 "$HERE/tf_feedback.py" "$@"
|