@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.
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.opencode/plugin/techieflow.js +179 -7
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +1 -1
- package/.tfcore/tasks/day1-greenfield.md +1 -1
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/telemetry/SCHEMA.md +4 -0
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -2
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +80 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +136 -6
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +92 -35
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +79 -29
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +15 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +60 -2
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +72 -32
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +171 -32
- package/.tfcore/utils/tf-verify-screens.sh +8 -3
- package/.tfcore/utils/tf-verify-tests.sh +215 -14
- package/.tfcore/utils/tf-verify-verdict.py +48 -9
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +4 -0
- package/docs/TechieFlow-Installation.md +5 -3
- package/package.json +1 -1
package/.tfcore/utils/tf-emit.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1058
|
-
#
|
|
1059
|
-
#
|
|
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
|
-
|
|
1117
|
+
_LIVE = None
|
|
1065
1118
|
|
|
1066
1119
|
|
|
1067
|
-
def
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
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
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
return
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
|
|
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":
|
|
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
|
-
|
|
113
|
-
|
|
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
|
package/.tfcore/utils/tf-goal.sh
CHANGED
|
@@ -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 $(
|
|
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
|
|
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 $(
|
|
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 "$(
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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="$(
|
|
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="$(
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|