@techierathore/techieflow 1.0.2 → 1.0.3

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 (174) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
  2. package/.claude/commands/TechieFlow/agents/architect.md +0 -6
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  9. package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
  10. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  11. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  12. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  13. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  14. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  15. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  16. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  17. package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
  18. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  19. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  20. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  21. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  22. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  23. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  24. package/.opencode/plugin/techieflow.js +39 -12
  25. package/.tfcore/agents/analyst.md +0 -5
  26. package/.tfcore/agents/architect.md +0 -6
  27. package/.tfcore/agents/flow-master.md +41 -215
  28. package/.tfcore/agents/verifier.md +23 -64
  29. package/.tfcore/core-config.yaml +10 -0
  30. package/.tfcore/hooks/block-git.sh +4 -4
  31. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  32. package/.tfcore/hooks/guard-build.sh +76 -0
  33. package/.tfcore/hooks/guard-db.sh +90 -0
  34. package/.tfcore/hooks/guard-metrics.sh +56 -0
  35. package/.tfcore/hooks/guard-status-html.sh +116 -71
  36. package/.tfcore/hooks/guard-status.sh +43 -16
  37. package/.tfcore/hooks/guard-verify.sh +70 -48
  38. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  39. package/.tfcore/install-manifest.yaml +0 -18
  40. package/.tfcore/routing.yaml +25 -14
  41. package/.tfcore/standards/coding-standards-core.md +61 -0
  42. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  43. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  44. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  45. package/.tfcore/tasks/_status-update-gate.md +10 -74
  46. package/.tfcore/tasks/_yolo-mode.md +20 -58
  47. package/.tfcore/tasks/amend-docs.md +20 -143
  48. package/.tfcore/tasks/build-phase.md +14 -224
  49. package/.tfcore/tasks/day1-brownfield.md +31 -505
  50. package/.tfcore/tasks/day1-greenfield.md +28 -213
  51. package/.tfcore/tasks/deploy-checklist.md +19 -0
  52. package/.tfcore/tasks/devguide.md +10 -219
  53. package/.tfcore/tasks/fix-issues.md +10 -129
  54. package/.tfcore/tasks/log-miss.md +19 -158
  55. package/.tfcore/tasks/metrics-report.md +1 -1
  56. package/.tfcore/tasks/mockups.md +12 -117
  57. package/.tfcore/tasks/refresh-status.md +9 -130
  58. package/.tfcore/tasks/split-brd.md +10 -126
  59. package/.tfcore/tasks/triage-and-fix.md +14 -0
  60. package/.tfcore/tasks/triage-issues.md +11 -152
  61. package/.tfcore/tasks/verify-phase.md +36 -572
  62. package/.tfcore/telemetry/SCHEMA.md +43 -7
  63. package/.tfcore/telemetry/tf-metrics.sh +152 -17
  64. package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
  65. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  66. package/.tfcore/templates/stack-questions.md +77 -0
  67. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  68. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  69. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  70. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  71. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  72. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  73. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  74. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  75. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  76. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  77. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  78. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  79. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  80. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  81. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  82. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  83. package/.tfcore/user-guide.md +0 -23
  84. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  90. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  91. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  92. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  93. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  94. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  95. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  96. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  98. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  99. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  100. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  101. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  102. package/.tfcore/utils/tf-brd-status.py +176 -0
  103. package/.tfcore/utils/tf-brd-status.sh +19 -0
  104. package/.tfcore/utils/tf-build-list.py +191 -0
  105. package/.tfcore/utils/tf-build-list.sh +20 -0
  106. package/.tfcore/utils/tf-build.sh +143 -0
  107. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  108. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  109. package/.tfcore/utils/tf-day1-files.py +208 -0
  110. package/.tfcore/utils/tf-day1-files.sh +20 -0
  111. package/.tfcore/utils/tf-devguide-list.py +249 -0
  112. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  113. package/.tfcore/utils/tf-doc-check.py +1267 -0
  114. package/.tfcore/utils/tf-doc-check.sh +29 -0
  115. package/.tfcore/utils/tf-emit.sh +203 -13
  116. package/.tfcore/utils/tf-fix-close.sh +83 -0
  117. package/.tfcore/utils/tf-goal.sh +193 -62
  118. package/.tfcore/utils/tf-harness.sh +4 -9
  119. package/.tfcore/utils/tf-log-miss.py +197 -0
  120. package/.tfcore/utils/tf-log-miss.sh +14 -0
  121. package/.tfcore/utils/tf-misses-md.py +211 -0
  122. package/.tfcore/utils/tf-misses-md.sh +13 -0
  123. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  124. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  125. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  126. package/.tfcore/utils/tf-perf-grade.py +97 -0
  127. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  128. package/.tfcore/utils/tf-phase.sh +74 -0
  129. package/.tfcore/utils/tf-routing-bind.sh +10 -5
  130. package/.tfcore/utils/tf-routing.sh +6 -7
  131. package/.tfcore/utils/tf-split-brd.py +304 -0
  132. package/.tfcore/utils/tf-split-brd.sh +22 -0
  133. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  134. package/.tfcore/utils/tf-status-facts.py +309 -0
  135. package/.tfcore/utils/tf-status-facts.sh +20 -0
  136. package/.tfcore/utils/tf-triage.py +243 -0
  137. package/.tfcore/utils/tf-triage.sh +16 -0
  138. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  139. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  140. package/.tfcore/utils/tf-verify-env.sh +88 -0
  141. package/.tfcore/utils/tf-verify-list.py +270 -0
  142. package/.tfcore/utils/tf-verify-list.sh +13 -0
  143. package/.tfcore/utils/tf-verify-screens.mjs +283 -0
  144. package/.tfcore/utils/tf-verify-screens.sh +22 -0
  145. package/.tfcore/utils/tf-verify-tests.sh +123 -0
  146. package/.tfcore/utils/tf-verify-verdict.py +283 -0
  147. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  148. package/.tfcore/utils/tf-yolo.sh +59 -0
  149. package/README.md +114 -1149
  150. package/docs/TechieFlow-Installation.md +1 -5
  151. package/opencode.jsonc +13 -33
  152. package/package.json +1 -5
  153. package/scripts/install.mjs +52 -46
  154. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  155. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  156. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  157. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  158. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  159. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  160. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  161. package/.codex/config.toml +0 -46
  162. package/.codex/hooks.json +0 -63
  163. package/.codex/rules/techieflow.rules +0 -8
  164. package/.tfcore/hooks/codex-adapter.py +0 -248
  165. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  166. package/.tfcore/tasks/author-brd.md +0 -79
  167. package/.tfcore/tasks/document-project.md +0 -343
  168. package/.tfcore/tasks/execute-checklist.md +0 -86
  169. package/.tfcore/tasks/index-docs.md +0 -173
  170. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  171. package/.tfcore/tasks/shard-doc.md +0 -185
  172. package/.tfcore/utils/tf-codex-bind.py +0 -196
  173. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  174. package/WORKFLOW.html +0 -1986
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # tf-status-evidence.sh — the evidence *refresh-status rebuilds PROJECT-STATUS from, after a
3
+ # session died before its status gate (Sitting 4b, 2026-09-06). Reads only; never git.
4
+ #
5
+ # bash .tfcore/utils/tf-status-evidence.sh <App> [--no-build] [--phase N]
6
+ #
7
+ # Prints, in this order:
8
+ # 1. what the stale PROJECT-STATUS.md claims (last_updated, current_phase, the next command)
9
+ # 2. files under src/ and tests/ changed after that file was last written (work the gate never saw)
10
+ # 3. checklist rows left at Implemented / In Progress / Needs re-verify / PARTIAL / FAIL
11
+ # (the fingerprint of a smoke that ran and a verify that did not)
12
+ # 4. the build verdict from tf-build.sh (skipped with --no-build)
13
+ # Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
14
+ set -uo pipefail
15
+ APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
16
+ shift
17
+ BUILD=1; PHASE=""
18
+ while [[ $# -gt 0 ]]; do
19
+ case "$1" in
20
+ --no-build) BUILD=0; shift ;;
21
+ --phase) PHASE="$2"; shift 2 ;;
22
+ *) shift ;;
23
+ esac
24
+ 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
+ [[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
27
+ ST="PROJECT-STATUS.md"
28
+
29
+ echo "# tf-status-evidence — $APP — $(date -u +%Y-%m-%dT%H:%M:%SZ) — phase $PHASE"
30
+ echo
31
+ echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
32
+ if [[ -f "$ST" ]]; then
33
+ 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)"
36
+ else
37
+ echo "- no PROJECT-STATUS.md; treat as never written"
38
+ fi
39
+ echo
40
+ echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
41
+ 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)
45
+ echo "- ${#NEWER[@]} file(s)"
46
+ printf -- '- %s\n' "${NEWER[@]:0:30}"
47
+ [[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
48
+ else
49
+ echo "- no status file to compare against"
50
+ fi
51
+ echo
52
+ echo "## 3. Checklist rows that look interrupted ($CL)"
53
+ if [[ -f "$CL" ]]; then
54
+ grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
55
+ | 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)
57
+ [[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
58
+ else
59
+ echo "- $CL does not exist"
60
+ fi
61
+ echo
62
+ echo "## 4. Build"
63
+ if [[ $BUILD -eq 1 ]]; then
64
+ bash "$(dirname "${BASH_SOURCE[0]}")/tf-build.sh" build 2>&1 | tail -3 | sed 's/^/- /'
65
+ else
66
+ echo "- skipped (--no-build)"
67
+ fi
68
+ exit 0
@@ -0,0 +1,309 @@
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 last_verify(root, app):
81
+ """(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:
88
+ return "", "not-run"
89
+ date = str(data.get("date", ""))[:10]
90
+ result = "PASS"
91
+ gates = os.path.join(root, "docs", "metrics", "gates.jsonl")
92
+ if date and os.path.isfile(gates):
93
+ for line in read(gates).splitlines():
94
+ try:
95
+ rec = json.loads(line)
96
+ except Exception:
97
+ continue
98
+ if rec.get("app") == app and str(rec.get("ts", ""))[:10] == date and rec.get("gate") == "build":
99
+ result = "FAIL"
100
+ break
101
+ return date, result
102
+
103
+
104
+ def existing_log_rows(status_text):
105
+ m = re.search(r"(?ms)^## Verification log\s*\n(.*?)(?=^## |\Z)", status_text)
106
+ if not m:
107
+ return []
108
+ rows = []
109
+ for line in m.group(1).splitlines():
110
+ if re.match(r"^\s*\|", line) and not re.match(r"^\s*\|\s*-", line) and "Date" not in line.split("|")[1]:
111
+ rows.append(line.strip())
112
+ return rows
113
+
114
+
115
+ def handoff_ran(log_rows):
116
+ return any("handoff" in r.lower() for r in log_rows)
117
+
118
+
119
+ 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"]
133
+
134
+
135
+ def config_phase(root):
136
+ p = os.path.join(root, ".tfcore", "core-config.yaml")
137
+ if os.path.isfile(p):
138
+ m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
139
+ if m:
140
+ return int(m.group(1))
141
+ return 1
142
+
143
+
144
+ def phase_prefix(app, phase):
145
+ return f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
146
+
147
+
148
+ def phase_row(root, app, phase):
149
+ """(name, total phases) from docs/<App>-Phases.md; ('', 0) when the project has no phases."""
150
+ p = os.path.join(root, "docs", f"{app}-Phases.md")
151
+ if not os.path.isfile(p):
152
+ return "", 0
153
+ m = re.search(r"(?ms)^##\s+(?:\d+[.)]\s*)?Phases[^\n]*\n(.*?)(?=^## |\Z)", read(p))
154
+ name, total = "", 0
155
+ for line in (m.group(1) if m else "").splitlines():
156
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
157
+ if len(cells) >= 2 and cells[0].strip("`* ").isdigit():
158
+ total += 1
159
+ if int(cells[0].strip("`* ")) == phase:
160
+ name = cells[1].strip("`* ")
161
+ return name, total
162
+
163
+
164
+ def next_command(app, rows, log_rows, phase=1):
165
+ """Returns (phase, qualifier, cc_line, oc_line, reason)."""
166
+ def ids(lst):
167
+ return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
168
+
169
+ if not rows:
170
+ if os.path.isfile(os.path.join(os.getcwd(), "docs", f"{phase_prefix(app, phase)}BRD.md")):
171
+ return ("Day-1", "stage 1 done, awaiting owner review",
172
+ f"{CC}analyst *day1-greenfield {app} --stage2", f"{OC}flow-analyst *day1-greenfield {app} --stage2",
173
+ "the BRD exists and no checklist; stage 2 runs on the owner's go-ahead")
174
+ return ("Day-1", "no documents yet",
175
+ f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
176
+ "no BRD and no checklist exist; day-1 stage 1 has not run")
177
+
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)}")
189
+ 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}",
193
+ 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)}")
195
+ if owner:
196
+ if handoff_ran(log_rows):
197
+ line = f"(owner-run) docs/{app}-UsageGuide.md — {ids(owner)}"
198
+ return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
199
+ f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
200
+ return ("Handoff", f"{len(owner)} owner-run, {q}",
201
+ f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
202
+ f"{len(owner)} rows are Owner-UAT and handoff has not run yet")
203
+ if handoff_ran(log_rows):
204
+ line = "(owner) set current_phase to Released after UAT — no agent command"
205
+ return ("UAT", f"handoff done, {q}", line, line,
206
+ "every row is terminal and handoff has run; waiting on the owner")
207
+ return ("Handoff", q,
208
+ 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")
210
+
211
+
212
+ def main(argv):
213
+ if len(argv) < 2 or argv[1] in ("-h", "--help"):
214
+ print(__doc__)
215
+ return 0
216
+ app = argv[1]
217
+ args = argv[2:]
218
+ root = os.getcwd()
219
+ phase_n = None
220
+ if "--phase" in args:
221
+ i = args.index("--phase")
222
+ if i + 1 >= len(args) or not args[i + 1].isdigit() or int(args[i + 1]) < 1:
223
+ die("--phase needs a number from 1 upwards")
224
+ phase_n = int(args[i + 1])
225
+ del args[i:i + 2]
226
+ phase_n = phase_n or config_phase(root)
227
+ label = args[0] if args else "status"
228
+ today = datetime.date.today().isoformat()
229
+
230
+ cl_rel = f"docs/{phase_prefix(app, phase_n)}Checklist.md"
231
+ cl_path = os.path.join(root, cl_rel)
232
+ rows = checklist_rows(read(cl_path)) if os.path.isfile(cl_path) else []
233
+ st_path = os.path.join(root, "PROJECT-STATUS.md")
234
+ st_text = read(st_path) if os.path.isfile(st_path) else ""
235
+ log_rows = existing_log_rows(st_text)
236
+ vdate, vresult = last_verify(root, app)
237
+ phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n)
238
+ pname, ptotal = phase_row(root, app, phase_n)
239
+ if ptotal or phase_n > 1:
240
+ tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
241
+ phase = f"{tag} · {phase}"
242
+ reason = f"{reason}; working {cl_rel}"
243
+
244
+ total = len(rows)
245
+ closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
246
+ counts = {}
247
+ for r in rows:
248
+ counts[r["status"]] = counts.get(r["status"], 0) + 1
249
+ open_rows = sorted((r for r in rows if r["status"].lower() not in TERMINAL),
250
+ key=lambda r: (ladder_rank(r["status"]), r["id"]))
251
+
252
+ print(f"# tf-status-facts — {app} — {today} — copy into PROJECT-STATUS.md (Write tool)")
253
+ print()
254
+ print("## Frontmatter values")
255
+ print(f"last_updated: {today}")
256
+ print(f"current_phase: {phase} — {qual}")
257
+ print(f"last_verified_build: {vresult}")
258
+ print(f"last_verified_date: {vdate or 'never'}")
259
+ print()
260
+ print("## Next command to run")
261
+ print(f"Claude Code:\n```\n{cc}\n```\nOpenCode:\n```\n{oc}\n```")
262
+ print(f"Why: {reason}.")
263
+ print()
264
+ print("## Open requirements")
265
+ print("| Status | Count |\n|---|---|")
266
+ shown = set()
267
+ for s in COUNT_ROWS:
268
+ print(f"| {s} | {counts.get(s, 0)} |")
269
+ shown.add(s)
270
+ for s, n in sorted(counts.items(), key=lambda kv: ladder_rank(kv[0])):
271
+ if s not in shown and s.lower() not in TERMINAL:
272
+ print(f"| {s} | {n} |")
273
+ print()
274
+ if open_rows:
275
+ for r in open_rows[:10]:
276
+ print(f"- [ ] {r['id']} — {r['name']} ({r['status']})")
277
+ if len(open_rows) > 10:
278
+ print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
279
+ else:
280
+ print("- None")
281
+ print()
282
+ print("## Verification log")
283
+ print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
284
+ print()
285
+ print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
286
+ new_row = f"| {today} | {label} | {closed}/{total} Verified | {cl_rel}#requirements-status |"
287
+ for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
288
+ print(r)
289
+ print(new_row)
290
+ print()
291
+ print("## Library feedback summary")
292
+ for line in feedback_lines(root, app):
293
+ print(line)
294
+ print()
295
+ print("## You write these yourself")
296
+ 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 ""))
297
+ print("- Known blockers: one line each, or `- None`.")
298
+ print("- Standards compliance: `- Last check {date}: {n} findings, see the checklist Remarks.`")
299
+ print("- Deferred / future: parked ideas, one line each.")
300
+ return 0
301
+
302
+
303
+ if __name__ == "__main__":
304
+ try:
305
+ sys.exit(main(sys.argv))
306
+ except SystemExit:
307
+ raise
308
+ except Exception as e: # never a stack trace for the agent to wade through
309
+ 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" "$@"