@techierathore/techieflow 1.0.3 → 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 (150) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -10
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  8. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  9. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  12. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  13. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  14. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
  15. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  16. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  17. package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
  18. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
  19. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  20. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  21. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  22. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  23. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  24. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  25. package/.opencode/plugin/techieflow.js +181 -9
  26. package/.tfcore/agents/analyst.md +5 -8
  27. package/.tfcore/agents/architect.md +1 -10
  28. package/.tfcore/agents/flow-master.md +1 -0
  29. package/.tfcore/agents/verifier.md +1 -0
  30. package/.tfcore/hooks/block-git.sh +4 -2
  31. package/.tfcore/hooks/guard-build.sh +5 -1
  32. package/.tfcore/hooks/guard-db.sh +6 -4
  33. package/.tfcore/hooks/guard-metrics.sh +13 -1
  34. package/.tfcore/hooks/guard-status-html.sh +86 -2
  35. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  36. package/.tfcore/hooks/guard-verify.sh +8 -3
  37. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  38. package/.tfcore/routing.yaml +61 -0
  39. package/.tfcore/standards/owner-words.txt +23 -0
  40. package/.tfcore/tasks/_owner-language.md +27 -0
  41. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  42. package/.tfcore/tasks/_status-update-gate.md +4 -3
  43. package/.tfcore/tasks/amend-docs.md +6 -5
  44. package/.tfcore/tasks/build-phase.md +1 -1
  45. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  46. package/.tfcore/tasks/create-doc.md +4 -0
  47. package/.tfcore/tasks/create-project-brief.md +22 -0
  48. package/.tfcore/tasks/day1-brownfield.md +3 -3
  49. package/.tfcore/tasks/day1-greenfield.md +3 -3
  50. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  51. package/.tfcore/tasks/fix-issues.md +2 -2
  52. package/.tfcore/tasks/generate-html.md +7 -3
  53. package/.tfcore/tasks/handoff-phase.md +20 -18
  54. package/.tfcore/tasks/metrics-report.md +2 -0
  55. package/.tfcore/tasks/mockups.md +1 -1
  56. package/.tfcore/tasks/productguide.md +2 -2
  57. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  58. package/.tfcore/tasks/triage-issues.md +2 -2
  59. package/.tfcore/tasks/verify-phase.md +1 -1
  60. package/.tfcore/telemetry/SCHEMA.md +61 -1
  61. package/.tfcore/telemetry/install-metrics.sh +3 -2
  62. package/.tfcore/telemetry/tf-metrics.sh +435 -21
  63. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  64. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  65. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  66. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  67. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  68. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  69. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  70. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  71. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  72. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  73. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  74. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  75. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  76. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  77. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  78. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  79. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  80. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  81. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  82. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  83. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  84. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  90. package/.tfcore/utils/tf-assets.sh +97 -12
  91. package/.tfcore/utils/tf-brd-status.py +14 -5
  92. package/.tfcore/utils/tf-build-list.py +99 -11
  93. package/.tfcore/utils/tf-build-list.sh +5 -3
  94. package/.tfcore/utils/tf-build.sh +88 -15
  95. package/.tfcore/utils/tf-ci-repro.py +685 -0
  96. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  97. package/.tfcore/utils/tf-devguide-list.py +47 -22
  98. package/.tfcore/utils/tf-doc-check.py +242 -10
  99. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  100. package/.tfcore/utils/tf-emit.sh +413 -19
  101. package/.tfcore/utils/tf-feedback.sh +13 -0
  102. package/.tfcore/utils/tf-fix-close.sh +113 -31
  103. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  104. package/.tfcore/utils/tf-goal.sh +105 -18
  105. package/.tfcore/utils/tf-harness.sh +9 -4
  106. package/.tfcore/utils/tf-lock.sh +29 -0
  107. package/.tfcore/utils/tf-log-miss.py +34 -8
  108. package/.tfcore/utils/tf-login.mjs +167 -0
  109. package/.tfcore/utils/tf-misses-md.py +24 -21
  110. package/.tfcore/utils/tf-misses-md.sh +5 -4
  111. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  112. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  113. package/.tfcore/utils/tf-model-pick.sh +475 -0
  114. package/.tfcore/utils/tf-owner-text.py +233 -0
  115. package/.tfcore/utils/tf-owner-text.sh +14 -0
  116. package/.tfcore/utils/tf-phase.sh +33 -1
  117. package/.tfcore/utils/tf-portable.sh +230 -0
  118. package/.tfcore/utils/tf-render-html.py +80 -2
  119. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  120. package/.tfcore/utils/tf-routing.sh +7 -0
  121. package/.tfcore/utils/tf-selfcheck.sh +356 -0
  122. package/.tfcore/utils/tf-split-brd.py +17 -2
  123. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  124. package/.tfcore/utils/tf-status-facts.py +133 -44
  125. package/.tfcore/utils/tf-triage.py +62 -16
  126. package/.tfcore/utils/tf-triage.sh +2 -0
  127. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  128. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  129. package/.tfcore/utils/tf-verify-env.sh +33 -4
  130. package/.tfcore/utils/tf-verify-list.py +75 -8
  131. package/.tfcore/utils/tf-verify-screens.mjs +254 -43
  132. package/.tfcore/utils/tf-verify-screens.sh +18 -4
  133. package/.tfcore/utils/tf-verify-tests.sh +272 -28
  134. package/.tfcore/utils/tf-verify-verdict.py +59 -11
  135. package/.tfcore/utils/tf_defect.py +27 -0
  136. package/.tfcore/utils/tf_feedback.py +209 -0
  137. package/.tfcore/utils/tf_roadmap.py +35 -0
  138. package/README.md +19 -5
  139. package/docs/TechieFlow-Installation.md +5 -3
  140. package/opencode.jsonc +24 -0
  141. package/package.json +1 -1
  142. package/scripts/install.mjs +4 -0
  143. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  144. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  145. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  146. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  147. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  148. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  149. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  150. package/.tfcore/utils/techieflow-doc-template.md +0 -325
@@ -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,22 +73,76 @@ 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
 
79
107
 
108
+ def ledger(root):
109
+ """docs/.last-verify.json as a dict, or {} when there is none.
110
+
111
+ The verifier writes it with `json.dump(..., indent=1)`, so it is ONE pretty-printed
112
+ object spanning many lines. This reader used to take `splitlines()[-1]` — written for
113
+ an append-per-line ledger that no version of the verifier has ever produced — so it
114
+ parsed the closing brace, threw, and every project reported `last_verified_build:
115
+ not-run` however many verify runs it had behind it (TfLens: 69 of 73 rows Verified and
116
+ a full ledger, reported as never verified). Whole file first, last line second for a
117
+ legacy JSONL ledger. tests/regression/run.sh tf_ledger."""
118
+ path = os.path.join(root, "docs", ".last-verify.json")
119
+ if not os.path.isfile(path):
120
+ return {}
121
+ text = read(path).strip()
122
+ for candidate in (text, text.splitlines()[-1] if text else ""):
123
+ try:
124
+ data = json.loads(candidate)
125
+ if isinstance(data, dict):
126
+ return data
127
+ except Exception:
128
+ continue
129
+ return {}
130
+
131
+
132
+ def ledger_verdicts(root):
133
+ """{REQ id: verdict} from the last verify run, or {} when none has run.
134
+
135
+ The verifier already writes every row's verdict into the ledger. Reading it here is
136
+ what lets the status line tell "built, waiting for a verify" apart from "verified as
137
+ far as it can be, and the rest needs a decision only the owner can make"."""
138
+ rows = ledger(root).get("rows")
139
+ return rows if isinstance(rows, dict) else {}
140
+
141
+
80
142
  def last_verify(root, app):
81
143
  """(date, build_result) from docs/.last-verify.json and gates.jsonl; ('', 'not-run') if none."""
82
- ledger = os.path.join(root, "docs", ".last-verify.json")
83
- if not os.path.isfile(ledger):
84
- return "", "not-run"
85
- try:
86
- data = json.loads(read(ledger).strip().splitlines()[-1])
87
- except Exception:
144
+ data = ledger(root)
145
+ if not data:
88
146
  return "", "not-run"
89
147
  date = str(data.get("date", ""))[:10]
90
148
  result = "PASS"
@@ -117,19 +175,13 @@ def handoff_ran(log_rows):
117
175
 
118
176
 
119
177
  def feedback_lines(root, app):
120
- out = []
121
- for path in sorted(glob.glob(os.path.join(root, "docs", f"{app}-*-Feedback.md"))):
122
- lib = re.sub(rf"^{re.escape(app)}-(.+)-Feedback\.md$", r"\1", os.path.basename(path))
123
- text = read(path)
124
- entries = re.split(r"(?m)^###\s+", text)[1:]
125
- open_n = 0
126
- for e in entries:
127
- closed = re.search(r"(?im)^\s*[-*]\s*\*\*(status|resolved|fixed)[^:]*:\*\*\s*(fixed|resolved|closed|done)", e)
128
- if not closed:
129
- open_n += 1
130
- rel = os.path.relpath(path, root).replace(os.sep, "/")
131
- out.append(f"- {lib}: {open_n} open of {len(entries)} — {rel}")
132
- 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"]
133
185
 
134
186
 
135
187
  def config_phase(root):
@@ -161,8 +213,19 @@ def phase_row(root, app, phase):
161
213
  return name, total
162
214
 
163
215
 
164
- def next_command(app, rows, log_rows, phase=1):
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
+
225
+ def next_command(app, rows, log_rows, phase=1, verdicts=None):
165
226
  """Returns (phase, qualifier, cc_line, oc_line, reason)."""
227
+ verdicts = verdicts or {}
228
+
166
229
  def ids(lst):
167
230
  return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
168
231
 
@@ -175,26 +238,49 @@ def next_command(app, rows, log_rows, phase=1):
175
238
  f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
176
239
  "no BRD and no checklist exist; day-1 stage 1 has not run")
177
240
 
178
- unbuilt = [r for r in rows if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
179
- built = [r for r in rows if r["status"].lower() in BUILT]
180
- owner = [r for r in rows if r["status"].lower() in OWNER]
181
- total = len(rows)
182
- closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
183
- q = f"{closed} of {total} verified"
184
-
185
- if unbuilt:
186
- return ("Build", f"{len(unbuilt)} not built, {q}",
187
- f"{CC}flow-master *build-phase {app}", f"{OC}flow-master *build-phase {app}",
188
- 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)
189
258
  if built:
190
- ui = [r for r in built if r["id"].startswith("REQ-UI")]
191
- scope = "ui" if len(ui) == len(built) else ("functional" if not ui else "all")
192
- return ("Verify", f"{len(built)} to verify, {q}",
259
+ # A row the last verify could not MEASURE is not a row waiting for another verify.
260
+ # Its tests declined — the state they need does not exist in this environment — so
261
+ # running the verifier again produces the same NOT-TESTED and the project sits in a
262
+ # loop that looks like progress. Say what it actually needs: data, a changed
263
+ # acceptance line, or the row marked N/A. Only the owner can choose. FR-69's sibling
264
+ # case; the verdicts come from the ledger the verifier already writes.
265
+ unmeasured = [r for r in built if verdicts.get(r["id"]) == "NOT-TESTED"]
266
+ waiting = [r for r in built if r not in unmeasured]
267
+ if unmeasured and not waiting:
268
+ line = (f"(owner) {len(unmeasured)} row(s) cannot be measured here — create the data, "
269
+ f"change the acceptance line, or mark the row N/A: {ids(unmeasured)}")
270
+ return ("Verify", f"{len(unmeasured)} not measurable, {q}", line, line,
271
+ f"every test carrying these ids was skipped, so another verify run changes nothing: {ids(unmeasured)}")
272
+ ui = [r for r in waiting if r["id"].startswith("REQ-UI")]
273
+ scope = "ui" if len(ui) == len(waiting) else ("functional" if not ui else "all")
274
+ reason = f"{len(waiting)} rows are built and not verified: {ids(waiting)}"
275
+ if unmeasured:
276
+ reason += (f"; {len(unmeasured)} more could not be measured (every test skipped) "
277
+ f"and need the owner: {ids(unmeasured)}")
278
+ return ("Verify", f"{len(waiting)} to verify, {q}",
193
279
  f"{CC}verifier *verify {scope} {app}", f"{OC}flow-verifier *verify {scope} {app}",
194
- f"{len(built)} rows are built and not verified: {ids(built)}")
280
+ reason)
195
281
  if owner:
196
282
  if handoff_ran(log_rows):
197
- line = f"(owner-run) docs/{app}-UsageGuide.md — {ids(owner)}"
283
+ line = f"(owner-run) {usage_guide(app)} — {ids(owner)}"
198
284
  return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
199
285
  f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
200
286
  return ("Handoff", f"{len(owner)} owner-run, {q}",
@@ -203,10 +289,10 @@ def next_command(app, rows, log_rows, phase=1):
203
289
  if handoff_ran(log_rows):
204
290
  line = "(owner) set current_phase to Released after UAT — no agent command"
205
291
  return ("UAT", f"handoff done, {q}", line, line,
206
- "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")
207
293
  return ("Handoff", q,
208
294
  f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
209
- "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")
210
296
 
211
297
 
212
298
  def main(argv):
@@ -234,15 +320,18 @@ def main(argv):
234
320
  st_text = read(st_path) if os.path.isfile(st_path) else ""
235
321
  log_rows = existing_log_rows(st_text)
236
322
  vdate, vresult = last_verify(root, app)
237
- phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n)
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)}")
238
328
  pname, ptotal = phase_row(root, app, phase_n)
239
329
  if ptotal or phase_n > 1:
240
330
  tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
241
331
  phase = f"{tag} · {phase}"
242
332
  reason = f"{reason}; working {cl_rel}"
243
333
 
244
- total = len(rows)
245
- closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
334
+ verified, counted, na = figures(rows)
246
335
  counts = {}
247
336
  for r in rows:
248
337
  counts[r["status"]] = counts.get(r["status"], 0) + 1
@@ -273,7 +362,7 @@ def main(argv):
273
362
  print()
274
363
  if open_rows:
275
364
  for r in open_rows[:10]:
276
- 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 ''})")
277
366
  if len(open_rows) > 10:
278
367
  print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
279
368
  else:
@@ -283,7 +372,7 @@ def main(argv):
283
372
  print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
284
373
  print()
285
374
  print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
286
- 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 |"
287
376
  for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
288
377
  print(r)
289
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,