@techierathore/techieflow 1.0.2 → 1.0.4

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 (204) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -16
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
  7. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  8. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  9. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
  11. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  12. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  13. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  14. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  17. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  18. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  19. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  20. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  21. package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
  22. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
  23. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  24. package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
  25. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  26. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  27. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  28. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  29. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  30. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  31. package/.opencode/plugin/techieflow.js +40 -13
  32. package/.tfcore/agents/analyst.md +5 -13
  33. package/.tfcore/agents/architect.md +1 -16
  34. package/.tfcore/agents/flow-master.md +42 -215
  35. package/.tfcore/agents/verifier.md +24 -64
  36. package/.tfcore/core-config.yaml +10 -0
  37. package/.tfcore/hooks/block-git.sh +4 -4
  38. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  39. package/.tfcore/hooks/guard-build.sh +76 -0
  40. package/.tfcore/hooks/guard-db.sh +90 -0
  41. package/.tfcore/hooks/guard-metrics.sh +68 -0
  42. package/.tfcore/hooks/guard-status-html.sh +116 -71
  43. package/.tfcore/hooks/guard-status.sh +43 -16
  44. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  45. package/.tfcore/hooks/guard-verify.sh +70 -48
  46. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  47. package/.tfcore/install-manifest.yaml +0 -18
  48. package/.tfcore/routing.yaml +86 -14
  49. package/.tfcore/standards/coding-standards-core.md +61 -0
  50. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  51. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  52. package/.tfcore/tasks/_owner-language.md +26 -0
  53. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  54. package/.tfcore/tasks/_status-update-gate.md +10 -74
  55. package/.tfcore/tasks/_yolo-mode.md +20 -58
  56. package/.tfcore/tasks/amend-docs.md +21 -143
  57. package/.tfcore/tasks/build-phase.md +14 -224
  58. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  59. package/.tfcore/tasks/create-doc.md +4 -0
  60. package/.tfcore/tasks/create-project-brief.md +22 -0
  61. package/.tfcore/tasks/day1-brownfield.md +31 -505
  62. package/.tfcore/tasks/day1-greenfield.md +28 -213
  63. package/.tfcore/tasks/deploy-checklist.md +19 -0
  64. package/.tfcore/tasks/devguide.md +10 -219
  65. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  66. package/.tfcore/tasks/fix-issues.md +10 -129
  67. package/.tfcore/tasks/generate-html.md +5 -1
  68. package/.tfcore/tasks/handoff-phase.md +18 -16
  69. package/.tfcore/tasks/log-miss.md +19 -158
  70. package/.tfcore/tasks/metrics-report.md +3 -1
  71. package/.tfcore/tasks/mockups.md +12 -117
  72. package/.tfcore/tasks/refresh-status.md +9 -130
  73. package/.tfcore/tasks/split-brd.md +10 -126
  74. package/.tfcore/tasks/triage-and-fix.md +14 -0
  75. package/.tfcore/tasks/triage-issues.md +11 -152
  76. package/.tfcore/tasks/verify-phase.md +36 -572
  77. package/.tfcore/telemetry/SCHEMA.md +100 -8
  78. package/.tfcore/telemetry/tf-metrics.sh +564 -32
  79. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  80. package/.tfcore/templates/stack-questions.md +77 -0
  81. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  82. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  83. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  84. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  85. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  86. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  87. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  88. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  89. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  90. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  91. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  92. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  93. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  94. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  95. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  96. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  97. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  98. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  99. package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
  100. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  101. package/.tfcore/user-guide.md +0 -23
  102. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  107. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  108. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  109. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  110. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  111. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  112. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  113. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  114. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  115. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  116. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  117. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  118. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  119. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  120. package/.tfcore/utils/tf-brd-status.py +176 -0
  121. package/.tfcore/utils/tf-brd-status.sh +19 -0
  122. package/.tfcore/utils/tf-build-list.py +211 -0
  123. package/.tfcore/utils/tf-build-list.sh +21 -0
  124. package/.tfcore/utils/tf-build.sh +143 -0
  125. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  126. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  127. package/.tfcore/utils/tf-day1-files.py +208 -0
  128. package/.tfcore/utils/tf-day1-files.sh +20 -0
  129. package/.tfcore/utils/tf-devguide-list.py +249 -0
  130. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  131. package/.tfcore/utils/tf-doc-check.py +1369 -0
  132. package/.tfcore/utils/tf-doc-check.sh +29 -0
  133. package/.tfcore/utils/tf-emit.sh +545 -18
  134. package/.tfcore/utils/tf-fix-close.sh +115 -0
  135. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  136. package/.tfcore/utils/tf-goal.sh +284 -66
  137. package/.tfcore/utils/tf-harness.sh +4 -9
  138. package/.tfcore/utils/tf-log-miss.py +197 -0
  139. package/.tfcore/utils/tf-log-miss.sh +14 -0
  140. package/.tfcore/utils/tf-misses-md.py +208 -0
  141. package/.tfcore/utils/tf-misses-md.sh +14 -0
  142. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  143. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  144. package/.tfcore/utils/tf-model-pick.sh +475 -0
  145. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  146. package/.tfcore/utils/tf-perf-grade.py +97 -0
  147. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  148. package/.tfcore/utils/tf-phase.sh +92 -0
  149. package/.tfcore/utils/tf-render-html.py +20 -0
  150. package/.tfcore/utils/tf-routing-bind.sh +97 -6
  151. package/.tfcore/utils/tf-routing.sh +13 -7
  152. package/.tfcore/utils/tf-selfcheck.sh +323 -0
  153. package/.tfcore/utils/tf-split-brd.py +309 -0
  154. package/.tfcore/utils/tf-split-brd.sh +22 -0
  155. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  156. package/.tfcore/utils/tf-status-facts.py +358 -0
  157. package/.tfcore/utils/tf-status-facts.sh +20 -0
  158. package/.tfcore/utils/tf-triage.py +243 -0
  159. package/.tfcore/utils/tf-triage.sh +16 -0
  160. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  161. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  162. package/.tfcore/utils/tf-verify-env.sh +88 -0
  163. package/.tfcore/utils/tf-verify-list.py +270 -0
  164. package/.tfcore/utils/tf-verify-list.sh +13 -0
  165. package/.tfcore/utils/tf-verify-screens.mjs +355 -0
  166. package/.tfcore/utils/tf-verify-screens.sh +31 -0
  167. package/.tfcore/utils/tf-verify-tests.sh +166 -0
  168. package/.tfcore/utils/tf-verify-verdict.py +292 -0
  169. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  170. package/.tfcore/utils/tf-yolo.sh +59 -0
  171. package/README.md +120 -1145
  172. package/docs/TechieFlow-Installation.md +1 -5
  173. package/opencode.jsonc +37 -33
  174. package/package.json +1 -5
  175. package/scripts/install.mjs +56 -46
  176. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  177. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  178. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  179. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  180. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  181. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  182. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  183. package/.codex/config.toml +0 -46
  184. package/.codex/hooks.json +0 -63
  185. package/.codex/rules/techieflow.rules +0 -8
  186. package/.tfcore/hooks/codex-adapter.py +0 -248
  187. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  188. package/.tfcore/tasks/author-brd.md +0 -79
  189. package/.tfcore/tasks/document-project.md +0 -343
  190. package/.tfcore/tasks/execute-checklist.md +0 -86
  191. package/.tfcore/tasks/index-docs.md +0 -173
  192. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  193. package/.tfcore/tasks/shard-doc.md +0 -185
  194. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  195. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  196. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  197. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  198. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  199. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  200. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  201. package/.tfcore/utils/techieflow-doc-template.md +0 -325
  202. package/.tfcore/utils/tf-codex-bind.py +0 -196
  203. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  204. package/WORKFLOW.html +0 -1986
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env python3
2
+ """tf-status-facts.py — print the facts PROJECT-STATUS.md is written from.
3
+
4
+ bash .tfcore/utils/tf-status-facts.sh <App> ["<command label>"] [--phase N]
5
+
6
+ Reads docs/<App>-Checklist.md, docs/.last-verify.json, docs/metrics/gates.jsonl,
7
+ docs/<App>-*-Feedback.md and the current PROJECT-STATUS.md. On a Large project the
8
+ checklist is the one of the phase being worked: --phase N, else appPhase in
9
+ .tfcore/core-config.yaml (phase 1 is docs/<App>-Checklist.md, phase n is
10
+ docs/<App>-Pn-Checklist.md), and the phase line names it from docs/<App>-Phases.md. Prints, ready to copy
11
+ into the template (.tfcore/templates/v4custom/app-project-status-tmpl.md):
12
+
13
+ - the frontmatter values (last_updated, current_phase, last_verified_*)
14
+ - the Open requirements section (counts by status, at most ten named rows)
15
+ - the Verification log table with its new row (last five kept)
16
+ - the Library feedback summary lines
17
+ - the Next command to run, in both harness forms, and why
18
+
19
+ Writes nothing. The agent writes PROJECT-STATUS.md with the harness Write tool.
20
+ Python 3 standard library only. Exit 0 printed, 2 could not run.
21
+ """
22
+ import datetime
23
+ import glob
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+
29
+ TERMINAL = {"verified", "done (pre-existing)", "n/a"}
30
+ PASS_THROUGH = {"blocked"} # a library gap; counts as closed for the ladder
31
+ BUILT = {"implemented", "needs re-verify"}
32
+ OWNER = {"owner-uat"} # only the owner can close it, from the UsageGuide test plan
33
+ # anything else (Not Started, In Progress, PARTIAL, FAIL, unknown) is not built yet
34
+
35
+ LADDER = ["Not Started", "In Progress", "PARTIAL", "FAIL", "Implemented", "Needs re-verify",
36
+ "Blocked", "Owner-UAT", "Verified", "Done (pre-existing)", "N/A"]
37
+ COUNT_ROWS = ["Not Started", "In Progress", "Implemented", "Needs re-verify", "Blocked"]
38
+
39
+ CC = "/TechieFlow:agents:"
40
+ OC = "/"
41
+ AGENT_OC = {"flow-master": "flow-master", "verifier": "flow-verifier", "analyst": "flow-analyst"}
42
+
43
+
44
+ def die(msg):
45
+ print(f"tf-status-facts: {msg}", file=sys.stderr)
46
+ sys.exit(2)
47
+
48
+
49
+ def norm_status(raw):
50
+ s = raw.strip().strip("`* ").strip()
51
+ low = s.lower()
52
+ for v in LADDER:
53
+ if low == v.lower() or low.startswith(v.lower() + " ("):
54
+ return v
55
+ return s or "?"
56
+
57
+
58
+ def read(path):
59
+ with open(path, encoding="utf-8", errors="replace") as f:
60
+ return f.read()
61
+
62
+
63
+ def checklist_rows(text):
64
+ rows = []
65
+ for line in text.splitlines():
66
+ if not re.match(r"^\s*\|\s*`?REQ-", line):
67
+ continue
68
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
69
+ if len(cells) < 3:
70
+ continue
71
+ rid = cells[0].strip("`* ")
72
+ rows.append({"id": rid, "name": cells[1].strip("`* "), "status": norm_status(cells[2])})
73
+ return rows
74
+
75
+
76
+ def ladder_rank(status):
77
+ return LADDER.index(status) if status in LADDER else 0
78
+
79
+
80
+ def ledger(root):
81
+ """docs/.last-verify.json as a dict, or {} when there is none.
82
+
83
+ The verifier writes it with `json.dump(..., indent=1)`, so it is ONE pretty-printed
84
+ object spanning many lines. This reader used to take `splitlines()[-1]` — written for
85
+ an append-per-line ledger that no version of the verifier has ever produced — so it
86
+ parsed the closing brace, threw, and every project reported `last_verified_build:
87
+ not-run` however many verify runs it had behind it (TfLens: 69 of 73 rows Verified and
88
+ a full ledger, reported as never verified). Whole file first, last line second for a
89
+ legacy JSONL ledger. tests/regression/run.sh tf_ledger."""
90
+ path = os.path.join(root, "docs", ".last-verify.json")
91
+ if not os.path.isfile(path):
92
+ return {}
93
+ text = read(path).strip()
94
+ for candidate in (text, text.splitlines()[-1] if text else ""):
95
+ try:
96
+ data = json.loads(candidate)
97
+ if isinstance(data, dict):
98
+ return data
99
+ except Exception:
100
+ continue
101
+ return {}
102
+
103
+
104
+ def ledger_verdicts(root):
105
+ """{REQ id: verdict} from the last verify run, or {} when none has run.
106
+
107
+ The verifier already writes every row's verdict into the ledger. Reading it here is
108
+ what lets the status line tell "built, waiting for a verify" apart from "verified as
109
+ far as it can be, and the rest needs a decision only the owner can make"."""
110
+ rows = ledger(root).get("rows")
111
+ return rows if isinstance(rows, dict) else {}
112
+
113
+
114
+ def last_verify(root, app):
115
+ """(date, build_result) from docs/.last-verify.json and gates.jsonl; ('', 'not-run') if none."""
116
+ data = ledger(root)
117
+ if not data:
118
+ return "", "not-run"
119
+ date = str(data.get("date", ""))[:10]
120
+ result = "PASS"
121
+ gates = os.path.join(root, "docs", "metrics", "gates.jsonl")
122
+ if date and os.path.isfile(gates):
123
+ for line in read(gates).splitlines():
124
+ try:
125
+ rec = json.loads(line)
126
+ except Exception:
127
+ continue
128
+ if rec.get("app") == app and str(rec.get("ts", ""))[:10] == date and rec.get("gate") == "build":
129
+ result = "FAIL"
130
+ break
131
+ return date, result
132
+
133
+
134
+ def existing_log_rows(status_text):
135
+ m = re.search(r"(?ms)^## Verification log\s*\n(.*?)(?=^## |\Z)", status_text)
136
+ if not m:
137
+ return []
138
+ rows = []
139
+ for line in m.group(1).splitlines():
140
+ if re.match(r"^\s*\|", line) and not re.match(r"^\s*\|\s*-", line) and "Date" not in line.split("|")[1]:
141
+ rows.append(line.strip())
142
+ return rows
143
+
144
+
145
+ def handoff_ran(log_rows):
146
+ return any("handoff" in r.lower() for r in log_rows)
147
+
148
+
149
+ 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"]
163
+
164
+
165
+ def config_phase(root):
166
+ p = os.path.join(root, ".tfcore", "core-config.yaml")
167
+ if os.path.isfile(p):
168
+ m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
169
+ if m:
170
+ return int(m.group(1))
171
+ return 1
172
+
173
+
174
+ def phase_prefix(app, phase):
175
+ return f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
176
+
177
+
178
+ def phase_row(root, app, phase):
179
+ """(name, total phases) from docs/<App>-Phases.md; ('', 0) when the project has no phases."""
180
+ p = os.path.join(root, "docs", f"{app}-Phases.md")
181
+ if not os.path.isfile(p):
182
+ return "", 0
183
+ m = re.search(r"(?ms)^##\s+(?:\d+[.)]\s*)?Phases[^\n]*\n(.*?)(?=^## |\Z)", read(p))
184
+ name, total = "", 0
185
+ for line in (m.group(1) if m else "").splitlines():
186
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
187
+ if len(cells) >= 2 and cells[0].strip("`* ").isdigit():
188
+ total += 1
189
+ if int(cells[0].strip("`* ")) == phase:
190
+ name = cells[1].strip("`* ")
191
+ return name, total
192
+
193
+
194
+ def next_command(app, rows, log_rows, phase=1, verdicts=None):
195
+ """Returns (phase, qualifier, cc_line, oc_line, reason)."""
196
+ verdicts = verdicts or {}
197
+
198
+ def ids(lst):
199
+ return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
200
+
201
+ if not rows:
202
+ if os.path.isfile(os.path.join(os.getcwd(), "docs", f"{phase_prefix(app, phase)}BRD.md")):
203
+ return ("Day-1", "stage 1 done, awaiting owner review",
204
+ f"{CC}analyst *day1-greenfield {app} --stage2", f"{OC}flow-analyst *day1-greenfield {app} --stage2",
205
+ "the BRD exists and no checklist; stage 2 runs on the owner's go-ahead")
206
+ return ("Day-1", "no documents yet",
207
+ f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
208
+ "no BRD and no checklist exist; day-1 stage 1 has not run")
209
+
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)}")
221
+ if built:
222
+ # A row the last verify could not MEASURE is not a row waiting for another verify.
223
+ # Its tests declined — the state they need does not exist in this environment — so
224
+ # running the verifier again produces the same NOT-TESTED and the project sits in a
225
+ # loop that looks like progress. Say what it actually needs: data, a changed
226
+ # acceptance line, or the row marked N/A. Only the owner can choose. FR-69's sibling
227
+ # case; the verdicts come from the ledger the verifier already writes.
228
+ unmeasured = [r for r in built if verdicts.get(r["id"]) == "NOT-TESTED"]
229
+ waiting = [r for r in built if r not in unmeasured]
230
+ if unmeasured and not waiting:
231
+ line = (f"(owner) {len(unmeasured)} row(s) cannot be measured here — create the data, "
232
+ f"change the acceptance line, or mark the row N/A: {ids(unmeasured)}")
233
+ return ("Verify", f"{len(unmeasured)} not measurable, {q}", line, line,
234
+ f"every test carrying these ids was skipped, so another verify run changes nothing: {ids(unmeasured)}")
235
+ ui = [r for r in waiting if r["id"].startswith("REQ-UI")]
236
+ scope = "ui" if len(ui) == len(waiting) else ("functional" if not ui else "all")
237
+ reason = f"{len(waiting)} rows are built and not verified: {ids(waiting)}"
238
+ if unmeasured:
239
+ reason += (f"; {len(unmeasured)} more could not be measured (every test skipped) "
240
+ f"and need the owner: {ids(unmeasured)}")
241
+ return ("Verify", f"{len(waiting)} to verify, {q}",
242
+ f"{CC}verifier *verify {scope} {app}", f"{OC}flow-verifier *verify {scope} {app}",
243
+ reason)
244
+ if owner:
245
+ if handoff_ran(log_rows):
246
+ line = f"(owner-run) docs/{app}-UsageGuide.md — {ids(owner)}"
247
+ return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
248
+ f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
249
+ return ("Handoff", f"{len(owner)} owner-run, {q}",
250
+ f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
251
+ f"{len(owner)} rows are Owner-UAT and handoff has not run yet")
252
+ if handoff_ran(log_rows):
253
+ line = "(owner) set current_phase to Released after UAT — no agent command"
254
+ return ("UAT", f"handoff done, {q}", line, line,
255
+ "every row is terminal and handoff has run; waiting on the owner")
256
+ return ("Handoff", q,
257
+ 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")
259
+
260
+
261
+ def main(argv):
262
+ if len(argv) < 2 or argv[1] in ("-h", "--help"):
263
+ print(__doc__)
264
+ return 0
265
+ app = argv[1]
266
+ args = argv[2:]
267
+ root = os.getcwd()
268
+ phase_n = None
269
+ if "--phase" in args:
270
+ i = args.index("--phase")
271
+ if i + 1 >= len(args) or not args[i + 1].isdigit() or int(args[i + 1]) < 1:
272
+ die("--phase needs a number from 1 upwards")
273
+ phase_n = int(args[i + 1])
274
+ del args[i:i + 2]
275
+ phase_n = phase_n or config_phase(root)
276
+ label = args[0] if args else "status"
277
+ today = datetime.date.today().isoformat()
278
+
279
+ cl_rel = f"docs/{phase_prefix(app, phase_n)}Checklist.md"
280
+ cl_path = os.path.join(root, cl_rel)
281
+ rows = checklist_rows(read(cl_path)) if os.path.isfile(cl_path) else []
282
+ st_path = os.path.join(root, "PROJECT-STATUS.md")
283
+ st_text = read(st_path) if os.path.isfile(st_path) else ""
284
+ log_rows = existing_log_rows(st_text)
285
+ vdate, vresult = last_verify(root, app)
286
+ phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n, ledger_verdicts(root))
287
+ pname, ptotal = phase_row(root, app, phase_n)
288
+ if ptotal or phase_n > 1:
289
+ tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
290
+ phase = f"{tag} · {phase}"
291
+ reason = f"{reason}; working {cl_rel}"
292
+
293
+ total = len(rows)
294
+ closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
295
+ counts = {}
296
+ for r in rows:
297
+ counts[r["status"]] = counts.get(r["status"], 0) + 1
298
+ open_rows = sorted((r for r in rows if r["status"].lower() not in TERMINAL),
299
+ key=lambda r: (ladder_rank(r["status"]), r["id"]))
300
+
301
+ print(f"# tf-status-facts — {app} — {today} — copy into PROJECT-STATUS.md (Write tool)")
302
+ print()
303
+ print("## Frontmatter values")
304
+ print(f"last_updated: {today}")
305
+ print(f"current_phase: {phase} — {qual}")
306
+ print(f"last_verified_build: {vresult}")
307
+ print(f"last_verified_date: {vdate or 'never'}")
308
+ print()
309
+ print("## Next command to run")
310
+ print(f"Claude Code:\n```\n{cc}\n```\nOpenCode:\n```\n{oc}\n```")
311
+ print(f"Why: {reason}.")
312
+ print()
313
+ print("## Open requirements")
314
+ print("| Status | Count |\n|---|---|")
315
+ shown = set()
316
+ for s in COUNT_ROWS:
317
+ print(f"| {s} | {counts.get(s, 0)} |")
318
+ shown.add(s)
319
+ for s, n in sorted(counts.items(), key=lambda kv: ladder_rank(kv[0])):
320
+ if s not in shown and s.lower() not in TERMINAL:
321
+ print(f"| {s} | {n} |")
322
+ print()
323
+ if open_rows:
324
+ for r in open_rows[:10]:
325
+ print(f"- [ ] {r['id']} — {r['name']} ({r['status']})")
326
+ if len(open_rows) > 10:
327
+ print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
328
+ else:
329
+ print("- None")
330
+ print()
331
+ print("## Verification log")
332
+ print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
333
+ print()
334
+ print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
335
+ new_row = f"| {today} | {label} | {closed}/{total} Verified | {cl_rel}#requirements-status |"
336
+ for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
337
+ print(r)
338
+ print(new_row)
339
+ print()
340
+ print("## Library feedback summary")
341
+ for line in feedback_lines(root, app):
342
+ print(line)
343
+ print()
344
+ print("## You write these yourself")
345
+ print("- Where I am: at most 80 words, state not story." + (f" Name the phase: {phase.split(' · ')[0]}." if ptotal or phase_n > 1 else ""))
346
+ print("- Known blockers: one line each, or `- None`.")
347
+ print("- Standards compliance: `- Last check {date}: {n} findings, see the checklist Remarks.`")
348
+ print("- Deferred / future: parked ideas, one line each.")
349
+ return 0
350
+
351
+
352
+ if __name__ == "__main__":
353
+ try:
354
+ sys.exit(main(sys.argv))
355
+ except SystemExit:
356
+ raise
357
+ except Exception as e: # never a stack trace for the agent to wade through
358
+ die(str(e))
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # tf-status-facts.sh — print the facts PROJECT-STATUS.md is written from.
3
+ #
4
+ # bash .tfcore/utils/tf-status-facts.sh MyApp "build-phase"
5
+ #
6
+ # Prints the frontmatter values, the next command in both harness forms, the
7
+ # Open requirements section, the Verification log with its new row, and the
8
+ # library feedback lines, all copy-ready for the template
9
+ # .tfcore/templates/v4custom/app-project-status-tmpl.md. Writes nothing:
10
+ # PROJECT-STATUS.md is written with the harness Write tool (guard-status.sh
11
+ # refuses a shell write). Step 1 of _status-update-gate.md.
12
+ #
13
+ # Thin wrapper over tf-status-facts.py. Python 3 standard library only.
14
+ set -uo pipefail
15
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ if ! command -v python3 >/dev/null 2>&1; then
17
+ echo "tf-status-facts: python3 is required (standard library only)." >&2
18
+ exit 2
19
+ fi
20
+ exec python3 "$SELF_DIR/tf-status-facts.py" "$@"
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ """tf-triage.py — log reported bugs in the checklist and the telemetry (see tf-triage.sh).
3
+
4
+ bash .tfcore/utils/tf-triage.sh <App> demote <REQ> "<symptom>" [--kind layout|render-empty|data-logic|rag]
5
+ [--evidence <path>] [--source owner|production] [--why <why_missed>] [--sort <sort>]
6
+ bash .tfcore/utils/tf-triage.sh <App> new "<title>" "<When … on <screen>, then …>" [--prefix UI|FN|NFR|RAG]
7
+ [--section "<screen>"] [--evidence <path>] [--source owner|production] [--mockup <file>] [--sort <sort>]
8
+ bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
9
+ bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--verify-ran] [--cmd triage-issues|fix-issues]
10
+
11
+ demote the row goes to Needs re-verify (% capped at 75) with a dated Remark holding the symptom in the
12
+ reporter's words, the evidence path and the kind.
13
+ new a defect no row covers: the next free id in the prefix, a Not Started row, a detail entry with the
14
+ acceptance line and the marker BRD-pending (*amend-docs gives it its BRD item).
15
+ note a dated remark on the nearest row, no status change (could not reproduce).
16
+ close what triage owes the telemetry, from the actions above (tests/.artifacts/verify/triage.json):
17
+ one gate record per demoted or new row with gate "escaped" (no check caught it), one miss per row
18
+ with found_by owner or production, the symptom as `what` and the sort (FR-32; --sort on demote or
19
+ new, else weak-check for a demoted row, whose line existed and whose check let it through, and
20
+ spec for a new row, which the spec never had) (duplicates collapsed through
21
+ tf-emit.sh --open-miss; the origin run looked up with --origin-of), then the run record. It also
22
+ 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).
24
+ Exit 0 · 2 could not run.
25
+ """
26
+ import datetime
27
+ import importlib.util
28
+ import json
29
+ import os
30
+ import subprocess
31
+ import sys
32
+
33
+ HERE = os.path.dirname(os.path.abspath(__file__))
34
+ LOGP = os.path.join("tests", ".artifacts", "verify", "triage.json")
35
+ KIND_CLASS = {"layout": "overlap", "render-empty": "blank-data", "data-logic": "assert-fail", "rag": "other", None: "other"}
36
+ EMIT = os.path.join(HERE, "tf-emit.sh")
37
+
38
+
39
+ def die(m):
40
+ print(f"tf-triage: {m}", file=sys.stderr)
41
+ sys.exit(2)
42
+
43
+
44
+ def mod():
45
+ spec = importlib.util.spec_from_file_location("tf_checklist_edit", os.path.join(HERE, "tf-checklist-edit.py"))
46
+ m = importlib.util.module_from_spec(spec)
47
+ spec.loader.exec_module(m)
48
+ return m
49
+
50
+
51
+ def cfg_phase():
52
+ p = os.path.join(".tfcore", "core-config.yaml")
53
+ if os.path.isfile(p):
54
+ import re
55
+ m = re.search(r"(?m)^appPhase:\s*(\d+)", open(p, encoding="utf-8").read())
56
+ if m:
57
+ return int(m.group(1))
58
+ return 1
59
+
60
+
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
67
+
68
+
69
+ def log_load():
70
+ try:
71
+ return json.load(open(LOGP, encoding="utf-8"))
72
+ except Exception:
73
+ return {"actions": []}
74
+
75
+
76
+ def log_save(d):
77
+ os.makedirs(os.path.dirname(LOGP), exist_ok=True)
78
+ json.dump(d, open(LOGP, "w", encoding="utf-8"), indent=1)
79
+
80
+
81
+ def opt(argv, name, default=None):
82
+ return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else default
83
+
84
+
85
+ SORTS = ("spec", "unsaid", "weak-check", "ignored")
86
+
87
+
88
+ def sort_opt(argv, default):
89
+ s = opt(argv, "--sort", default)
90
+ if s not in SORTS:
91
+ die(f"--sort must be one of {', '.join(SORTS)}")
92
+ return s
93
+
94
+
95
+ def q(*args):
96
+ p = subprocess.run(["bash", EMIT, *args], capture_output=True, text=True)
97
+ return p.stdout.strip()
98
+
99
+
100
+ def emit(stream, rec):
101
+ p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
102
+ msg = (p.stdout + p.stderr).strip()
103
+ ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
104
+ if not ok:
105
+ print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
106
+ return ok
107
+
108
+
109
+ def main(argv):
110
+ if len(argv) < 3 or argv[1] in ("-h", "--help"):
111
+ print(__doc__)
112
+ return 0 if len(argv) > 1 else 2
113
+ app, verb = argv[1], argv[2]
114
+ ce = mod()
115
+ cl = checklist(app)
116
+ log = log_load()
117
+ log.setdefault("app", app)
118
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
119
+
120
+ if verb == "demote":
121
+ if len(argv) < 5:
122
+ die("demote needs <REQ> \"<symptom>\"")
123
+ rid, symptom = argv[3].upper(), argv[4]
124
+ kind = opt(argv, "--kind"); ev = opt(argv, "--evidence"); src = opt(argv, "--source", "owner")
125
+ prior = ce.demote(cl, rid, symptom, kind=kind, evidence=ev, source=src, prefix="⚠ prod bug" if src == "production" else "⚠ UAT bug")
126
+ if prior is None:
127
+ die(f"{rid} is not a row of {cl}")
128
+ log["actions"].append({"verb": "demote", "req_id": rid, "symptom": symptom, "kind": kind, "evidence": ev, "source": src,
129
+ "why": opt(argv, "--why", "insufficient-verify-method"), "sort": sort_opt(argv, "weak-check"),
130
+ "prior": prior["status"], "ts": now})
131
+ log_save(log)
132
+ print(f"{rid}: {prior['status']} → Needs re-verify — {symptom}")
133
+ return 0
134
+
135
+ if verb == "new":
136
+ if len(argv) < 5:
137
+ die("new needs \"<title>\" \"<acceptance>\"")
138
+ title, acc = argv[3], argv[4]
139
+ prefix = opt(argv, "--prefix", "FN").upper(); section = opt(argv, "--section"); ev = opt(argv, "--evidence")
140
+ src = opt(argv, "--source", "owner"); mockup = opt(argv, "--mockup")
141
+ if prefix == "UI" and not mockup:
142
+ print("note: a UI row needs a mockup link; pass --mockup docs/mockups/<screen>.html or the checker will flag it")
143
+ rid = ce.add_row(cl, prefix, title, acc, section=section, mockup=mockup,
144
+ remark=f"logged from {'production' if src == 'production' else 'UAT'} {ce.TODAY}" + (f" (evidence: {ev})" if ev else ""))
145
+ log["actions"].append({"verb": "new", "req_id": rid, "symptom": title, "kind": None, "evidence": ev, "source": src,
146
+ "why": opt(argv, "--why", "missing-checklist-item"), "sort": sort_opt(argv, "spec"), "prior": None, "ts": now})
147
+ log_save(log)
148
+ print(f"{rid}: new Not Started row — {title}")
149
+ return 0
150
+
151
+ if verb == "note":
152
+ if len(argv) < 5:
153
+ die("note needs <REQ> \"<text>\"")
154
+ rid = argv[3].upper()
155
+ if ce.note(cl, rid, argv[4]) is None:
156
+ die(f"{rid} is not a row of {cl}")
157
+ log["actions"].append({"verb": "note", "req_id": rid, "symptom": argv[4], "ts": now})
158
+ log_save(log)
159
+ print(f"{rid}: noted — {argv[4]}")
160
+ return 0
161
+
162
+ if verb == "close":
163
+ started = opt(argv, "--started") or subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
164
+ if started.startswith("{"):
165
+ import re
166
+ m = re.search(r'"started":"([^"]*)"', started)
167
+ started = m.group(1) if m else ""
168
+ started = started.strip() if started and not started.startswith("none") else ""
169
+ cmd = opt(argv, "--cmd", "triage-issues")
170
+ # code untouched?
171
+ changed = []
172
+ if started:
173
+ t0 = datetime.datetime.strptime(started, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc).timestamp()
174
+ for top in ("src", "source", "tests"):
175
+ 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")]
177
+ for f in files:
178
+ p = os.path.join(d, f)
179
+ try:
180
+ if os.path.getmtime(p) > t0:
181
+ changed.append(p.replace("\\", "/"))
182
+ except OSError:
183
+ pass
184
+ acts = [a for a in log.get("actions", []) if a["verb"] in ("demote", "new")]
185
+ gates = misses = 0
186
+ for a in acts:
187
+ rid = a["req_id"]; cls_req = rid.split("-")[1]
188
+ att = q("--next-attempt", rid) or "1"
189
+ fc = KIND_CLASS.get(a.get("kind"), "other")
190
+ rec = {"kind": "gate", "app": app, "run_id": started, "req_id": rid, "req_class": cls_req,
191
+ "attempt": int(att) if att.isdigit() else 1, "verdict": "Needs re-verify" if a["verb"] == "demote" else "FAIL",
192
+ "gate": "escaped", "gates_run": [], "failure_class": fc, "prior_verdict": a.get("prior")}
193
+ if emit("gates", rec):
194
+ gates += 1
195
+ mc = ("unspecified-gap" if a["verb"] == "new" else "regression" if (a.get("prior") or "").lower().startswith("verified") else "wrong-behaviour")
196
+ opened = q("--open-miss", rid)
197
+ if opened and opened.split()[-1] == mc:
198
+ print(f" {rid}: already open as {opened.split()[0]} ({mc}); no second miss")
199
+ continue
200
+ mid = q("--next-miss-id")
201
+ m = {"kind": "miss", "miss_id": mid, "req_id": rid, "req_class": cls_req, "miss_class": mc,
202
+ "artifact": "brd" if a["verb"] == "new" else "src", "severity": "major", "why_missed": a.get("why"),
203
+ "origin_phase": "day1-greenfield" if a["verb"] == "new" else "build-phase", "origin_agent": "analyst" if a["verb"] == "new" else "flow-master",
204
+ "found_by": a.get("source", "owner"), "found_phase": cmd, "found_gate": None, "found_run_id": started,
205
+ "failure_class": fc, "what": a["symptom"][:300], "sort": a.get("sort") or ("spec" if a["verb"] == "new" else "weak-check")}
206
+ if a["verb"] != "new":
207
+ o = q("--origin-of", rid).split()
208
+ if len(o) >= 3:
209
+ m["origin_run_id"], m["origin_phase"], m["origin_agent"] = o[0], o[1], o[2]
210
+ if emit("misses", m):
211
+ misses += 1
212
+ a["miss_id"] = mid
213
+ if changed and cmd == "triage-issues" and not log.get("code_miss_id"):
214
+ mid = q("--next-miss-id")
215
+ log["code_miss_id"] = mid
216
+ emit("misses", {"kind": "miss", "miss_id": mid, "req_id": None, "req_class": None, "miss_class": "other", "artifact": "src",
217
+ "severity": "major", "why_missed": "instruction-ignored", "origin_phase": "triage-issues", "origin_agent": "flow-master",
218
+ "found_by": "gate", "found_phase": "triage-issues", "found_gate": None, "found_run_id": started, "failure_class": "other",
219
+ "what": f"triage edited {len(changed)} file(s) under src, source or tests, which a triage never does: " + ", ".join(changed[:5]),
220
+ "sort": "ignored"})
221
+ print(f"WARNING: {len(changed)} file(s) changed under src/, source/ or tests/ during this triage (logged as {mid}): " + ", ".join(changed[:5]))
222
+ if cmd == "triage-issues":
223
+ rec = {"kind": "run", "app": app, "cmd": "triage-issues", "mode": None, "ended": now, "reqs_touched": [a["req_id"] for a in acts],
224
+ "reqs_count": len(acts), "subagents": [], "files_written": 1, "build_result": "not-run"}
225
+ if started:
226
+ rec["started"] = started
227
+ already = False # one run record per start: a close called twice must not count the triage twice
228
+ runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
229
+ if started and os.path.isfile(runs_path):
230
+ already = any('"cmd":"triage-issues"' in l and f'"started":"{started}"' in l for l in open(runs_path, encoding="utf-8", errors="replace"))
231
+ ok = True if already else emit("runs", rec)
232
+ else:
233
+ ok = None
234
+ log["closed"] = now
235
+ log_save(log)
236
+ print(f"triage: {len(acts)} row(s) logged — {gates} gate record(s) (escaped), {misses} miss(es)"
237
+ + (f", run record {'written' if ok else 'not written'}" if ok is not None else "") + f"; code untouched: {'yes' if not changed else 'NO'}")
238
+ return 0
239
+ die(f"unknown verb {verb}")
240
+
241
+
242
+ if __name__ == "__main__":
243
+ sys.exit(main(sys.argv))
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ # tf-triage.sh — log reported bugs in the checklist and the telemetry (Sitting 4c, 2026-09-06).
3
+ #
4
+ # bash .tfcore/utils/tf-triage.sh <App> demote <REQ> "<symptom>" [--kind layout|render-empty|data-logic|rag] [--evidence <path>] [--source owner|production] [--why <why_missed>]
5
+ # bash .tfcore/utils/tf-triage.sh <App> new "<title>" "<When … on <screen>, then …>" [--prefix UI|FN|NFR|RAG] [--section "<screen>"] [--evidence <path>] [--mockup <file>]
6
+ # bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
7
+ # bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--cmd triage-issues|fix-issues]
8
+ #
9
+ # demote: Needs re-verify with a dated Remark in the reporter's words. new: a Not Started row with its
10
+ # acceptance line and BRD-pending. note: a remark, no status change. close: the escaped gate records,
11
+ # one miss per row (found by the owner, the symptom as `what`, duplicates collapsed), the run record,
12
+ # and a warning plus a miss when code changed during the triage. Details: tf-triage.py.
13
+ set -u
14
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ command -v python3 >/dev/null 2>&1 || { echo "tf-triage: python3 is required" >&2; exit 2; }
16
+ exec python3 "$HERE/tf-triage.py" "$@"