@techierathore/techieflow 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +2 -2
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +2 -2
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +57 -1
- package/.tfcore/telemetry/tf-metrics.sh +416 -19
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-build-list.py +23 -3
- package/.tfcore/utils/tf-build-list.sh +3 -2
- package/.tfcore/utils/tf-doc-check.py +106 -4
- package/.tfcore/utils/tf-emit.sh +353 -16
- package/.tfcore/utils/tf-fix-close.sh +34 -2
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +93 -6
- package/.tfcore/utils/tf-misses-md.py +14 -17
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-phase.sh +18 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +6 -1
- package/.tfcore/utils/tf-status-facts.py +61 -12
- package/.tfcore/utils/tf-verify-screens.mjs +85 -13
- package/.tfcore/utils/tf-verify-screens.sh +12 -3
- package/.tfcore/utils/tf-verify-tests.sh +60 -17
- package/.tfcore/utils/tf-verify-verdict.py +11 -2
- package/README.md +15 -5
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
package/.tfcore/utils/tf-emit.sh
CHANGED
|
@@ -19,6 +19,10 @@
|
|
|
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.
|
|
22
26
|
# tf-emit.sh --origin-of REQ-UI-014 # prints "<started> <cmd> <agent>" of the last build or fix
|
|
23
27
|
# # run that touched the row, nothing when there is none
|
|
24
28
|
# tf-emit.sh --where # prints the resolved docs/metrics dir
|
|
@@ -270,6 +274,61 @@ fi
|
|
|
270
274
|
# patience rather than quality. Open = no miss-fix, or the latest miss-fix
|
|
271
275
|
# closed with a verdict_after other than "Verified". Prints "<miss_id>
|
|
272
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
|
+
|
|
273
332
|
if [[ "$1" == "--open-miss" ]]; then
|
|
274
333
|
REQ="$2"
|
|
275
334
|
[[ -n "$REQ" ]] || exit 0
|
|
@@ -344,9 +403,19 @@ AMENDABLE = {
|
|
|
344
403
|
# still make later, so a miss logged before the field existed can be sorted now.
|
|
345
404
|
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
346
405
|
}
|
|
347
|
-
|
|
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:
|
|
348
414
|
print("REFUSED %s is not an amendable field (SCHEMA.md §5.5.7)" % fld); raise SystemExit(0)
|
|
349
|
-
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]:
|
|
350
419
|
print("REFUSED %r is not in the closed vocabulary for %s" % (val, fld)); raise SystemExit(0)
|
|
351
420
|
|
|
352
421
|
parent, current = None, None
|
|
@@ -389,8 +458,77 @@ PY
|
|
|
389
458
|
exit 0
|
|
390
459
|
fi
|
|
391
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
|
+
|
|
392
524
|
# --- append path ----------------------------------------------------------
|
|
393
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
|
|
394
532
|
case "$STREAM" in
|
|
395
533
|
runs|gates|sessions|commits|misses) ;;
|
|
396
534
|
*) _warn "unknown stream '${STREAM:-<none>}' — event dropped"; exit 0 ;;
|
|
@@ -399,7 +537,7 @@ esac
|
|
|
399
537
|
# The program is captured into a variable rather than fed on stdin — a heredoc
|
|
400
538
|
# on `python3 -` would REPLACE the caller's piped JSON and silently drop every event.
|
|
401
539
|
TF_PROG="$(cat <<'PY'
|
|
402
|
-
import json, os, sys, datetime, glob, re, time
|
|
540
|
+
import json, os, sys, datetime, glob, re, subprocess, time
|
|
403
541
|
|
|
404
542
|
met_dir, stream, root = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
405
543
|
dbg = os.environ.get("TF_METRICS_DEBUG")
|
|
@@ -597,15 +735,41 @@ def enrich(rec):
|
|
|
597
735
|
if rec.get("kind") == "run" and isinstance(rec.get("ended"), str):
|
|
598
736
|
t_end, t_now = _iso_ms(rec["ended"]), _iso_ms(NOW)
|
|
599
737
|
t_start = _iso_ms(rec["started"]) if isinstance(rec.get("started"), str) else None
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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))
|
|
603
756
|
rec["ended"] = NOW
|
|
604
757
|
if t_start is not None and "duration_s" in rec:
|
|
605
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
|
|
606
769
|
# A run record that omits duration_s gets it from started and ended (the devguide and
|
|
607
770
|
# 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")
|
|
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"):
|
|
609
773
|
try:
|
|
610
774
|
rec["duration_s"] = max(0, (_iso_ms(rec["ended"]) - _iso_ms(rec["started"])) // 1000)
|
|
611
775
|
except Exception:
|
|
@@ -663,6 +827,52 @@ def _routing():
|
|
|
663
827
|
|
|
664
828
|
ROUTING = _routing()
|
|
665
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
|
+
|
|
666
876
|
def _iso_ms(s):
|
|
667
877
|
try:
|
|
668
878
|
return int(datetime.datetime.strptime(
|
|
@@ -836,6 +1046,97 @@ def _prior_runs():
|
|
|
836
1046
|
pass
|
|
837
1047
|
return _PRIOR_RUNS
|
|
838
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
|
+
|
|
839
1140
|
def stamp_attempt(rec):
|
|
840
1141
|
if stream != "runs" or "attempt" in rec or rec.get("backfilled"):
|
|
841
1142
|
return rec
|
|
@@ -901,8 +1202,21 @@ def enrich_run(rec):
|
|
|
901
1202
|
rec["tokens_out_subagents"] = win.get("sub_out", 0)
|
|
902
1203
|
if rec.get("tier_model") and rec.get("model"):
|
|
903
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
|
|
904
1216
|
else:
|
|
905
1217
|
rec["tokens_scope"] = "none"
|
|
1218
|
+
rec["billing_mode"] = _billing(rec.get("tier_model"), HARNESS)[0]
|
|
1219
|
+
rec["cost_source"] = "none"
|
|
906
1220
|
except Exception as e:
|
|
907
1221
|
warn("run enrichment failed (%s) — record kept unenriched" % e)
|
|
908
1222
|
return rec
|
|
@@ -970,7 +1284,7 @@ def _runs_by_start():
|
|
|
970
1284
|
return _RUNS_BY_START
|
|
971
1285
|
|
|
972
1286
|
_COST_FIELDS = ("tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
973
|
-
"cost_usd", "tokens_scope", "model")
|
|
1287
|
+
"cost_usd", "tokens_scope", "model", "billing_mode", "cost_source")
|
|
974
1288
|
|
|
975
1289
|
# Kept identical to the --amend branch above. Two doors, ONE enforcement: an
|
|
976
1290
|
# agent that hand-writes a miss-amend record onto the stream faces the same
|
|
@@ -982,13 +1296,21 @@ _AMENDABLE = {
|
|
|
982
1296
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
983
1297
|
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
984
1298
|
}
|
|
1299
|
+
_AMENDABLE_TEXT = ("what",) # free text, guarded by the never-overwrite rule (see --amend)
|
|
985
1300
|
|
|
986
1301
|
def _amend_ok(rec):
|
|
987
1302
|
"""True when this miss-amend may be appended. Returning False DROPS it —
|
|
988
1303
|
an invalid amend writes nothing rather than landing as a record the reader
|
|
989
1304
|
then has to defend itself against."""
|
|
990
1305
|
mid, fld, val = rec.get("miss_id"), rec.get("field"), rec.get("value")
|
|
991
|
-
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]:
|
|
992
1314
|
warn("amend refused — %r/%r outside the §5.5.7 allowlist" % (fld, val))
|
|
993
1315
|
return False
|
|
994
1316
|
parent, current = None, None
|
|
@@ -1084,12 +1406,19 @@ _ALLOWED = {
|
|
|
1084
1406
|
"subagents", "files_written", "build_result", "tier", "tier_model", "model", "models",
|
|
1085
1407
|
"routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
1086
1408
|
"cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
|
|
1087
|
-
"model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts"
|
|
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"},
|
|
1088
1416
|
("gates", "gate"): {"run_id", "req_id", "req_class", "attempt", "verdict", "gate", "gates_run",
|
|
1089
1417
|
"failure_class", "prior_verdict", "proof", "escaped", "phase", "result", "detail", "at",
|
|
1090
1418
|
"tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1091
1419
|
"tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
|
|
1092
|
-
"subagent_runs", "tokens_out_subagents", "session_id"
|
|
1420
|
+
"subagent_runs", "tokens_out_subagents", "session_id",
|
|
1421
|
+
"billing_mode", "cost_source", "fallback_from"},
|
|
1093
1422
|
("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
|
|
1094
1423
|
"cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
|
|
1095
1424
|
("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
|
|
@@ -1099,7 +1428,8 @@ _ALLOWED = {
|
|
|
1099
1428
|
"what", "sort"},
|
|
1100
1429
|
("misses", "miss-fix"): {"miss_id", "req_id", "fix_run_id", "fix_cmd", "fix_attempt", "verdict_after",
|
|
1101
1430
|
"reopened", "cost_attribution", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1102
|
-
"tokens_cache_write", "cost_usd", "tokens_scope", "model"
|
|
1431
|
+
"tokens_cache_write", "cost_usd", "tokens_scope", "model",
|
|
1432
|
+
"billing_mode", "cost_source"},
|
|
1103
1433
|
("misses", "miss-amend"): {"miss_id", "field", "value"},
|
|
1104
1434
|
# an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
|
|
1105
1435
|
# given, what producing the reviewed output cost, what applying the corrections cost. The two
|
|
@@ -1111,7 +1441,8 @@ _ALLOWED = {
|
|
|
1111
1441
|
# Fields the emitter derives for runs/gates: a caller's value is discarded.
|
|
1112
1442
|
_DERIVED = ("harness", "tier", "tier_model", "model", "models", "model_tokens_out", "routed",
|
|
1113
1443
|
"tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write", "cost_usd",
|
|
1114
|
-
"tokens_scope", "subagent_runs", "tokens_out_subagents"
|
|
1444
|
+
"tokens_scope", "subagent_runs", "tokens_out_subagents",
|
|
1445
|
+
"billing_mode", "cost_source", "fallback_from")
|
|
1115
1446
|
|
|
1116
1447
|
|
|
1117
1448
|
def _shape_ok(rec):
|
|
@@ -1231,7 +1562,12 @@ lines = []
|
|
|
1231
1562
|
for rec in records:
|
|
1232
1563
|
if not _shape_ok(rec):
|
|
1233
1564
|
continue
|
|
1234
|
-
|
|
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)))
|
|
1235
1571
|
if out is None: # a refused miss-amend (§5.5.7) — dropped, never appended
|
|
1236
1572
|
continue
|
|
1237
1573
|
line = json.dumps(out, separators=(",", ":"), ensure_ascii=False)
|
|
@@ -1256,8 +1592,9 @@ PY
|
|
|
1256
1592
|
python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
|
|
1257
1593
|
|
|
1258
1594
|
# 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
|
-
#
|
|
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.
|
|
1261
1598
|
if [[ "$STREAM" == "misses" ]]; then
|
|
1262
1599
|
python3 "$(dirname "${BASH_SOURCE[0]}")/tf-misses-md.py" --root "$ROOT" --quiet 2>"$TF_ERR" || true
|
|
1263
1600
|
fi
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
#
|
|
4
4
|
# bash .tfcore/utils/tf-fix-close.sh <App> [--started <ISO>] [--reqs REQ-UI-009,REQ-FN-014]
|
|
5
5
|
# [--subagents trblazeui,builder] [--build pass|fail|not-run] [--files N]
|
|
6
|
+
# bash .tfcore/utils/tf-fix-close.sh <App> --misses MISS-App-20260901-06,MISS-App-20260830-03 \
|
|
7
|
+
# --fix-cmd amend-docs [--verdict Verified]
|
|
8
|
+
#
|
|
9
|
+
# The second form closes misses DIRECTLY, by id, with no checklist row and no verify ledger.
|
|
10
|
+
# It exists because a miss whose deficient artifact is a document is fixed by *amend-docs
|
|
11
|
+
# editing the BRD -- never by the verifier touching a row -- so the row-driven form above can
|
|
12
|
+
# never reach it, and the miss stayed open forever (TF-016). It writes NO run record: the
|
|
13
|
+
# amending command emits its own through the status gate. `fix_cmd: "amend-docs"` was already
|
|
14
|
+
# legal in SCHEMA §5.5.2; only this door was missing.
|
|
6
15
|
#
|
|
7
16
|
# Emits, in this order, through tf-emit.sh only:
|
|
8
17
|
# 1. the fix-issues run record (cmd fix-issues, mode fix, the rows touched) — first, because the
|
|
@@ -17,7 +26,7 @@ set -u
|
|
|
17
26
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
27
|
[[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
|
|
19
28
|
APP="$1"; shift
|
|
20
|
-
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""
|
|
29
|
+
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""; MISSES=""; FIXCMD="fix-issues"; VERDICT=""
|
|
21
30
|
while [[ $# -gt 0 ]]; do
|
|
22
31
|
case "$1" in
|
|
23
32
|
--started) STARTED="${2:-}"; shift 2 ;;
|
|
@@ -25,11 +34,15 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
34
|
--subagents) SUBS="${2:-}"; shift 2 ;;
|
|
26
35
|
--build) BUILD="${2:-}"; shift 2 ;;
|
|
27
36
|
--files) FILES="${2:-}"; shift 2 ;;
|
|
37
|
+
--misses) MISSES="${2:-}"; shift 2 ;;
|
|
38
|
+
--fix-cmd) FIXCMD="${2:-}"; shift 2 ;;
|
|
39
|
+
--verdict) VERDICT="${2:-}"; shift 2 ;;
|
|
28
40
|
*) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
|
|
29
41
|
esac
|
|
30
42
|
done
|
|
31
43
|
[[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | sed -n 's/.*"started":"\([^"]*\)".*/\1/p')"
|
|
32
|
-
TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES"
|
|
44
|
+
TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES" \
|
|
45
|
+
TF_MISSES="$MISSES" TF_FIXCMD="$FIXCMD" TF_VERDICT="$VERDICT" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
|
|
33
46
|
import datetime, json, os, subprocess
|
|
34
47
|
EMIT = os.environ["TF_EMIT"]; app = os.environ["TF_APP"]; started = os.environ["TF_STARTED"]
|
|
35
48
|
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -39,6 +52,25 @@ def emit(stream, rec):
|
|
|
39
52
|
msg = (p.stdout + p.stderr).strip(); ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
|
|
40
53
|
if not ok: print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
|
|
41
54
|
return ok
|
|
55
|
+
# --- close by miss id (the *amend-docs door) -------------------------------------------
|
|
56
|
+
direct = [m.strip() for m in (os.environ.get("TF_MISSES") or "").split(",") if m.strip()]
|
|
57
|
+
if direct:
|
|
58
|
+
fix_cmd = os.environ.get("TF_FIXCMD") or "amend-docs"
|
|
59
|
+
verdict = os.environ.get("TF_VERDICT") or "Verified"
|
|
60
|
+
closed = 0
|
|
61
|
+
for mid in direct:
|
|
62
|
+
fa = q("--next-fix-attempt", mid) or "1"
|
|
63
|
+
rec = {"kind": "miss-fix", "miss_id": mid, "fix_cmd": fix_cmd,
|
|
64
|
+
"fix_attempt": int(fa) if fa.isdigit() else 1,
|
|
65
|
+
"verdict_after": verdict, "reopened": False}
|
|
66
|
+
if started:
|
|
67
|
+
rec["fix_run_id"] = started
|
|
68
|
+
if emit("misses", rec):
|
|
69
|
+
closed += 1
|
|
70
|
+
print("%s: %d of %d miss-fix record(s) written (no run record — the amending command emits its own)"
|
|
71
|
+
% (fix_cmd, closed, len(direct)))
|
|
72
|
+
raise SystemExit(0)
|
|
73
|
+
|
|
42
74
|
ledger = {}
|
|
43
75
|
try:
|
|
44
76
|
ledger = json.load(open(os.path.join("docs", ".last-verify.json"), encoding="utf-8"))
|