@techierathore/techieflow 1.0.4 → 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.
Files changed (98) hide show
  1. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  2. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  3. package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
  4. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +1 -1
  5. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +1 -1
  6. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  9. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  11. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  12. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  13. package/.opencode/plugin/techieflow.js +179 -7
  14. package/.tfcore/hooks/block-git.sh +4 -2
  15. package/.tfcore/hooks/guard-build.sh +5 -1
  16. package/.tfcore/hooks/guard-db.sh +6 -4
  17. package/.tfcore/hooks/guard-status-html.sh +86 -2
  18. package/.tfcore/hooks/guard-verify.sh +8 -3
  19. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  20. package/.tfcore/standards/owner-words.txt +23 -0
  21. package/.tfcore/tasks/_owner-language.md +1 -0
  22. package/.tfcore/tasks/_status-update-gate.md +4 -3
  23. package/.tfcore/tasks/amend-docs.md +1 -1
  24. package/.tfcore/tasks/day1-brownfield.md +1 -1
  25. package/.tfcore/tasks/day1-greenfield.md +1 -1
  26. package/.tfcore/tasks/fix-issues.md +2 -2
  27. package/.tfcore/tasks/generate-html.md +2 -2
  28. package/.tfcore/tasks/handoff-phase.md +2 -2
  29. package/.tfcore/tasks/mockups.md +1 -1
  30. package/.tfcore/tasks/productguide.md +2 -2
  31. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  32. package/.tfcore/tasks/triage-issues.md +2 -2
  33. package/.tfcore/telemetry/SCHEMA.md +4 -0
  34. package/.tfcore/telemetry/install-metrics.sh +3 -2
  35. package/.tfcore/telemetry/tf-metrics.sh +19 -2
  36. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  37. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  38. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  39. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  40. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  41. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  42. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  43. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  44. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  45. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  46. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  47. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  48. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  49. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  50. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  51. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  52. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  53. package/.tfcore/utils/tf-assets.sh +97 -12
  54. package/.tfcore/utils/tf-brd-status.py +14 -5
  55. package/.tfcore/utils/tf-build-list.py +80 -12
  56. package/.tfcore/utils/tf-build-list.sh +2 -1
  57. package/.tfcore/utils/tf-build.sh +88 -15
  58. package/.tfcore/utils/tf-ci-repro.py +685 -0
  59. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  60. package/.tfcore/utils/tf-devguide-list.py +47 -22
  61. package/.tfcore/utils/tf-doc-check.py +136 -6
  62. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  63. package/.tfcore/utils/tf-emit.sh +92 -35
  64. package/.tfcore/utils/tf-feedback.sh +13 -0
  65. package/.tfcore/utils/tf-fix-close.sh +79 -29
  66. package/.tfcore/utils/tf-goal.sh +12 -12
  67. package/.tfcore/utils/tf-harness.sh +9 -4
  68. package/.tfcore/utils/tf-lock.sh +29 -0
  69. package/.tfcore/utils/tf-log-miss.py +34 -8
  70. package/.tfcore/utils/tf-login.mjs +167 -0
  71. package/.tfcore/utils/tf-misses-md.py +10 -4
  72. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  73. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  74. package/.tfcore/utils/tf-owner-text.py +233 -0
  75. package/.tfcore/utils/tf-owner-text.sh +14 -0
  76. package/.tfcore/utils/tf-phase.sh +15 -1
  77. package/.tfcore/utils/tf-portable.sh +230 -0
  78. package/.tfcore/utils/tf-render-html.py +60 -2
  79. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  80. package/.tfcore/utils/tf-split-brd.py +11 -1
  81. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  82. package/.tfcore/utils/tf-status-facts.py +72 -32
  83. package/.tfcore/utils/tf-triage.py +62 -16
  84. package/.tfcore/utils/tf-triage.sh +2 -0
  85. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  86. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  87. package/.tfcore/utils/tf-verify-env.sh +33 -4
  88. package/.tfcore/utils/tf-verify-list.py +75 -8
  89. package/.tfcore/utils/tf-verify-screens.mjs +171 -32
  90. package/.tfcore/utils/tf-verify-screens.sh +8 -3
  91. package/.tfcore/utils/tf-verify-tests.sh +215 -14
  92. package/.tfcore/utils/tf-verify-verdict.py +48 -9
  93. package/.tfcore/utils/tf_defect.py +27 -0
  94. package/.tfcore/utils/tf_feedback.py +209 -0
  95. package/.tfcore/utils/tf_roadmap.py +35 -0
  96. package/README.md +4 -0
  97. package/docs/TechieFlow-Installation.md +5 -3
  98. package/package.json +1 -1
@@ -23,6 +23,8 @@
23
23
  # # marks a WRONG run record as not to be counted: both
24
24
  # # records stay, every figure skips it and says how many
25
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)
26
28
  # tf-emit.sh --origin-of REQ-UI-014 # prints "<started> <cmd> <agent>" of the last build or fix
27
29
  # # run that touched the row, nothing when there is none
28
30
  # tf-emit.sh --where # prints the resolved docs/metrics dir
@@ -292,7 +294,7 @@ if [[ "$1" == "--open-misses" ]]; then
292
294
  import json, os, sys
293
295
  DOC = {"brd", "architecture", "uidesign", "checklist", "devguide"}
294
296
  app, klass = os.environ.get("TF_OMAPP") or "", os.environ.get("TF_OMCLASS") or ""
295
- opened, fixes = [], {}
297
+ opened, fixes, voided = [], {}, set()
296
298
  try:
297
299
  with open(sys.argv[1], encoding="utf-8") as fh:
298
300
  for line in fh:
@@ -305,6 +307,8 @@ try:
305
307
  continue
306
308
  if r.get("kind") == "miss":
307
309
  opened.append(r)
310
+ elif r.get("kind") == "miss-void":
311
+ voided.add(r.get("miss_id"))
308
312
  elif r.get("kind") == "miss-fix":
309
313
  mid = r.get("miss_id")
310
314
  if mid:
@@ -314,6 +318,8 @@ try:
314
318
  except FileNotFoundError:
315
319
  pass
316
320
  for r in opened:
321
+ if r.get("miss_id") in voided:
322
+ continue
317
323
  if app and r.get("app") and r.get("app") != app:
318
324
  continue
319
325
  art = r.get("artifact") or ""
@@ -335,7 +341,7 @@ if [[ "$1" == "--open-miss" ]]; then
335
341
  python3 - "$MET_DIR/misses.jsonl" "$REQ" 2>"$TF_ERR" <<'PY' || true
336
342
  import json, sys
337
343
  path, req = sys.argv[1], sys.argv[2]
338
- opened, fixes = [], {}
344
+ opened, fixes, voided = [], {}, set()
339
345
  try:
340
346
  with open(path, encoding="utf-8") as fh:
341
347
  for line in fh:
@@ -348,6 +354,8 @@ try:
348
354
  continue
349
355
  if r.get("kind") == "miss" and r.get("req_id") == req:
350
356
  opened.append(r)
357
+ elif r.get("kind") == "miss-void":
358
+ voided.add(r.get("miss_id"))
351
359
  elif r.get("kind") == "miss-fix":
352
360
  mid = r.get("miss_id")
353
361
  if mid:
@@ -357,6 +365,8 @@ try:
357
365
  except FileNotFoundError:
358
366
  pass
359
367
  for r in reversed(opened): # most recent first
368
+ if r.get("miss_id") in voided:
369
+ continue
360
370
  fix = fixes.get(r.get("miss_id"))
361
371
  if fix is None or fix.get("verdict_after") != "Verified":
362
372
  print("%s %s" % (r.get("miss_id"), r.get("miss_class") or "other"))
@@ -458,6 +468,45 @@ PY
458
468
  exit 0
459
469
  fi
460
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
+
461
510
  # --- write helper: mark a run record as not to be counted -----------------
462
511
  # SCHEMA.md §2.7. The same doctrine as --amend, one stream over: a run record that
463
512
  # is WRONG cannot be edited or deleted, because the stream is append-only and the
@@ -536,7 +585,7 @@ esac
536
585
 
537
586
  # The program is captured into a variable rather than fed on stdin — a heredoc
538
587
  # on `python3 -` would REPLACE the caller's piped JSON and silently drop every event.
539
- TF_PROG="$(cat <<'PY'
588
+ IFS= read -r -d '' TF_PROG <<'PY' || true # bash 3.2 misreads a here-document inside a command substitution
540
589
  import json, os, sys, datetime, glob, re, subprocess, time
541
590
 
542
591
  met_dir, stream, root = sys.argv[1], sys.argv[2], sys.argv[3]
@@ -1054,20 +1103,25 @@ def _prior_runs():
1054
1103
  # (MISS-TechieFlow-20260910-04, sorted `ignored`: it was written down and not followed).
1055
1104
  #
1056
1105
  # 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.
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.
1060
1113
  #
1061
1114
  # The one legitimate overlap is two machines appending to the same merge=union stream, so
1062
1115
  # `--allow-overlap` exists and the refusal message names it. It is deliberately a flag and not a
1063
1116
  # fallback: the mistake this refuses is easy to make and invisible afterwards.
1064
- _LAST_END = None
1117
+ _LIVE = None
1065
1118
 
1066
1119
 
1067
- def _last_live_end(app):
1068
- global _LAST_END
1069
- if _LAST_END is None:
1070
- _LAST_END = {}
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 = {}
1071
1125
  voided = set()
1072
1126
  rows = []
1073
1127
  try:
@@ -1089,6 +1143,9 @@ def _last_live_end(app):
1089
1143
  for r in rows:
1090
1144
  if (r.get("app"), r.get("cmd"), r.get("started")) in voided:
1091
1145
  continue
1146
+ start = r.get("started")
1147
+ if not start:
1148
+ continue
1092
1149
  # `ended` ONLY. `ts` is when the record was WRITTEN, not when the run finished, so
1093
1150
  # falling back to it would make every historical record emitted today look like an
1094
1151
  # overlap with the record written a second ago. A run with no `ended` has no window
@@ -1103,38 +1160,37 @@ def _last_live_end(app):
1103
1160
  end = r.get("ended")
1104
1161
  if not end:
1105
1162
  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)
1163
+ _LIVE.setdefault(r.get("app"), []).append((start, end, r.get("cmd")))
1164
+ return _LIVE.get(app, [])
1110
1165
 
1111
1166
 
1112
1167
  def _overlap_ok(rec):
1113
- """False (and the reason printed) when this run would start before the last one ended."""
1168
+ """False (and the reason printed) when this run's window overlaps a live one's."""
1114
1169
  if stream != "runs" or rec.get("kind", "run") != "run" or rec.get("backfilled"):
1115
1170
  return True
1116
1171
  if os.environ.get("TF_ALLOW_OVERLAP") == "1":
1117
1172
  return True
1118
- started = rec.get("started")
1173
+ started, ended = rec.get("started"), rec.get("ended")
1119
1174
  if not started:
1120
1175
  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
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
1138
1194
 
1139
1195
 
1140
1196
  def stamp_attempt(rec):
@@ -1431,6 +1487,8 @@ _ALLOWED = {
1431
1487
  "tokens_cache_write", "cost_usd", "tokens_scope", "model",
1432
1488
  "billing_mode", "cost_source"},
1433
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"},
1434
1492
  # an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
1435
1493
  # given, what producing the reviewed output cost, what applying the corrections cost. The two
1436
1494
  # costs are copied from the runs named by reviewed_run_id and correction_run_id (their `started`).
@@ -1588,7 +1646,6 @@ except Exception as e:
1588
1646
  warn("append failed (%s) — event dropped" % e)
1589
1647
  raise SystemExit(0)
1590
1648
  PY
1591
- )"
1592
1649
  python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
1593
1650
 
1594
1651
  # The readable miss list beside the record (FR-31, Session 5 2026-09-07): after any write to the
@@ -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" "$@"
@@ -15,13 +15,17 @@
15
15
  #
16
16
  # Emits, in this order, through tf-emit.sh only:
17
17
  # 1. the fix-issues run record (cmd fix-issues, mode fix, the rows touched) — first, because the
18
- # miss-fix records below carry no numbers of their own: the emitter copies this run's token window
19
- # 2. one miss-fix per row that has an open miss, with verdict_after taken from the verify ledger
20
- # docs/.last-verify.json written by the verify the fix chained, mapped to a checklist status (PASS →
21
- # Verified; FAIL → FAIL; any other failing verdict or an absent row → Needs re-verify). A row with no open miss is listed,
18
+ # miss-fix records below carry no numbers of their own: the emitter copies this run's token window.
19
+ # A run the fix chained (the inline verify) keeps its own record, so the fix is recorded in the gaps
20
+ # around it; the first gap carries the rows and the fix's start (TF-052).
21
+ # 2. one miss-fix per row that has an open miss, with verdict_after taken from every verify the fix
22
+ # chained: the gate records written since the fix started, then the ledger docs/.last-verify.json
23
+ # when it belongs to this fix (PASS → Verified; FAIL → FAIL; any other failing verdict → Needs
24
+ # re-verify). A row no verify graded gets no miss-fix and is listed. A row with no open miss is listed,
22
25
  # not invented: a fix on a defect nobody logged is a triage gap, so open it first with tf-triage.sh
23
26
  # demote or tf-log-miss.sh, then re-run this.
24
- # --reqs defaults to every row in the ledger. Telemetry never blocks: refusals are printed, exit 0.
27
+ # --reqs defaults to every row those verifies graded. The start defaults to the fix's marker, or its
28
+ # "outer" entry when a chained verify replaced it. Telemetry never blocks: refusals are printed, exit 0.
25
29
  set -u
26
30
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
31
  [[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
@@ -40,7 +44,12 @@ while [[ $# -gt 0 ]]; do
40
44
  *) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
41
45
  esac
42
46
  done
43
- [[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | sed -n 's/.*"started":"\([^"]*\)".*/\1/p')"
47
+ # the fix's start: its own marker, or the "outer" one when a verify it chained has replaced the marker
48
+ [[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | python3 -c "import json,sys
49
+ try: m=json.load(sys.stdin)
50
+ except Exception: m={}
51
+ o=m.get('outer') or {}
52
+ print(m.get('started','') if m.get('cmd')!='verify-phase' or not o.get('started') else o['started'])" 2>/dev/null)"
44
53
  TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES" \
45
54
  TF_MISSES="$MISSES" TF_FIXCMD="$FIXCMD" TF_VERDICT="$VERDICT" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
46
55
  import datetime, json, os, subprocess
@@ -71,45 +80,86 @@ if direct:
71
80
  % (fix_cmd, closed, len(direct)))
72
81
  raise SystemExit(0)
73
82
 
83
+ met = q("--where") or "docs/metrics"
84
+ def jsonl(name):
85
+ try:
86
+ for line in open(os.path.join(met, name), encoding="utf-8", errors="replace"):
87
+ try: yield json.loads(line)
88
+ except Exception: pass
89
+ except OSError:
90
+ return
91
+ # The verdicts: every verify this fix chained, not only the last. docs/.last-verify.json holds one scoped
92
+ # verify, so a fix that verified a Phase 3 scope and then a --phase 1 scope read the Phase 3 rows as absent
93
+ # and wrote Needs re-verify on their misses (TF-052). The gate records each verify wrote since the fix
94
+ # started come first; the ledger fills in only when it belongs to this fix.
95
+ rows = {} # rid -> checklist status
96
+ if started:
97
+ for g in jsonl("gates.jsonl"):
98
+ if g.get("kind") == "gate" and g.get("app") in (None, app) and not g.get("backfilled") and g.get("gate") != "escaped" \
99
+ and (g.get("run_id") or "") >= started and g.get("req_id"):
100
+ rows[g["req_id"].upper()] = g.get("verdict")
74
101
  ledger = {}
75
102
  try:
76
103
  ledger = json.load(open(os.path.join("docs", ".last-verify.json"), encoding="utf-8"))
77
104
  except Exception:
78
105
  pass
79
- rows = ledger.get("rows") or {}
106
+ if not started or not ledger.get("run_id") or ledger["run_id"] >= started:
107
+ row_runs = ledger.get("row_runs") # a ledger kept row by row also holds older runs' rows (Lekhak TF-007)
108
+ for rid, v in (ledger.get("rows") or {}).items():
109
+ if started and isinstance(row_runs, dict) and rid not in (ledger.get("this_run") or []) \
110
+ and (row_runs.get(rid) or "") < started:
111
+ continue
112
+ # verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
113
+ # (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
114
+ rows.setdefault(rid.upper(), "Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
80
115
  reqs = [r.strip().upper() for r in os.environ["TF_REQS"].split(",") if r.strip()] or sorted(rows)
81
116
  subs = [s.strip() for s in os.environ["TF_SUBS"].split(",") if s.strip()]
82
117
  files = os.environ["TF_FILES"]
83
- run = {"kind": "run", "app": app, "cmd": "fix-issues", "mode": "fix", "ended": now, "reqs_touched": reqs, "reqs_count": len(reqs),
84
- "subagents": subs, "files_written": int(files) if files.isdigit() else len(reqs), "build_result": os.environ["TF_BUILD"]}
85
- if started: run["started"] = started
86
- # one run record per start: a close called twice must not count the fix twice
87
- runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
88
- already = False
89
- if started and os.path.isfile(runs_path):
90
- for line in open(runs_path, encoding="utf-8", errors="replace"):
91
- if '"cmd":"fix-issues"' in line and f'"started":"{started}"' in line:
92
- already = True
93
- break
94
- ok = True if already else emit("runs", run)
95
- if already:
96
- print(f"fix-issues: run record for start {started} already exists; not written again")
97
- closed, none = 0, []
118
+ # The fix's own time only: a run it chained (the inline verify) has its own record, so the fix is recorded
119
+ # in the gaps around it, the first gap keyed on the fix's start with the rows (its miss-fixes name that start).
120
+ inner, voided = [], set()
121
+ for r in jsonl("runs.jsonl"):
122
+ if r.get("kind") == "run-void":
123
+ voided.add((r.get("cmd"), r.get("started")))
124
+ elif r.get("kind", "run") == "run" and r.get("app") == app and r.get("cmd") != "fix-issues" and not r.get("backfilled") \
125
+ and started and (r.get("started") or "") >= started and (r.get("ended") or "") and r["ended"] <= now:
126
+ inner.append((r["started"], r["ended"], r.get("cmd")))
127
+ inner = sorted(w for w in inner if (w[2], w[0]) not in voided)
128
+ gaps, t = [], started
129
+ for s, e, _c in inner:
130
+ if t and s > t: gaps.append((t, s))
131
+ t = max(t, e) if t else e
132
+ if not started or not gaps or t < now: gaps.append((t, now))
133
+ existing = {r.get("started") for r in jsonl("runs.jsonl") if r.get("kind", "run") == "run" and r.get("cmd") == "fix-issues"}
134
+ wrote = had = 0
135
+ for i, (s, e) in enumerate(gaps):
136
+ first = i == 0
137
+ run = {"kind": "run", "app": app, "cmd": "fix-issues", "mode": "fix", "ended": e, "reqs_touched": reqs if first else [],
138
+ "reqs_count": len(reqs) if first else 0, "subagents": subs if first else [],
139
+ "files_written": (int(files) if files.isdigit() else len(reqs)) if first else 0, "build_result": os.environ["TF_BUILD"]}
140
+ if s: run["started"] = s
141
+ # one run record per start: a close called twice must not count the fix twice
142
+ if s and s in existing:
143
+ had += 1; continue
144
+ wrote += emit("runs", run)
145
+ if inner:
146
+ print(f"fix-issues: recorded around {len(inner)} chained run(s) ({', '.join(sorted({c for _s, _e, c in inner}))}) in {len(gaps)} segment(s)")
147
+ closed, none, unverified = 0, [], []
98
148
  for rid in reqs:
149
+ if rid not in rows:
150
+ unverified.append(rid); continue
99
151
  opened = q("--open-miss", rid)
100
152
  if not opened:
101
153
  none.append(rid); continue
102
154
  mid = opened.split()[0]
103
155
  fa = q("--next-fix-attempt", mid) or "1"
104
- v = rows.get(rid)
105
- # verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
106
- # (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
107
- after = ("Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
108
156
  rec = {"kind": "miss-fix", "miss_id": mid, "req_id": rid, "fix_cmd": "fix-issues", "fix_attempt": int(fa) if fa.isdigit() else 1,
109
- "verdict_after": after, "reopened": False}
157
+ "verdict_after": rows[rid], "reopened": False}
110
158
  if started: rec["fix_run_id"] = started
111
159
  if emit("misses", rec): closed += 1
112
- print(f"fix-issues: run record {'already there' if already else 'written' if ok else 'not written'}; {closed} miss-fix record(s) with the verifier's verdict"
113
- + (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else ""))
160
+ state = "already there" if had == len(gaps) else "written" if wrote + had == len(gaps) else "not written"
161
+ print(f"fix-issues: run record {state}; {closed} miss-fix record(s) with the verifier's verdict"
162
+ + (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else "")
163
+ + (f"; no verify graded {', '.join(unverified)} during this fix, so its miss stays open with no miss-fix" if unverified else ""))
114
164
  PY
115
165
  exit 0
@@ -68,6 +68,8 @@
68
68
  # Override command lines with TF_GOAL_CLAUDE_FLAGS / TF_GOAL_OPENCODE_FLAGS /
69
69
 
70
70
  set -u
71
+ # GNU-only commands (setsid, stat -c, date -d) go through the shim, so a stock Mac runs this too.
72
+ source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh"
71
73
 
72
74
  HARNESS="claude"; MODEL=""; BUFFER_MIN=15; DEFAULT_WAIT_MIN=60; MAX_CYCLES=60; IDLE_RETRY=30
73
75
  PROBE_MIN=15; PROBE_MAX_H=8; STALL_MIN=15
@@ -162,7 +164,7 @@ else
162
164
  _lr="$(state_get last_reason)"
163
165
  case "$_lr" in
164
166
  done:*|max-cycles|stopped|"") ;;
165
- *) echo "tf-goal: a run looks active in $APP_DIR (goal.json last_reason=$_lr, touched $(date -u -r "$STATE" +%H:%MZ)). Wait for it, or continue it with --resume." >&2; exit 2 ;;
167
+ *) echo "tf-goal: a run looks active in $APP_DIR (goal.json last_reason=$_lr, touched $(tf_date_from -u "@$(tf_stat_mtime "$STATE")" +%H:%MZ)). Wait for it, or continue it with --resume." >&2; exit 2 ;;
166
168
  esac
167
169
  fi
168
170
  CYCLE=0; SESSION_ID=""; STALLS=0
@@ -436,11 +438,11 @@ probe_until_clear() {
436
438
  else
437
439
  ( cd "$APP_DIR" && opencode run --auto "Reply with the single word OK." ) > "$pout" 2>&1; prc=$?
438
440
  fi
439
- if [[ $prc -eq 0 ]] && ! grep -qiE 'usage limit|hit your limit|rate[ _-]?limit|limit (has been )?(reached|exceeded)|too many requests|\b429\b|overloaded|weekly limit|resets? (at|in)\b' "$pout"; then
441
+ if [[ $prc -eq 0 ]] && ! grep -qiE 'usage limit|hit your limit|rate[ _-]?limit|limit (has been )?(reached|exceeded)|too many requests|(^|[^[:alnum:]_])429([^[:alnum:]_]|$)|overloaded|weekly limit|resets? (at|in)([^[:alnum:]_]|$)' "$pout"; then
440
442
  log "probe #$n OK"; return 0
441
443
  fi
442
444
  log "probe #$n still limited (rc=$prc: $(head -c 120 "$pout" | tr '\n' ' ')) — next in ${PROBE_MIN}m"
443
- state_set resume_at "probe #$((n+1)) at $(date -d "+${PROBE_MIN} min" '+%H:%M' 2>/dev/null)"
445
+ state_set resume_at "probe #$((n+1)) at $(tf_date_from "+${PROBE_MIN} min" '+%H:%M' 2>/dev/null)"
444
446
  if [[ $(date +%s) -ge $deadline ]]; then log "probe window (${PROBE_MAX_H}h) exhausted — resuming anyway"; return 1; fi
445
447
  nap $(( PROBE_MIN * 60 ))
446
448
  done
@@ -450,7 +452,7 @@ sleep_until() { # epoch
450
452
  local target="$1" now left
451
453
  while :; do
452
454
  now=$(date +%s); left=$(( target - now )); [[ $left -le 0 ]] && break
453
- state_set resume_at "$(date -u -d "@$target" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "$target")"
455
+ state_set resume_at "$(tf_date_from -u "@$target" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "$target")"
454
456
  if [[ $left -gt 300 ]]; then nap 300; else nap "$left"; fi
455
457
  done
456
458
  state_set resume_at ""
@@ -482,16 +484,14 @@ run_cycle() { # runs "${CMD[@]}" in $APP_DIR, output to $OUT and $LOG; sets RC,
482
484
  STALLED=0
483
485
  local quiet=0 prev=-1 size
484
486
  if [[ -n "${TF_GOAL_FAKE_CMD:-}" ]]; then CMD=(bash -c "$TF_GOAL_FAKE_CMD"); fi
485
- if command -v setsid >/dev/null 2>&1; then
486
- ( cd "$APP_DIR" && TF_YOLO=1 exec setsid "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
487
- else
488
- ( cd "$APP_DIR" && TF_YOLO=1 exec "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
489
- fi
487
+ # Its own session and process group (tf_setsid: setsid(1), or python3/perl on a Mac), so
488
+ # kill_child can stop the whole tree.
489
+ ( cd "$APP_DIR" && TF_YOLO=1 tf_setsid --exec "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
490
490
  CHILD=$!
491
491
  while kill -0 "$CHILD" 2>/dev/null; do
492
492
  nap "$STALL_TICK"
493
493
  kill -0 "$CHILD" 2>/dev/null || break
494
- size="$(stat -c %s "$OUT" 2>/dev/null || echo 0)"
494
+ size="$(tf_stat_size "$OUT" 2>/dev/null || echo 0)"
495
495
  if [[ "$size" == "$prev" ]]; then quiet=$(( quiet + STALL_TICK )); else quiet=0; prev="$size"; fi
496
496
  if [[ "$STALL_SEC" -gt 0 && $quiet -ge "$STALL_SEC" ]]; then
497
497
  log "STALL: no output for $(( quiet / 60 ))m$(( quiet % 60 ))s (cycle $CYCLE, ${size} bytes) — stopping the harness, re-prompting"
@@ -569,7 +569,7 @@ while :; do
569
569
  OUT="$STATE_DIR/goal-cycle-$CYCLE.out"
570
570
  if [[ $DRY -eq 1 ]]; then echo "dry run — would launch cycle $CYCLE ($KIND):" >&2; printf ' %q' "${CMD[@]}"; echo; exit 0; fi
571
571
  state_set cycle "$CYCLE"; CYCLE_START="$(date +%s)"
572
- log "cycle $CYCLE ($KIND) → ${CMD[*]:0:6} … (prompt ${#CMD[-1]} chars)"
572
+ log "cycle $CYCLE ($KIND) → ${CMD[*]:0:6} … (prompt ${#CMD[${#CMD[@]}-1]} chars)"
573
573
 
574
574
  # The run's start is now, not when the agent reaches step 0: write an unclaimed marker the
575
575
  # first command's `tf-phase.sh start` claims (MISS-TechieFlow-20260905-20).
@@ -620,7 +620,7 @@ while :; do
620
620
  log "probe succeeded — resuming session ${SESSION_ID:-(--continue)} after a ${BUFFER_MIN}m buffer"
621
621
  nap $(( BUFFER_MIN * 60 ))
622
622
  else
623
- WHEN="$(date -d "@$DETAIL" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || echo "$DETAIL")"
623
+ WHEN="$(tf_date_from "@$DETAIL" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || echo "$DETAIL")"
624
624
  log "USAGE LIMIT hit (cycle $CYCLE) — RETRY AT $WHEN (stated reset + ${BUFFER_MIN}m buffer). Sleeping."
625
625
  state_set last_reason "limit" resume_at "$WHEN"
626
626
  sleep_until "$DETAIL"
@@ -48,13 +48,18 @@ _detect() {
48
48
  fi
49
49
  local k
50
50
  for k in $(compgen -v | grep '^OPENCODE' 2>/dev/null); do printf 'opencode'; return; done
51
- # /proc ancestry walk (Linux/WSL); bounded
51
+ # ancestry walk, bounded: /proc on Linux/WSL, ps where there is no /proc (macOS)
52
52
  local pid=$PPID name ppid seen=0 stat
53
53
  while [[ -n "$pid" && "$pid" -gt 1 && $seen -lt 12 ]]; do
54
54
  seen=$((seen + 1))
55
- stat="$(cat /proc/$pid/stat 2>/dev/null)" || break
56
- name="${stat#*(}"; name="${name%%)*}"
57
- ppid="$(printf '%s' "${stat##*) }" | awk '{print $2}')"
55
+ if [[ -d /proc/self ]]; then
56
+ stat="$(cat /proc/$pid/stat 2>/dev/null)" || break
57
+ name="${stat#*(}"; name="${name%%)*}"
58
+ ppid="$(printf '%s' "${stat##*) }" | awk '{print $2}')"
59
+ else
60
+ ppid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')"; [[ -n "$ppid" ]] || break
61
+ name="$(ps -o comm= -p "$pid" 2>/dev/null)"; name="${name##*/}"
62
+ fi
58
63
  case "$name" in
59
64
  *opencode*) printf 'opencode'; return ;;
60
65
  *claude*) printf 'claude-code'; return ;;
@@ -0,0 +1,29 @@
1
+ # tf-lock.sh — one build or browser check at a time per repository. Sourced, not run.
2
+ # source "$HERE/tf-lock.sh"; tf_take_lock <label> waits for the lock, then holds it until exit
3
+ # A directory, because mkdir is atomic on every drive this runs on. A lock whose process has gone
4
+ # is taken over. A script started by the holder (tf-verify-tests.sh running tf-build.sh) shares it
5
+ # through TF_REPO_LOCK_PID. TF_BUILD_LOCK_MAX seconds to wait (1800). (TF-043, TfLens TF-048)
6
+ TF_LOCK="tests/.artifacts/build/.lock"
7
+ tf_release_lock() { [[ "$(cut -d' ' -f1 "$TF_LOCK/owner" 2>/dev/null)" == "$$" ]] && rm -rf "$TF_LOCK"; }
8
+ tf_take_lock() {
9
+ local label="${1:-}" waited=0 max="${TF_BUILD_LOCK_MAX:-1800}" said=0 owner opid ohost
10
+ if [[ -n "${TF_REPO_LOCK_PID:-}" ]] && kill -0 "$TF_REPO_LOCK_PID" 2>/dev/null; then return 0; fi
11
+ mkdir -p "$(dirname "$TF_LOCK")"
12
+ while ! mkdir "$TF_LOCK" 2>/dev/null; do
13
+ owner="$(cat "$TF_LOCK/owner" 2>/dev/null)"; opid="$(cut -d' ' -f1 <<<"$owner")"; ohost="$(cut -d' ' -f2 <<<"$owner")"
14
+ if [[ -z "$owner" ]]; then
15
+ [[ -n "$(find "$TF_LOCK" -maxdepth 0 -mmin +1 2>/dev/null)" ]] && { rm -rf "$TF_LOCK"; continue; }
16
+ elif [[ "$ohost" == "$(hostname)" ]] && ! kill -0 "$opid" 2>/dev/null; then
17
+ rm -rf "$TF_LOCK"; continue
18
+ fi
19
+ [[ $said -eq 0 ]] && { echo "wait another build or browser check is running in this repository (${owner:-starting}); this one starts when it finishes" >&2; said=1; }
20
+ if [[ $waited -ge $max ]]; then
21
+ echo "NOT-RUN another build or browser check held this repository for $((max / 60)) minutes ($owner). If nothing is running, remove $TF_LOCK and run again. Not a code error"
22
+ exit 2
23
+ fi
24
+ sleep 3; waited=$((waited + 3))
25
+ done
26
+ echo "$$ $(hostname) $label $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TF_LOCK/owner"
27
+ export TF_REPO_LOCK_PID=$$
28
+ trap tf_release_lock EXIT
29
+ }
@@ -16,7 +16,8 @@ re-logged; a null why_missed or sort on it is completed with --amend); the origi
16
16
  (tf-emit.sh --origin-of); the miss record with the sentence as `what` and the sort; the checklist line
17
17
  (the row demoted to Needs re-verify with "⚠ miss <date>: <sentence>", or a new Not Started row with
18
18
  BRD-pending when nothing owns it; nothing when the repo has no checklist); with --fixed, a miss-fix
19
- record instead of a demotion; then the run record (cmd log-miss). The emitter rewrites the readable
19
+ record instead of a demotion; then the run record (cmd log-miss) — none when another command is
20
+ running, because that command's own record covers the time (TF-029). The emitter rewrites the readable
20
21
  docs/<App>-Misses.md. Prints the report block. Exit 0 · 2 could not run.
21
22
  """
22
23
  import datetime
@@ -91,11 +92,31 @@ def main(argv):
91
92
  found_by = opt(argv, "--found-by", "owner")
92
93
  fixed = "--fixed" in argv
93
94
  started = opt(argv, "--started") or ""
95
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
96
+ # The command whose run record covers this time. Called inside another command (*amend-docs
97
+ # step 10), that command's record covers it, so this script writes none: a log-miss run over
98
+ # the enclosing command's window made the enclosing record overlap and be refused (TF-029).
99
+ # A marker older than 24 h belongs to a session that died, and is ignored as every reader does.
100
+ running = "log-miss"
94
101
  if not started:
102
+ # The marker's own cmd and started, never its "outer" block: a command started after another
103
+ # one keeps that one as "outer", written first, and a first-match read took it for this one
104
+ # (TF-052 follow-up: log-miss after *triage-issues wrote no run record).
95
105
  mk = subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
96
- m = re.search(r'"started":"([^"]*)"', mk)
97
- started = m.group(1) if m else datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
98
- now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
106
+ try:
107
+ marker = json.loads(mk) if mk.strip().startswith("{") else {}
108
+ except ValueError:
109
+ marker = {}
110
+ m_started, m_cmd = marker.get("started") or "", marker.get("cmd") or ""
111
+ try:
112
+ age = (datetime.datetime.now(datetime.timezone.utc)
113
+ - datetime.datetime.strptime(m_started, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc))
114
+ fresh = age < datetime.timedelta(hours=24)
115
+ except ValueError:
116
+ fresh = False
117
+ started = m_started if fresh else now
118
+ if fresh and m_cmd and m_cmd not in ("log-miss", "goal"):
119
+ running = m_cmd
99
120
 
100
121
  spec = importlib.util.spec_from_file_location("tf_checklist_edit", os.path.join(HERE, "tf-checklist-edit.py"))
101
122
  ce = importlib.util.module_from_spec(spec)
@@ -165,10 +186,14 @@ def main(argv):
165
186
  fix_ok, _ = emit("misses", fr)
166
187
  fix_line = ("closed in the same run" + (f" against run {fr['fix_run_id']}" if "fix_run_id" in fr else " (fix run unknown: costed none)")) if fix_ok else "miss-fix not written"
167
188
 
168
- # 5. the run record
169
- run = {"kind": "run", "app": app, "cmd": "log-miss", "mode": None, "started": started, "ended": now,
170
- "reqs_touched": [req] if req else [], "reqs_count": 1 if req else 0, "subagents": [], "files_written": 1 if has_cl else 0, "build_result": "not-run"}
171
- emit("runs", run)
189
+ # 5. the run record — only when log-miss is the command running
190
+ if running == "log-miss":
191
+ run = {"kind": "run", "app": app, "cmd": "log-miss", "mode": None, "started": started, "ended": now,
192
+ "reqs_touched": [req] if req else [], "reqs_count": 1 if req else 0, "subagents": [], "files_written": 1 if has_cl else 0, "build_result": "not-run"}
193
+ emit("runs", run)
194
+ run_line = f"log-miss, {started} to {now}"
195
+ else:
196
+ run_line = f"none written — the running *{running}'s own record covers this time"
172
197
 
173
198
  if not ok:
174
199
  print(f"# Miss NOT recorded — {app}")
@@ -189,6 +214,7 @@ def main(argv):
189
214
  print(f"Docs : {doc_line}; docs/{app}-Misses.md rewritten")
190
215
  if fix_line:
191
216
  print(f"Fix : {fix_line}")
217
+ print(f"Run record : {run_line}")
192
218
  print(f"Next : *fix-issues {app}" + (f" {req}" if req and not fixed else ""))
193
219
  return 0
194
220