@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
@@ -117,7 +117,7 @@ else:
117
117
  # TF-019: a row can sit at `Needs re-verify` forever, which pinned the mode and dropped
118
118
  # every not-started row out of the list AND out of the counts, so a build pass could report
119
119
  # a phase finished with rows at 0%. The arithmetic is the check: every row lands in exactly
120
- # one of open / terminal / Blocked.
120
+ # one of open / terminal / Blocked / roadmap.
121
121
  apps = app_names()
122
122
  if not apps:
123
123
  skip("tf-build-list", "no checklist in docs/ yet")
@@ -128,7 +128,7 @@ for app, phase, path in apps:
128
128
  if rc not in (0, 2):
129
129
  broken("tf-build-list", "%s: exited %d" % (label, rc), out.strip().splitlines()[0][:120] if out.strip() else "")
130
130
  continue
131
- m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (\d+) total", out)
131
+ m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (?:(\d+) roadmap, )?(\d+) total", out)
132
132
  if not m:
133
133
  if "has no REQ rows" in out:
134
134
  skip("tf-build-list", "%s: checklist has no requirement rows yet" % label)
@@ -136,11 +136,11 @@ for app, phase, path in apps:
136
136
  broken("tf-build-list", "%s: no counts line to check" % label,
137
137
  "the output shape changed; the arithmetic can no longer be verified")
138
138
  continue
139
- mode, work, term, blocked, total = m.group(1), *(int(x) for x in m.groups()[1:])
140
- if work + term + blocked != total:
139
+ mode, work, term, blocked, roadmap, total = m.group(1), *(int(x or 0) for x in m.groups()[1:])
140
+ if work + term + blocked + roadmap != total:
141
141
  broken("tf-build-list",
142
- "%s: %d rows are in no category (%d open + %d terminal + %d blocked ≠ %d total)"
143
- % (label, total - work - term - blocked, work, term, blocked, total),
142
+ "%s: %d rows are in no category (%d open + %d terminal + %d blocked + %d roadmap ≠ %d total)"
143
+ % (label, total - work - term - blocked - roadmap, work, term, blocked, roadmap, total),
144
144
  "a build pass trusting this list would leave those rows at 0%")
145
145
  else:
146
146
  ok("tf-build-list", "%s: %d rows, all accounted for (%s)" % (label, total, mode))
@@ -288,12 +288,45 @@ for name, cmd in (("tf-doc-check", ["bash", os.path.join(UTILS, "tf-doc-check.sh
288
288
  first = next((l for l in out.splitlines() if l.strip()), "")
289
289
  ok(name, "runs (exit %d)" % rc)
290
290
 
291
+ # ---- 8. the upstream feedback files read as they are ------------------------------------
292
+ # 2026-09-11: PROJECT-STATUS counted every ### heading as an entry and nothing as fixed, so
293
+ # TfLens read "62 open of 62" and its agent told the owner two problems fixed two days earlier
294
+ # were open (MISS-TechieFlow-20260911-04). The one reader is run here, on the real files, and
295
+ # whatever an upstream team has fixed and this project has not re-checked is named below.
296
+ waiting = []
297
+ sys.path.insert(0, UTILS)
298
+ try:
299
+ import tf_feedback
300
+ fb = tf_feedback.files(REPO)
301
+ if not fb:
302
+ skip("tf_feedback", "no upstream feedback files in this project")
303
+ else:
304
+ n = 0
305
+ for f in fb:
306
+ for e in tf_feedback.entries(f):
307
+ n += 1
308
+ if e["state"] == "fixed":
309
+ waiting.append((e["id"], e["since"], os.path.basename(f)))
310
+ ok("tf_feedback", "%d feedback file(s), %d entries read; %d fixed upstream, not yet re-checked"
311
+ % (len(fb), n, len(waiting)))
312
+ except Exception as e:
313
+ broken("tf_feedback", "crashed on this project's feedback files", str(e)[:120])
314
+
291
315
  # ---- output -----------------------------------------------------------------------------
292
316
  proj = os.path.basename(REPO.rstrip(os.sep))
293
317
  print("# tf-selfcheck — %s — the framework's own scripts against this project's real files" % proj)
294
318
  for l in lines:
295
319
  print(l)
296
320
  print()
321
+ if waiting:
322
+ # Not a defect and not a failure: work this project owes. An upstream fix counts only once it
323
+ # has been re-checked here; until then it must never be reported to the owner as open.
324
+ print("Fixed upstream, not yet re-checked here — never report these as open problems:")
325
+ for rid, since, f in waiting:
326
+ print(" %-12s fixed %-10s %s" % (rid, since or "", f))
327
+ print("Re-check each with its entry's \"Verify from here\" step, then close it:")
328
+ print(' bash .tfcore/utils/tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"')
329
+ print()
297
330
  if not findings:
298
331
  print("Every framework script works on this project. Nothing to file.")
299
332
  raise SystemExit(0)
@@ -223,7 +223,17 @@ def run(app, phase, force, add_missing, in_order=False):
223
223
  existing = read(cl_p) if os.path.isfile(cl_p) else ""
224
224
  if existing and not force and not add_missing:
225
225
  die(f"{cl_p} already exists; use --add-missing to append rows for new BRD items, or --force to rewrite", 1)
226
- mapped = set(re.findall(r"\*BRD:\*\s*(BRD-\d+)", existing)) | set(re.findall(r"\((BRD-\d+)\)", existing))
226
+ # A BRD item already has a row when any row names it: on its `*BRD:*` detail line, or anywhere on
227
+ # its status-table line. The table form used to be read only as a bracket holding exactly one id,
228
+ # so "(BRD-76, Phase 3)" and "(BRD-118, BRD-120, Phase 3)" were invisible and --add-missing
229
+ # appended 44 rows to TfLens phase 3 for 13 new items, 31 of them copies of Verified rows (TF-025).
230
+ mapped = {b for line in re.findall(r"(?m)^.*\*BRD:\*.*$", existing) for b in re.findall(r"BRD-\d+", line)}
231
+ mapped |= {b for row in re.findall(r"(?m)^\|\s*REQ-.*$", existing) for b in re.findall(r"BRD-\d+", row)}
232
+ # ...and on the requirement's own line in its detail entry, with the lines indented under it. A
233
+ # checklist migrated from an older plan names its items there, as "(BRD-1, BRD-2)" or
234
+ # "*(BRD-84, BRD-85)*", with no *BRD:* line: AppManager's run appended a row for all 88 items (TF-001).
235
+ for block in re.findall(r"(?m)^\s*[-*]\s*(?:<a id=[^>]*>\s*</a>\s*)?\*\*REQ-[A-Z]+-\d{3}\*\*.*(?:\n[ \t]+\S.*)*", existing):
236
+ mapped |= set(re.findall(r"BRD-\d+", block))
227
237
  counters = other_phase_counters(app, cl_p, below=phase if in_order else None)
228
238
  if add_missing:
229
239
  for m in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", existing):
@@ -12,6 +12,7 @@
12
12
  # 4. the build verdict from tf-build.sh (skipped with --no-build)
13
13
  # Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
14
14
  set -uo pipefail
15
+ source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # GNU-only commands, for a stock Mac
15
16
  APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
16
17
  shift
17
18
  BUILD=1; PHASE=""
@@ -22,7 +23,7 @@ while [[ $# -gt 0 ]]; do
22
23
  *) shift ;;
23
24
  esac
24
25
  done
25
- [[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:\s*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
26
+ [[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:[[:space:]]*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
26
27
  [[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
27
28
  ST="PROJECT-STATUS.md"
28
29
 
@@ -31,19 +32,20 @@ echo
31
32
  echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
32
33
  if [[ -f "$ST" ]]; then
33
34
  grep -E '^(last_updated|current_phase|last_verified_build|last_verified_date):' "$ST" | sed 's/^/- /'
34
- awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '^\s*/' | sed 's/^\s*/- next: /' | head -2
35
- echo "- written: $(date -u -r "$ST" +%Y-%m-%dT%H:%M:%SZ)"
35
+ awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '^[[:space:]]*/' | sed 's/^[[:space:]]*/- next: /' | head -2
36
+ echo "- written: $(tf_date_from -u "@$(tf_stat_mtime "$ST")" +%Y-%m-%dT%H:%M:%SZ)"
36
37
  else
37
38
  echo "- no PROJECT-STATUS.md; treat as never written"
38
39
  fi
39
40
  echo
40
41
  echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
41
42
  if [[ -f "$ST" ]]; then
42
- mapfile -t NEWER < <(find src tests -type f -newer "$ST" \
43
- -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null \
44
- | xargs -r ls -t 2>/dev/null)
43
+ # xargs runs ls only when find listed something (GNU xargs -r; BSD xargs has no -r)
44
+ NEWER=(); _found="$(find src tests -type f -newer "$ST" \
45
+ -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null)"
46
+ [[ -n "$_found" ]] && tf_read_lines NEWER < <(printf '%s\n' "$_found" | xargs ls -t 2>/dev/null)
45
47
  echo "- ${#NEWER[@]} file(s)"
46
- printf -- '- %s\n' "${NEWER[@]:0:30}"
48
+ printf -- '- %s\n' ${NEWER[@]+"${NEWER[@]:0:30}"}
47
49
  [[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
48
50
  else
49
51
  echo "- no status file to compare against"
@@ -51,9 +53,9 @@ fi
51
53
  echo
52
54
  echo "## 3. Checklist rows that look interrupted ($CL)"
53
55
  if [[ -f "$CL" ]]; then
54
- grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
56
+ grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
55
57
  | grep -iE ' — (Implemented|In Progress|Needs re-verify|PARTIAL|FAIL)' | sed 's/^/- /' | head -40
56
- n=$(grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
58
+ n=$(grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
57
59
  [[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
58
60
  else
59
61
  echo "- $CL does not exist"
@@ -27,6 +27,7 @@ import re
27
27
  import sys
28
28
 
29
29
  TERMINAL = {"verified", "done (pre-existing)", "n/a"}
30
+ NA = {"n/a"} # terminal, but never counted as verified (TF-018)
30
31
  PASS_THROUGH = {"blocked"} # a library gap; counts as closed for the ladder
31
32
  BUILT = {"implemented", "needs re-verify"}
32
33
  OWNER = {"owner-uat"} # only the owner can close it, from the UsageGuide test plan
@@ -61,6 +62,9 @@ def read(path):
61
62
 
62
63
 
63
64
  def checklist_rows(text):
65
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
66
+ import tf_roadmap
67
+ marked = tf_roadmap.ids(text)
64
68
  rows = []
65
69
  for line in text.splitlines():
66
70
  if not re.match(r"^\s*\|\s*`?REQ-", line):
@@ -69,10 +73,34 @@ def checklist_rows(text):
69
73
  if len(cells) < 3:
70
74
  continue
71
75
  rid = cells[0].strip("`* ")
72
- rows.append({"id": rid, "name": cells[1].strip("`* "), "status": norm_status(cells[2])})
76
+ status = norm_status(cells[2])
77
+ rows.append({"id": rid, "name": cells[1].strip("`* "), "status": status,
78
+ "remarks": cells[4] if len(cells) > 4 else "",
79
+ "roadmap": rid.upper() in marked and status.lower() not in TERMINAL})
73
80
  return rows
74
81
 
75
82
 
83
+ def figures(rows):
84
+ """(verified, counted, not applicable). An N/A row is terminal but was never verified, so it is
85
+ left out of both numbers and named: AppManager read "89 of 89 verified" with REQ-NFR-008 at N/A
86
+ (TF-018). tests/regression/run.sh am_018."""
87
+ na = sum(1 for r in rows if r["status"].lower() in NA)
88
+ return sum(1 for r in rows if r["status"].lower() in TERMINAL - NA), len(rows) - na, na
89
+
90
+
91
+ # The defect marks live in tf_defect.py. A bare ⚠ is not enough: sending "not verifiable here" to a
92
+ # build is the TfLens loop again.
93
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
94
+ from tf_defect import DEFECT # noqa: E402
95
+
96
+
97
+ def has_defect(r):
98
+ """A Needs re-verify row whose Remarks carry a framework defect mark holds a defect no fix has
99
+ cleared, and a verify of the acceptance line cannot see it. So it is fix work, as build-phase step 7
100
+ and tf-build-list say, never a verify (AppManager TF-019). tests/regression/run.sh am_019."""
101
+ return r["status"].lower() == "needs re-verify" and bool(DEFECT.search(r.get("remarks", "")))
102
+
103
+
76
104
  def ladder_rank(status):
77
105
  return LADDER.index(status) if status in LADDER else 0
78
106
 
@@ -147,19 +175,13 @@ def handoff_ran(log_rows):
147
175
 
148
176
 
149
177
  def feedback_lines(root, app):
150
- out = []
151
- for path in sorted(glob.glob(os.path.join(root, "docs", f"{app}-*-Feedback.md"))):
152
- lib = re.sub(rf"^{re.escape(app)}-(.+)-Feedback\.md$", r"\1", os.path.basename(path))
153
- text = read(path)
154
- entries = re.split(r"(?m)^###\s+", text)[1:]
155
- open_n = 0
156
- for e in entries:
157
- closed = re.search(r"(?im)^\s*[-*]\s*\*\*(status|resolved|fixed)[^:]*:\*\*\s*(fixed|resolved|closed|done)", e)
158
- if not closed:
159
- open_n += 1
160
- rel = os.path.relpath(path, root).replace(os.sep, "/")
161
- out.append(f"- {lib}: {open_n} open of {len(entries)} — {rel}")
162
- return out or ["- None"]
178
+ # One reader for every script (tf_feedback.py). This function used to count every `###`
179
+ # heading as an entry and accept only a closing line no file used, so TfLens's status read
180
+ # "TechieFlow: 62 open of 62" for 27 entries, 19 of them fixed or closed, and the agent told
181
+ # the owner two fixed problems were open (MISS-TechieFlow-20260911-04).
182
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
183
+ import tf_feedback
184
+ return [tf_feedback.summary_line(p, app) for p in tf_feedback.files(root, app)] or ["- None"]
163
185
 
164
186
 
165
187
  def config_phase(root):
@@ -191,6 +213,15 @@ def phase_row(root, app, phase):
191
213
  return name, total
192
214
 
193
215
 
216
+ def usage_guide(app):
217
+ """UsageGuide or Usage-Guide, whichever exists: tf-verify-list.py's reader (AppManager TF-022)."""
218
+ import importlib.util
219
+ spec = importlib.util.spec_from_file_location("tf_verify_list", os.path.join(os.path.dirname(os.path.abspath(__file__)), "tf-verify-list.py"))
220
+ vl = importlib.util.module_from_spec(spec)
221
+ spec.loader.exec_module(vl)
222
+ return vl.usage_guide(app)
223
+
224
+
194
225
  def next_command(app, rows, log_rows, phase=1, verdicts=None):
195
226
  """Returns (phase, qualifier, cc_line, oc_line, reason)."""
196
227
  verdicts = verdicts or {}
@@ -207,17 +238,23 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
207
238
  f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
208
239
  "no BRD and no checklist exist; day-1 stage 1 has not run")
209
240
 
210
- unbuilt = [r for r in rows if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
211
- built = [r for r in rows if r["status"].lower() in BUILT]
212
- owner = [r for r in rows if r["status"].lower() in OWNER]
213
- total = len(rows)
214
- closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
215
- q = f"{closed} of {total} verified"
216
-
217
- if unbuilt:
218
- return ("Build", f"{len(unbuilt)} not built, {q}",
219
- f"{CC}flow-master *build-phase {app}", f"{OC}flow-master *build-phase {app}",
220
- f"{len(unbuilt)} rows are not built yet: {ids(unbuilt)}")
241
+ # A roadmap row is not this phase's work, so it never decides the next command (TF-017).
242
+ now = [r for r in rows if not r.get("roadmap")]
243
+ unbuilt = [r for r in now if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
244
+ defects = [r for r in now if has_defect(r)]
245
+ built = [r for r in now if r["status"].lower() in BUILT and r not in defects]
246
+ owner = [r for r in now if r["status"].lower() in OWNER]
247
+ verified, counted, na = figures(rows)
248
+ q = f"{verified} of {counted} verified" + (f", {na} not applicable" if na else "")
249
+
250
+ if unbuilt or defects:
251
+ qual = ", ".join(x for x in (f"{len(unbuilt)} not built" if unbuilt else "",
252
+ f"{len(defects)} to fix" if defects else "") if x)
253
+ why = "; ".join(x for x in (f"{len(unbuilt)} rows are not built yet: {ids(unbuilt)}" if unbuilt else "",
254
+ f"{len(defects)} rows carry a defect (⚠ in Remarks) that a fix must clear before "
255
+ f"a verify: {ids(defects)}" if defects else "") if x)
256
+ return ("Build", f"{qual}, {q}",
257
+ f"{CC}flow-master *build-phase {app}", f"{OC}flow-master *build-phase {app}", why)
221
258
  if built:
222
259
  # A row the last verify could not MEASURE is not a row waiting for another verify.
223
260
  # Its tests declined — the state they need does not exist in this environment — so
@@ -243,7 +280,7 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
243
280
  reason)
244
281
  if owner:
245
282
  if handoff_ran(log_rows):
246
- line = f"(owner-run) docs/{app}-UsageGuide.md — {ids(owner)}"
283
+ line = f"(owner-run) {usage_guide(app)} — {ids(owner)}"
247
284
  return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
248
285
  f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
249
286
  return ("Handoff", f"{len(owner)} owner-run, {q}",
@@ -252,10 +289,10 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
252
289
  if handoff_ran(log_rows):
253
290
  line = "(owner) set current_phase to Released after UAT — no agent command"
254
291
  return ("UAT", f"handoff done, {q}", line, line,
255
- "every row is terminal and handoff has run; waiting on the owner")
292
+ "every row in this phase's scope is terminal and handoff has run; waiting on the owner")
256
293
  return ("Handoff", q,
257
294
  f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
258
- "every row is terminal and handoff has not run yet")
295
+ "every row in this phase's scope is terminal and handoff has not run yet")
259
296
 
260
297
 
261
298
  def main(argv):
@@ -284,14 +321,17 @@ def main(argv):
284
321
  log_rows = existing_log_rows(st_text)
285
322
  vdate, vresult = last_verify(root, app)
286
323
  phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n, ledger_verdicts(root))
324
+ roadmap = [r["id"] for r in rows if r["roadmap"]]
325
+ if roadmap:
326
+ reason += (f"; {len(roadmap)} roadmap row(s) are not in this phase's scope and are left for the owner "
327
+ f"(*amend-docs): {', '.join(roadmap)}")
287
328
  pname, ptotal = phase_row(root, app, phase_n)
288
329
  if ptotal or phase_n > 1:
289
330
  tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
290
331
  phase = f"{tag} · {phase}"
291
332
  reason = f"{reason}; working {cl_rel}"
292
333
 
293
- total = len(rows)
294
- closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
334
+ verified, counted, na = figures(rows)
295
335
  counts = {}
296
336
  for r in rows:
297
337
  counts[r["status"]] = counts.get(r["status"], 0) + 1
@@ -322,7 +362,7 @@ def main(argv):
322
362
  print()
323
363
  if open_rows:
324
364
  for r in open_rows[:10]:
325
- print(f"- [ ] {r['id']} — {r['name']} ({r['status']})")
365
+ print(f"- [ ] {r['id']} — {r['name']} ({r['status']}{', roadmap — not in this phase' if r['roadmap'] else ''})")
326
366
  if len(open_rows) > 10:
327
367
  print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
328
368
  else:
@@ -332,7 +372,7 @@ def main(argv):
332
372
  print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
333
373
  print()
334
374
  print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
335
- new_row = f"| {today} | {label} | {closed}/{total} Verified | {cl_rel}#requirements-status |"
375
+ new_row = f"| {today} | {label} | {verified}/{counted} Verified{f', {na} N/A' if na else ''} | {cl_rel}#requirements-status |"
336
376
  for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
337
377
  print(r)
338
378
  print(new_row)
@@ -8,6 +8,9 @@
8
8
  bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
9
9
  bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--verify-ran] [--cmd triage-issues|fix-issues]
10
10
 
11
+ demote, new and note take --phase N for another phase's checklist; without it demote and note find the
12
+ row in whichever docs/<App>-Checklist.md or docs/<App>-P<N>-Checklist.md holds it.
13
+
11
14
  demote the row goes to Needs re-verify (% capped at 75) with a dated Remark holding the symptom in the
12
15
  reporter's words, the evidence path and the kind.
13
16
  new a defect no row covers: the next free id in the prefix, a Not Started row, a detail entry with the
@@ -20,7 +23,8 @@ close what triage owes the telemetry, from the actions above (tests/.artifacts
20
23
  spec for a new row, which the spec never had) (duplicates collapsed through
21
24
  tf-emit.sh --open-miss; the origin run looked up with --origin-of), then the run record. It also
22
25
  lists every file under src/, source/ or tests/ changed since the start: a triage that edited code
23
- is reported and logged as an instruction-ignored miss (FR-28).
26
+ is reported and logged as an instruction-ignored miss (FR-28). Only actions logged since the start
27
+ count, and a close empties the list (TrBlazeUI TF-001).
24
28
  Exit 0 · 2 could not run.
25
29
  """
26
30
  import datetime
@@ -58,12 +62,36 @@ def cfg_phase():
58
62
  return 1
59
63
 
60
64
 
61
- def checklist(app):
62
- ph = cfg_phase()
63
- p = os.path.join("docs", f"{app}-Checklist.md" if ph <= 1 else f"{app}-P{ph}-Checklist.md")
64
- if not os.path.isfile(p):
65
- die(f"{p} does not exist")
66
- return p
65
+ def phase_checklist(app, ph):
66
+ return os.path.join("docs", f"{app}-Checklist.md" if ph <= 1 else f"{app}-P{ph}-Checklist.md")
67
+
68
+
69
+ def checklist(app, argv, ce, rid=None):
70
+ """--phase N names the checklist, as tf-verify-list.py takes it. Without it: the appPhase
71
+ checklist, or, for a row that is not there, the one earlier-phase checklist that holds it —
72
+ a Phase 3 verify finds Phase 1 rows failing, and demote could not reach them (TF-051)."""
73
+ if "--phase" in argv:
74
+ v = opt(argv, "--phase", "")
75
+ if not v.isdigit():
76
+ die("--phase needs a number")
77
+ p = phase_checklist(app, int(v))
78
+ if not os.path.isfile(p):
79
+ die(f"{p} does not exist")
80
+ return p
81
+ p = phase_checklist(app, cfg_phase())
82
+ if rid is None or (os.path.isfile(p) and rid in ce.rows(ce.read(p))):
83
+ if not os.path.isfile(p):
84
+ die(f"{p} does not exist")
85
+ return p
86
+ import glob
87
+ every = sorted(set(glob.glob(os.path.join("docs", f"{app}-Checklist.md")) + glob.glob(os.path.join("docs", f"{app}-P*-Checklist.md"))))
88
+ hits = [c for c in every if rid in ce.rows(ce.read(c))]
89
+ if len(hits) == 1:
90
+ print(f"note: {rid} is a row of {hits[0]}, not of {p}; written there")
91
+ return hits[0]
92
+ if len(hits) > 1:
93
+ die(f"{rid} is a row of {', '.join(hits)}; name one with --phase N")
94
+ die(f"{rid} is not a row of {', '.join(every) or p}")
67
95
 
68
96
 
69
97
  def log_load():
@@ -112,7 +140,6 @@ def main(argv):
112
140
  return 0 if len(argv) > 1 else 2
113
141
  app, verb = argv[1], argv[2]
114
142
  ce = mod()
115
- cl = checklist(app)
116
143
  log = log_load()
117
144
  log.setdefault("app", app)
118
145
  now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -121,6 +148,7 @@ def main(argv):
121
148
  if len(argv) < 5:
122
149
  die("demote needs <REQ> \"<symptom>\"")
123
150
  rid, symptom = argv[3].upper(), argv[4]
151
+ cl = checklist(app, argv, ce, rid)
124
152
  kind = opt(argv, "--kind"); ev = opt(argv, "--evidence"); src = opt(argv, "--source", "owner")
125
153
  prior = ce.demote(cl, rid, symptom, kind=kind, evidence=ev, source=src, prefix="⚠ prod bug" if src == "production" else "⚠ UAT bug")
126
154
  if prior is None:
@@ -136,6 +164,7 @@ def main(argv):
136
164
  if len(argv) < 5:
137
165
  die("new needs \"<title>\" \"<acceptance>\"")
138
166
  title, acc = argv[3], argv[4]
167
+ cl = checklist(app, argv, ce)
139
168
  prefix = opt(argv, "--prefix", "FN").upper(); section = opt(argv, "--section"); ev = opt(argv, "--evidence")
140
169
  src = opt(argv, "--source", "owner"); mockup = opt(argv, "--mockup")
141
170
  if prefix == "UI" and not mockup:
@@ -152,6 +181,7 @@ def main(argv):
152
181
  if len(argv) < 5:
153
182
  die("note needs <REQ> \"<text>\"")
154
183
  rid = argv[3].upper()
184
+ cl = checklist(app, argv, ce, rid)
155
185
  if ce.note(cl, rid, argv[4]) is None:
156
186
  die(f"{rid} is not a row of {cl}")
157
187
  log["actions"].append({"verb": "note", "req_id": rid, "symptom": argv[4], "ts": now})
@@ -162,9 +192,11 @@ def main(argv):
162
192
  if verb == "close":
163
193
  started = opt(argv, "--started") or subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
164
194
  if started.startswith("{"):
165
- import re
166
- m = re.search(r'"started":"([^"]*)"', started)
167
- started = m.group(1) if m else ""
195
+ # the marker's own started, never its "outer" block's (TF-052 follow-up)
196
+ try:
197
+ started = (json.loads(started) or {}).get("started") or ""
198
+ except ValueError:
199
+ started = ""
168
200
  started = started.strip() if started and not started.startswith("none") else ""
169
201
  cmd = opt(argv, "--cmd", "triage-issues")
170
202
  # code untouched?
@@ -173,16 +205,25 @@ def main(argv):
173
205
  t0 = datetime.datetime.strptime(started, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc).timestamp()
174
206
  for top in ("src", "source", "tests"):
175
207
  for d, dirs, files in os.walk(top):
176
- dirs[:] = [x for x in dirs if x not in ("bin", "obj", "node_modules", ".artifacts", "TestResults")]
208
+ # a running app's own log files are not code (AppManager TF-026)
209
+ dirs[:] = [x for x in dirs if x not in ("bin", "obj", "node_modules", ".artifacts", "TestResults")
210
+ and x.lower() not in ("logs", "log")]
177
211
  for f in files:
212
+ if f.lower().endswith(".log"):
213
+ continue
178
214
  p = os.path.join(d, f)
179
215
  try:
180
216
  if os.path.getmtime(p) > t0:
181
217
  changed.append(p.replace("\\", "/"))
182
218
  except OSError:
183
219
  pass
184
- acts = [a for a in log.get("actions", []) if a["verb"] in ("demote", "new")]
220
+ # only this triage's actions: an earlier run's leftovers were logged again under today's run (TrBlazeUI TF-001)
221
+ stale = [a for a in log.get("actions", []) if started and (a.get("ts") or "") < started]
222
+ if stale:
223
+ print(f" skipped {len(stale)} action(s) from before {started} — an earlier triage's, already closed or abandoned")
224
+ acts = [a for a in log.get("actions", []) if a["verb"] in ("demote", "new") and a not in stale]
185
225
  gates = misses = 0
226
+ failed = []
186
227
  for a in acts:
187
228
  rid = a["req_id"]; cls_req = rid.split("-")[1]
188
229
  att = q("--next-attempt", rid) or "1"
@@ -192,7 +233,9 @@ def main(argv):
192
233
  "gate": "escaped", "gates_run": [], "failure_class": fc, "prior_verdict": a.get("prior")}
193
234
  if emit("gates", rec):
194
235
  gates += 1
195
- mc = ("unspecified-gap" if a["verb"] == "new" else "regression" if (a.get("prior") or "").lower().startswith("verified") else "wrong-behaviour")
236
+ else:
237
+ failed.append(a)
238
+ mc =("unspecified-gap" if a["verb"] == "new" else "regression" if (a.get("prior") or "").lower().startswith("verified") else "wrong-behaviour")
196
239
  opened = q("--open-miss", rid)
197
240
  if opened and opened.split()[-1] == mc:
198
241
  print(f" {rid}: already open as {opened.split()[0]} ({mc}); no second miss")
@@ -210,9 +253,11 @@ def main(argv):
210
253
  if emit("misses", m):
211
254
  misses += 1
212
255
  a["miss_id"] = mid
213
- if changed and cmd == "triage-issues" and not log.get("code_miss_id"):
256
+ elif a not in failed:
257
+ failed.append(a)
258
+ if changed and cmd == "triage-issues" and log.get("code_miss_run") != started:
214
259
  mid = q("--next-miss-id")
215
- log["code_miss_id"] = mid
260
+ log["code_miss_id"] = mid; log["code_miss_run"] = started
216
261
  emit("misses", {"kind": "miss", "miss_id": mid, "req_id": None, "req_class": None, "miss_class": "other", "artifact": "src",
217
262
  "severity": "major", "why_missed": "instruction-ignored", "origin_phase": "triage-issues", "origin_agent": "flow-master",
218
263
  "found_by": "gate", "found_phase": "triage-issues", "found_gate": None, "found_run_id": started, "failure_class": "other",
@@ -232,6 +277,7 @@ def main(argv):
232
277
  else:
233
278
  ok = None
234
279
  log["closed"] = now
280
+ log["actions"] = failed # a closed action is never closed again; one whose record was refused waits for the next close
235
281
  log_save(log)
236
282
  print(f"triage: {len(acts)} row(s) logged — {gates} gate record(s) (escaped), {misses} miss(es)"
237
283
  + (f", run record {'written' if ok else 'not written'}" if ok is not None else "") + f"; code untouched: {'yes' if not changed else 'NO'}")
@@ -6,6 +6,8 @@
6
6
  # bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
7
7
  # bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--cmd triage-issues|fix-issues]
8
8
  #
9
+ # demote, new and note take --phase N; without it a row of an earlier phase's checklist is found (TF-051).
10
+ #
9
11
  # demote: Needs re-verify with a dated Remark in the reporter's words. new: a Not Started row with its
10
12
  # acceptance line and BRD-pending. note: a remark, no status change. close: the escaped gate records,
11
13
  # one miss per row (found by the owner, the symptom as `what`, duplicates collapsed), the run record,