@techierathore/techieflow 1.0.4 → 1.1.1

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 (148) hide show
  1. package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
  2. package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
  3. package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
  4. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  6. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  9. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
  10. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  11. package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
  12. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  14. package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
  15. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  16. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  17. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  18. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  19. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  20. package/.opencode/plugin/techieflow.js +194 -14
  21. package/.tfcore/agents/flow-master.md +2 -0
  22. package/.tfcore/agents/verifier.md +2 -2
  23. package/.tfcore/core-config.yaml +2 -1
  24. package/.tfcore/hooks/block-git.sh +4 -2
  25. package/.tfcore/hooks/guard-build.sh +5 -1
  26. package/.tfcore/hooks/guard-db.sh +112 -13
  27. package/.tfcore/hooks/guard-status-html.sh +86 -2
  28. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  29. package/.tfcore/hooks/guard-verify.sh +8 -3
  30. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  31. package/.tfcore/hooks/yolo-allow.sh +27 -0
  32. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  33. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  34. package/.tfcore/standards/owner-words.txt +23 -0
  35. package/.tfcore/tasks/_develop-mode.md +34 -0
  36. package/.tfcore/tasks/_owner-language.md +1 -0
  37. package/.tfcore/tasks/_status-update-gate.md +4 -3
  38. package/.tfcore/tasks/_yolo-mode.md +2 -2
  39. package/.tfcore/tasks/amend-docs.md +2 -2
  40. package/.tfcore/tasks/day1-brownfield.md +3 -3
  41. package/.tfcore/tasks/day1-greenfield.md +5 -5
  42. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  43. package/.tfcore/tasks/fix-issues.md +3 -2
  44. package/.tfcore/tasks/generate-html.md +2 -2
  45. package/.tfcore/tasks/handoff-phase.md +2 -2
  46. package/.tfcore/tasks/mockups.md +2 -2
  47. package/.tfcore/tasks/productguide.md +2 -2
  48. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  49. package/.tfcore/tasks/triage-and-fix.md +2 -2
  50. package/.tfcore/tasks/triage-issues.md +2 -2
  51. package/.tfcore/tasks/verify-phase.md +12 -11
  52. package/.tfcore/telemetry/SCHEMA.md +10 -2
  53. package/.tfcore/telemetry/install-metrics.sh +3 -2
  54. package/.tfcore/telemetry/tf-metrics.sh +38 -2
  55. package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
  56. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  57. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  58. package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
  59. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  60. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  61. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  62. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  63. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  64. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  65. package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
  66. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  67. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  68. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  69. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  70. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  71. package/.tfcore/utils/tf-appium.mjs +179 -0
  72. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  73. package/.tfcore/utils/tf-assets.sh +97 -12
  74. package/.tfcore/utils/tf-brd-status.py +14 -5
  75. package/.tfcore/utils/tf-build-list.py +95 -12
  76. package/.tfcore/utils/tf-build-list.sh +2 -1
  77. package/.tfcore/utils/tf-build.sh +163 -15
  78. package/.tfcore/utils/tf-checklist-edit.py +21 -3
  79. package/.tfcore/utils/tf-ci-repro.py +685 -0
  80. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  81. package/.tfcore/utils/tf-day1-files.py +55 -28
  82. package/.tfcore/utils/tf-develop.sh +264 -0
  83. package/.tfcore/utils/tf-devguide-list.py +53 -24
  84. package/.tfcore/utils/tf-doc-check.py +218 -26
  85. package/.tfcore/utils/tf-doc-tests.sh +116 -0
  86. package/.tfcore/utils/tf-emit.sh +132 -45
  87. package/.tfcore/utils/tf-feedback.sh +13 -0
  88. package/.tfcore/utils/tf-fix-close.sh +87 -31
  89. package/.tfcore/utils/tf-goal.sh +12 -12
  90. package/.tfcore/utils/tf-harness.sh +9 -4
  91. package/.tfcore/utils/tf-lock.sh +29 -0
  92. package/.tfcore/utils/tf-log-miss.py +34 -8
  93. package/.tfcore/utils/tf-login.mjs +167 -0
  94. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  95. package/.tfcore/utils/tf-menus.mjs +401 -0
  96. package/.tfcore/utils/tf-misses-md.py +10 -4
  97. package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
  98. package/.tfcore/utils/tf-mockup-parity.sh +25 -2
  99. package/.tfcore/utils/tf-owner-text.py +233 -0
  100. package/.tfcore/utils/tf-owner-text.sh +14 -0
  101. package/.tfcore/utils/tf-phase.sh +38 -4
  102. package/.tfcore/utils/tf-portable.sh +230 -0
  103. package/.tfcore/utils/tf-render-html.py +79 -5
  104. package/.tfcore/utils/tf-req-ids.py +36 -0
  105. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  106. package/.tfcore/utils/tf-split-brd.py +11 -1
  107. package/.tfcore/utils/tf-stack-check.sh +3 -0
  108. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  109. package/.tfcore/utils/tf-status-facts.py +160 -45
  110. package/.tfcore/utils/tf-theme.mjs +189 -0
  111. package/.tfcore/utils/tf-triage.py +74 -17
  112. package/.tfcore/utils/tf-triage.sh +2 -0
  113. package/.tfcore/utils/tf-verify-boot.sh +529 -54
  114. package/.tfcore/utils/tf-verify-emit.sh +57 -17
  115. package/.tfcore/utils/tf-verify-env.sh +33 -4
  116. package/.tfcore/utils/tf-verify-list.py +290 -24
  117. package/.tfcore/utils/tf-verify-list.sh +3 -1
  118. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  119. package/.tfcore/utils/tf-verify-screens.mjs +341 -44
  120. package/.tfcore/utils/tf-verify-screens.sh +23 -5
  121. package/.tfcore/utils/tf-verify-tests.sh +310 -19
  122. package/.tfcore/utils/tf-verify-verdict.py +163 -21
  123. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  124. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  125. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  126. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  127. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  128. package/.tfcore/utils/tf-yolo.sh +31 -3
  129. package/.tfcore/utils/tf_defect.py +27 -0
  130. package/.tfcore/utils/tf_feedback.py +272 -0
  131. package/.tfcore/utils/tf_roadmap.py +35 -0
  132. package/.tfcore/utils/tf_stack_check.py +93 -0
  133. package/README.md +6 -2
  134. package/docs/TechieFlow-Installation.md +5 -3
  135. package/opencode.jsonc +6 -2
  136. package/package.json +1 -1
  137. package/scripts/install.mjs +13 -2
  138. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  139. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  140. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  141. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  142. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  143. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  144. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  145. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  146. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  147. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  148. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
@@ -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]
@@ -966,16 +1015,34 @@ def _window_opencode(t0, t1):
966
1015
  return None
967
1016
  dbp = ptr.get("db_path")
968
1017
  sid = ptr.get("session_id")
969
- if not dbp or not sid or not os.path.isfile(dbp):
1018
+ if not dbp or not os.path.isfile(dbp):
970
1019
  return None
971
1020
  try:
972
1021
  import sqlite3
973
1022
  db = sqlite3.connect("file:%s?mode=ro" % dbp, uri=True)
974
- pairs = db.execute("SELECT id, parent_id FROM session").fetchall()
1023
+ # OpenCode 2 keeps sessions in session_v2 and messages in session_message; the old tables
1024
+ # stopped on 2026-09-27, so every OpenCode record since read tokens_scope "none". Both are read.
1025
+ tables = {r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
1026
+ pairs = db.execute("SELECT id, parent_id FROM session").fetchall() if "session" in tables else []
1027
+ if "session_v2" in tables:
1028
+ pairs += db.execute("SELECT id, parent_id FROM session_v2").fetchall()
975
1029
  kids = {}
976
1030
  for i, p in pairs:
977
1031
  kids.setdefault(p, []).append(i)
978
- tree, queue = {sid}, [sid]
1032
+ # The roots: the pointer's session, and every root session OpenCode 2 recorded in THIS project's
1033
+ # folder. Each *develop-end-to-end phase runs in a new session and the plugin's pointer kept the
1034
+ # first one, so the later phases read "none" (2026-10-08). Messages are still taken only inside
1035
+ # the record's own window.
1036
+ roots = {sid} if sid else set()
1037
+ if "session_v2" in tables:
1038
+ here = os.path.realpath(root)
1039
+ for i, p_, dr in db.execute("SELECT id, parent_id, directory FROM session_v2"):
1040
+ if not p_ and dr and os.path.realpath(dr) == here:
1041
+ roots.add(i)
1042
+ if not roots:
1043
+ db.close()
1044
+ return None
1045
+ tree, queue = set(roots), list(roots)
979
1046
  while queue:
980
1047
  for c in kids.get(queue.pop(), []):
981
1048
  if c not in tree:
@@ -987,15 +1054,20 @@ def _window_opencode(t0, t1):
987
1054
  # Same rule as the Claude branch: a CHILD session counts as a subagent run
988
1055
  # for THIS window only if it produced output inside it.
989
1056
  child_out = {}
990
- q = "SELECT session_id, data FROM message WHERE time_created BETWEEN ? AND ?"
991
- for msid, data in db.execute(q, (t0, t1)):
1057
+ rows = []
1058
+ if "message" in tables:
1059
+ rows += db.execute("SELECT session_id, data FROM message WHERE time_created BETWEEN ? AND ?", (t0, t1)).fetchall()
1060
+ if "session_message" in tables:
1061
+ rows += [(a, b) for a, b in db.execute(
1062
+ "SELECT session_id, data FROM session_message WHERE type = 'assistant' AND time_created BETWEEN ? AND ?", (t0, t1))]
1063
+ for msid, data in rows:
992
1064
  if msid not in tree:
993
1065
  continue
994
1066
  try:
995
1067
  d = json.loads(data)
996
1068
  except Exception:
997
1069
  continue
998
- if d.get("role") != "assistant":
1070
+ if d.get("role") not in (None, "assistant"):
999
1071
  continue
1000
1072
  t = d.get("tokens") or {}
1001
1073
  cache = t.get("cache") or {}
@@ -1005,9 +1077,11 @@ def _window_opencode(t0, t1):
1005
1077
  tot["cr"] += cache.get("read") or 0
1006
1078
  tot["cw"] += cache.get("write") or 0
1007
1079
  cost += d.get("cost") or 0
1008
- if msid != sid and o:
1080
+ if msid not in roots and o:
1009
1081
  child_out[msid] = child_out.get(msid, 0) + o
1010
- m = (d.get("providerID", "") + "/" if d.get("providerID") else "") + (d.get("modelID") or "")
1082
+ mo = d.get("model") if isinstance(d.get("model"), dict) else {}
1083
+ prov = d.get("providerID") or mo.get("providerID") or ""
1084
+ m = (prov + "/" if prov else "") + (d.get("modelID") or mo.get("id") or "")
1011
1085
  if m:
1012
1086
  models[m] = models.get(m, 0) + o
1013
1087
  db.close()
@@ -1054,20 +1128,25 @@ def _prior_runs():
1054
1128
  # (MISS-TechieFlow-20260910-04, sorted `ignored`: it was written down and not followed).
1055
1129
  #
1056
1130
  # 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.
1131
+ # app, a live `run` record's window may not OVERLAP another live `run` record's window. It is the
1132
+ # windows that are compared, not the order of writing: a command that chains another one inside
1133
+ # itself -- *build-phase chaining *verify -- finishes last but did its own first segment before the
1134
+ # inner run started, and comparing against the newest record alone meant that segment could never be
1135
+ # recorded at all (TfLens TF-040, 80 minutes and five builder clusters on no record). Two windows
1136
+ # that merely touch at a boundary do not overlap. Voided and backfilled records are ignored, which
1137
+ # is what makes the void-then-re-emit flow work. Records with no `started` are untouched.
1060
1138
  #
1061
1139
  # The one legitimate overlap is two machines appending to the same merge=union stream, so
1062
1140
  # `--allow-overlap` exists and the refusal message names it. It is deliberately a flag and not a
1063
1141
  # fallback: the mistake this refuses is easy to make and invisible afterwards.
1064
- _LAST_END = None
1142
+ _LIVE = None
1065
1143
 
1066
1144
 
1067
- def _last_live_end(app):
1068
- global _LAST_END
1069
- if _LAST_END is None:
1070
- _LAST_END = {}
1145
+ def _live_windows(app):
1146
+ """[(started, ended, cmd)] of every live run record for this app, oldest first."""
1147
+ global _LIVE
1148
+ if _LIVE is None:
1149
+ _LIVE = {}
1071
1150
  voided = set()
1072
1151
  rows = []
1073
1152
  try:
@@ -1089,6 +1168,9 @@ def _last_live_end(app):
1089
1168
  for r in rows:
1090
1169
  if (r.get("app"), r.get("cmd"), r.get("started")) in voided:
1091
1170
  continue
1171
+ start = r.get("started")
1172
+ if not start:
1173
+ continue
1092
1174
  # `ended` ONLY. `ts` is when the record was WRITTEN, not when the run finished, so
1093
1175
  # falling back to it would make every historical record emitted today look like an
1094
1176
  # overlap with the record written a second ago. A run with no `ended` has no window
@@ -1103,38 +1185,37 @@ def _last_live_end(app):
1103
1185
  end = r.get("ended")
1104
1186
  if not end:
1105
1187
  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)
1188
+ _LIVE.setdefault(r.get("app"), []).append((start, end, r.get("cmd")))
1189
+ return _LIVE.get(app, [])
1110
1190
 
1111
1191
 
1112
1192
  def _overlap_ok(rec):
1113
- """False (and the reason printed) when this run would start before the last one ended."""
1193
+ """False (and the reason printed) when this run's window overlaps a live one's."""
1114
1194
  if stream != "runs" or rec.get("kind", "run") != "run" or rec.get("backfilled"):
1115
1195
  return True
1116
1196
  if os.environ.get("TF_ALLOW_OVERLAP") == "1":
1117
1197
  return True
1118
- started = rec.get("started")
1198
+ started, ended = rec.get("started"), rec.get("ended")
1119
1199
  if not started:
1120
1200
  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
1201
+ def key(v):
1202
+ k = _iso_ms(v)
1203
+ return k if k is not None else v # unparseable: compare as strings, as before
1204
+ s, e = key(started), key(ended or started)
1205
+ for (ps, pe, pcmd) in _live_windows(rec.get("app")):
1206
+ a, b = key(ps), key(pe)
1207
+ if type(a) is not type(s) or type(b) is not type(e):
1208
+ continue
1209
+ if s < b and a < e: # windows that only touch at a boundary are fine
1210
+ sys.stdout.write(
1211
+ "tf-emit: REFUSED — this run (%s to %s) overlaps the %s run of %s already on the "
1212
+ "stream (%s to %s). Every total over the two would be that much too big. Record the "
1213
+ "part that did not overlap -- a command that chained another one records its own "
1214
+ "segment before the inner run started -- or pass --allow-overlap if they really did "
1215
+ "run at the same time (a second machine on the same stream). Nothing was appended.\n"
1216
+ % (started, ended or "now", pcmd, rec.get("app"), ps, pe))
1217
+ return False
1218
+ return True
1138
1219
 
1139
1220
 
1140
1221
  def stamp_attempt(rec):
@@ -1407,7 +1488,7 @@ _ALLOWED = {
1407
1488
  "routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
1408
1489
  "cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
1409
1490
  "model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts",
1410
- "billing_mode", "cost_source", "fallback_from"},
1491
+ "billing_mode", "cost_source", "fallback_from", "app_phase"},
1411
1492
  # A correction, not a deletion: it names a run record that is wrong by the pair every
1412
1493
  # run carries (`cmd` + `started`) and says why, so every figure can skip it while the
1413
1494
  # record itself stays where it is. Written by --void-run, which refuses one that names
@@ -1418,7 +1499,7 @@ _ALLOWED = {
1418
1499
  "tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
1419
1500
  "tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
1420
1501
  "subagent_runs", "tokens_out_subagents", "session_id",
1421
- "billing_mode", "cost_source", "fallback_from"},
1502
+ "billing_mode", "cost_source", "fallback_from", "app_phase"},
1422
1503
  ("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
1423
1504
  "cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
1424
1505
  ("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
@@ -1431,6 +1512,8 @@ _ALLOWED = {
1431
1512
  "tokens_cache_write", "cost_usd", "tokens_scope", "model",
1432
1513
  "billing_mode", "cost_source"},
1433
1514
  ("misses", "miss-amend"): {"miss_id", "field", "value"},
1515
+ # a miss that should never have been written, withdrawn by --void-miss (SCHEMA.md §5.5.11)
1516
+ ("misses", "miss-void"): {"miss_id", "reason"},
1434
1517
  # an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
1435
1518
  # given, what producing the reviewed output cost, what applying the corrections cost. The two
1436
1519
  # costs are copied from the runs named by reviewed_run_id and correction_run_id (their `started`).
@@ -1461,6 +1544,11 @@ def _shape_ok(rec):
1461
1544
  if kind == "run" and rec.get("build_result") not in (None, "pass", "fail", "not-run"):
1462
1545
  sys.stdout.write("tf-emit: REFUSED — build_result must be pass, fail or not-run (SCHEMA.md §2); got %r. Put the detail in the checklist Remarks or Known blockers, not here. Nothing was appended.\n" % (rec.get("build_result"),))
1463
1546
  return False
1547
+ # one REQ's verdict, never a suite note: Lekhak hand-wrote four {"gate":"verify-suite","result":…}
1548
+ # records with neither, and the report counted each as a failure (TF-020)
1549
+ if kind == "gate" and not rec.get("backfilled") and not (rec.get("req_id") and rec.get("verdict")):
1550
+ sys.stdout.write("tf-emit: REFUSED — a gate record is one REQ's verdict and needs both req_id and verdict (SCHEMA.md §3). A suite result (unit tests, the whole browser run) belongs in the run record's notes, not here. Nothing was appended.\n")
1551
+ return False
1464
1552
  if kind == "miss" and not rec.get("miss_id"):
1465
1553
  sys.stdout.write("tf-emit: REFUSED — a miss record needs a miss_id; get one with: bash .tfcore/utils/tf-emit.sh --next-miss-id. Nothing was appended.\n")
1466
1554
  return False
@@ -1588,7 +1676,6 @@ except Exception as e:
1588
1676
  warn("append failed (%s) — event dropped" % e)
1589
1677
  raise SystemExit(0)
1590
1678
  PY
1591
- )"
1592
1679
  python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
1593
1680
 
1594
1681
  # 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" "$@"
@@ -3,6 +3,7 @@
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
+ # [--cmd triage-and-fix] # the command the run record is filed under
6
7
  # bash .tfcore/utils/tf-fix-close.sh <App> --misses MISS-App-20260901-06,MISS-App-20260830-03 \
7
8
  # --fix-cmd amend-docs [--verdict Verified]
8
9
  #
@@ -15,18 +16,25 @@
15
16
  #
16
17
  # Emits, in this order, through tf-emit.sh only:
17
18
  # 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,
19
+ # miss-fix records below carry no numbers of their own: the emitter copies this run's token window.
20
+ # A run the fix chained (the inline verify) keeps its own record, so the fix is recorded in the gaps
21
+ # around it; the first gap carries the rows and the fix's start (TF-052).
22
+ # 2. one miss-fix per row that has an open miss, with verdict_after taken from every verify the fix
23
+ # chained: the gate records written since the fix started, then the ledger docs/.last-verify.json
24
+ # when it belongs to this fix (PASS → Verified; FAIL → FAIL; any other failing verdict → Needs
25
+ # re-verify). A row no verify graded gets no miss-fix and is listed. A row with no open miss is listed,
22
26
  # not invented: a fix on a defect nobody logged is a triage gap, so open it first with tf-triage.sh
23
27
  # 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.
28
+ # --cmd names the command the run record is filed under (default fix-issues): *triage-and-fix passes
29
+ # --cmd triage-and-fix, so its run is counted as itself and no second record over the same window is
30
+ # needed, which the overlap rule refused (TrBlazeUI TF-004). The miss-fix records keep fix_cmd fix-issues.
31
+ # --reqs defaults to every row those verifies graded. The start defaults to the fix's marker, or its
32
+ # "outer" entry when a chained verify replaced it. Telemetry never blocks: refusals are printed, exit 0.
25
33
  set -u
26
34
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
35
  [[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
28
36
  APP="$1"; shift
29
- STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""; MISSES=""; FIXCMD="fix-issues"; VERDICT=""
37
+ STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""; MISSES=""; FIXCMD="fix-issues"; VERDICT=""; RUNCMD="fix-issues"
30
38
  while [[ $# -gt 0 ]]; do
31
39
  case "$1" in
32
40
  --started) STARTED="${2:-}"; shift 2 ;;
@@ -37,14 +45,21 @@ while [[ $# -gt 0 ]]; do
37
45
  --misses) MISSES="${2:-}"; shift 2 ;;
38
46
  --fix-cmd) FIXCMD="${2:-}"; shift 2 ;;
39
47
  --verdict) VERDICT="${2:-}"; shift 2 ;;
48
+ --cmd) RUNCMD="${2:-}"; shift 2 ;;
40
49
  *) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
41
50
  esac
42
51
  done
43
- [[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | sed -n 's/.*"started":"\([^"]*\)".*/\1/p')"
52
+ # the fix's start: its own marker, or the "outer" one when a verify it chained has replaced the marker
53
+ [[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | python3 -c "import json,sys
54
+ try: m=json.load(sys.stdin)
55
+ except Exception: m={}
56
+ o=m.get('outer') or {}
57
+ print(m.get('started','') if m.get('cmd')!='verify-phase' or not o.get('started') else o['started'])" 2>/dev/null)"
44
58
  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'
59
+ TF_MISSES="$MISSES" TF_FIXCMD="$FIXCMD" TF_RUNCMD="$RUNCMD" TF_VERDICT="$VERDICT" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
46
60
  import datetime, json, os, subprocess
47
61
  EMIT = os.environ["TF_EMIT"]; app = os.environ["TF_APP"]; started = os.environ["TF_STARTED"]
62
+ cmd = os.environ.get("TF_RUNCMD") or "fix-issues"
48
63
  now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
49
64
  def q(*a): return subprocess.run(["bash", EMIT, *a], capture_output=True, text=True).stdout.strip()
50
65
  def emit(stream, rec):
@@ -71,45 +86,86 @@ if direct:
71
86
  % (fix_cmd, closed, len(direct)))
72
87
  raise SystemExit(0)
73
88
 
89
+ met = q("--where") or "docs/metrics"
90
+ def jsonl(name):
91
+ try:
92
+ for line in open(os.path.join(met, name), encoding="utf-8", errors="replace"):
93
+ try: yield json.loads(line)
94
+ except Exception: pass
95
+ except OSError:
96
+ return
97
+ # The verdicts: every verify this fix chained, not only the last. docs/.last-verify.json holds one scoped
98
+ # verify, so a fix that verified a Phase 3 scope and then a --phase 1 scope read the Phase 3 rows as absent
99
+ # and wrote Needs re-verify on their misses (TF-052). The gate records each verify wrote since the fix
100
+ # started come first; the ledger fills in only when it belongs to this fix.
101
+ rows = {} # rid -> checklist status
102
+ if started:
103
+ for g in jsonl("gates.jsonl"):
104
+ if g.get("kind") == "gate" and g.get("app") in (None, app) and not g.get("backfilled") and g.get("gate") != "escaped" \
105
+ and (g.get("run_id") or "") >= started and g.get("req_id"):
106
+ rows[g["req_id"].upper()] = g.get("verdict")
74
107
  ledger = {}
75
108
  try:
76
109
  ledger = json.load(open(os.path.join("docs", ".last-verify.json"), encoding="utf-8"))
77
110
  except Exception:
78
111
  pass
79
- rows = ledger.get("rows") or {}
112
+ if not started or not ledger.get("run_id") or ledger["run_id"] >= started:
113
+ row_runs = ledger.get("row_runs") # a ledger kept row by row also holds older runs' rows (Lekhak TF-007)
114
+ for rid, v in (ledger.get("rows") or {}).items():
115
+ if started and isinstance(row_runs, dict) and rid not in (ledger.get("this_run") or []) \
116
+ and (row_runs.get(rid) or "") < started:
117
+ continue
118
+ # verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
119
+ # (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
120
+ rows.setdefault(rid.upper(), "Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
80
121
  reqs = [r.strip().upper() for r in os.environ["TF_REQS"].split(",") if r.strip()] or sorted(rows)
81
122
  subs = [s.strip() for s in os.environ["TF_SUBS"].split(",") if s.strip()]
82
123
  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, []
124
+ # The fix's own time only: a run it chained (the inline verify) has its own record, so the fix is recorded
125
+ # in the gaps around it, the first gap keyed on the fix's start with the rows (its miss-fixes name that start).
126
+ inner, voided = [], set()
127
+ for r in jsonl("runs.jsonl"):
128
+ if r.get("kind") == "run-void":
129
+ voided.add((r.get("cmd"), r.get("started")))
130
+ elif r.get("kind", "run") == "run" and r.get("app") == app and r.get("cmd") not in ("fix-issues", cmd) and not r.get("backfilled") \
131
+ and started and (r.get("started") or "") >= started and (r.get("ended") or "") and r["ended"] <= now:
132
+ inner.append((r["started"], r["ended"], r.get("cmd")))
133
+ inner = sorted(w for w in inner if (w[2], w[0]) not in voided)
134
+ gaps, t = [], started
135
+ for s, e, _c in inner:
136
+ if t and s > t: gaps.append((t, s))
137
+ t = max(t, e) if t else e
138
+ if not started or not gaps or t < now: gaps.append((t, now))
139
+ existing = {r.get("started") for r in jsonl("runs.jsonl") if r.get("kind", "run") == "run" and r.get("cmd") == cmd}
140
+ wrote = had = 0
141
+ for i, (s, e) in enumerate(gaps):
142
+ first = i == 0
143
+ run = {"kind": "run", "app": app, "cmd": cmd, "mode": "fix", "ended": e, "reqs_touched": reqs if first else [],
144
+ "reqs_count": len(reqs) if first else 0, "subagents": subs if first else [],
145
+ "files_written": (int(files) if files.isdigit() else len(reqs)) if first else 0, "build_result": os.environ["TF_BUILD"]}
146
+ if s: run["started"] = s
147
+ # one run record per start: a close called twice must not count the fix twice
148
+ if s and s in existing:
149
+ had += 1; continue
150
+ wrote += emit("runs", run)
151
+ if inner:
152
+ print(f"{cmd}: recorded around {len(inner)} chained run(s) ({', '.join(sorted({c for _s, _e, c in inner}))}) in {len(gaps)} segment(s)")
153
+ closed, none, unverified = 0, [], []
98
154
  for rid in reqs:
155
+ if rid not in rows:
156
+ unverified.append(rid); continue
99
157
  opened = q("--open-miss", rid)
100
158
  if not opened:
101
159
  none.append(rid); continue
102
160
  mid = opened.split()[0]
103
161
  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
162
  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}
163
+ "verdict_after": rows[rid], "reopened": False}
110
164
  if started: rec["fix_run_id"] = started
111
165
  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 ""))
166
+ state = "already there" if had == len(gaps) else "written" if wrote + had == len(gaps) else "not written"
167
+ print(f"{cmd}: run record {state}; {closed} miss-fix record(s) with the verifier's verdict"
168
+ + (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else "")
169
+ + (f"; no verify graded {', '.join(unverified)} during this fix, so its miss stays open with no miss-fix" if unverified else ""))
114
170
  PY
115
171
  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"