@techierathore/techieflow 1.0.1 → 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 (178) 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 +27 -7
  151. package/opencode.jsonc +13 -33
  152. package/package.json +4 -7
  153. package/scripts/install.mjs +73 -46
  154. package/scripts/npm-cleanup.mjs +137 -0
  155. package/scripts/npm-postinstall.mjs +49 -0
  156. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  157. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  158. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  159. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  160. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  161. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  162. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  163. package/.codex/config.toml +0 -46
  164. package/.codex/hooks.json +0 -63
  165. package/.codex/rules/techieflow.rules +0 -8
  166. package/.tfcore/hooks/codex-adapter.py +0 -248
  167. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  168. package/.tfcore/tasks/author-brd.md +0 -79
  169. package/.tfcore/tasks/document-project.md +0 -343
  170. package/.tfcore/tasks/execute-checklist.md +0 -86
  171. package/.tfcore/tasks/index-docs.md +0 -173
  172. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  173. package/.tfcore/tasks/shard-doc.md +0 -185
  174. package/.tfcore/utils/tf-codex-bind.py +0 -196
  175. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  176. package/WORKFLOW.html +0 -1986
  177. package/scripts/test-install.mjs +0 -505
  178. package/scripts/validate.mjs +0 -163
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """tf-perf-grade.py — grade a tf-perf.sh measurement against a row's budget (see tf-perf-grade.sh).
3
+
4
+ bash .tfcore/utils/tf-perf-grade.sh --budget "p95 ttfb <= 500ms @ concurrency 50" \
5
+ --json tests/.artifacts/verify/perf/REQ-NFR-001.json [--json-out <file>]
6
+
7
+ Prints one line: PERF-OK | PERF-MARGINAL (over the budget by up to a quarter: a warning, never a
8
+ demotion) | PERF-FAIL (slower than budget × 1.25, or the app shed a tenth or more of the requests
9
+ at that concurrency) | PERF-UNMEASURED with the reason (unreachable, auth wall, non-200 answers,
10
+ errors during the run, a Debug build, fewer than 20 samples). It reads the budget, never invents
11
+ one. Exit 0 OK or MARGINAL · 1 FAIL · 2 UNMEASURED · 3 usage.
12
+ """
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+
18
+ BUDGET = re.compile(r"(p50|p95|max)\s+(ttfb|load)\s*<=\s*(\d+)\s*ms(?:\s*@\s*concurrency\s+(\d+))?", re.I)
19
+
20
+
21
+ def grade(budget, data):
22
+ """-> dict(verdict, reason, metric, percentile, measured, budget, concurrency, failure_class)"""
23
+ m = BUDGET.search(budget or "")
24
+ if not m:
25
+ return {"verdict": "PERF-UNMEASURED", "reason": f"budget line not in the fixed form: {budget!r}"}
26
+ pct, metric, limit, conc = m.group(1).lower(), m.group(2).lower(), int(m.group(3)), int(m.group(4) or 1)
27
+ out = {"metric": metric, "percentile": pct, "budget": limit, "concurrency": conc, "measured": None, "failure_class": None}
28
+ st = data.get("status")
29
+ if st == "unreachable":
30
+ return dict(out, verdict="PERF-UNMEASURED", reason="the app was unreachable (a build check problem, not a speed result)")
31
+ if st == "redirected":
32
+ return dict(out, verdict="PERF-UNMEASURED", reason="auth wall: every request was redirected; present a session with --cookie or --header")
33
+ levels = data.get("levels") or []
34
+ if not levels:
35
+ return dict(out, verdict="PERF-UNMEASURED", reason="no measurement level in the file")
36
+ lvl = next((l for l in levels if l.get("concurrency") == conc), None)
37
+ note = ""
38
+ if lvl is None:
39
+ lvl = levels[0]
40
+ note = f" (measured at concurrency {lvl.get('concurrency')}, the budget names {conc})"
41
+ err_rate = lvl.get("error_rate") or 0
42
+ if err_rate >= 0.10:
43
+ surv = (lvl.get(f"{metric}_ms") or {}).get(pct)
44
+ return dict(out, verdict="PERF-FAIL", failure_class="timeout", measured=surv,
45
+ reason=f"{lvl.get('errors')} of {lvl.get('samples', 0) + lvl.get('errors', 0)} requests failed at concurrency {lvl.get('concurrency')}: the budget is not servable; survivors' {pct} {metric} {surv} ms")
46
+ if lvl.get("non_200"):
47
+ return dict(out, verdict="PERF-UNMEASURED", reason=f"non-200 answers ({', '.join(lvl['non_200'][:3])}); a wrong path or a correctness failure, not a speed result")
48
+ if (lvl.get("redirect_rate") or 0) > 0:
49
+ return dict(out, verdict="PERF-UNMEASURED", reason="some requests were redirected (auth wall); present a session and re-measure")
50
+ if err_rate > 0:
51
+ return dict(out, verdict="PERF-UNMEASURED", reason=f"errors during the run (rate {err_rate}); re-run when quiet")
52
+ if str(data.get("build_config", "unknown")) != "Release":
53
+ return dict(out, verdict="PERF-UNMEASURED", reason=f"build is {data.get('build_config', 'unknown')}, not Release; a Debug number is not evidence")
54
+ val = (lvl.get(f"{metric}_ms") or {}).get(pct)
55
+ if val is None:
56
+ return dict(out, verdict="PERF-UNMEASURED", reason=f"no {pct} {metric} figure in the file")
57
+ out["measured"] = val
58
+ if lvl.get("weak"):
59
+ return dict(out, verdict="PERF-UNMEASURED", reason=f"weak sample ({lvl.get('samples')} requests behind the {pct}); raise --requests and re-run")
60
+ if val <= limit:
61
+ return dict(out, verdict="PERF-OK", reason=f"{pct} {metric} {val} ms within {limit} ms @ concurrency {lvl.get('concurrency')}{note}")
62
+ if val <= limit * 1.25:
63
+ return dict(out, verdict="PERF-MARGINAL", reason=f"{pct} {metric} {val} ms vs budget {limit} ms @ concurrency {lvl.get('concurrency')} (marginal){note}")
64
+ return dict(out, verdict="PERF-FAIL", failure_class="slow-ttfb" if metric == "ttfb" else "slow-load",
65
+ reason=f"{pct} {metric} {val} ms vs budget {limit} ms @ concurrency {lvl.get('concurrency')}{note}")
66
+
67
+
68
+ def main(argv):
69
+ budget = jpath = out = None
70
+ i = 1
71
+ while i < len(argv):
72
+ if argv[i] == "--budget" and i + 1 < len(argv):
73
+ budget = argv[i + 1]; i += 2
74
+ elif argv[i] == "--json" and i + 1 < len(argv):
75
+ jpath = argv[i + 1]; i += 2
76
+ elif argv[i] == "--json-out" and i + 1 < len(argv):
77
+ out = argv[i + 1]; i += 2
78
+ else:
79
+ print(__doc__); return 3
80
+ if not budget or not jpath:
81
+ print(__doc__); return 3
82
+ if not os.path.isfile(jpath):
83
+ print(f"PERF-UNMEASURED reason=measurement file {jpath} does not exist"); return 2
84
+ try:
85
+ data = json.load(open(jpath, encoding="utf-8"))
86
+ except Exception as e:
87
+ print(f"PERF-UNMEASURED reason=measurement file unreadable: {e}"); return 2
88
+ g = grade(budget, data)
89
+ if out:
90
+ os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
91
+ json.dump(g, open(out, "w"), indent=1)
92
+ print(f"{g['verdict']} reason={g['reason']}")
93
+ return {"PERF-OK": 0, "PERF-MARGINAL": 0, "PERF-FAIL": 1}.get(g["verdict"], 2)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ sys.exit(main(sys.argv))
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # tf-perf-grade.sh — grade a tf-perf.sh measurement against a row's declared budget (Sitting 4c, 2026-09-06).
3
+ #
4
+ # bash .tfcore/utils/tf-perf-grade.sh --budget "p95 ttfb <= 500ms @ concurrency 50" \
5
+ # --json tests/.artifacts/verify/perf/REQ-NFR-001.json [--json-out <file>]
6
+ #
7
+ # Prints PERF-OK, PERF-MARGINAL, PERF-FAIL or PERF-UNMEASURED with the reason. Never invents a budget.
8
+ # Exit 0 OK or MARGINAL · 1 FAIL · 2 UNMEASURED · 3 usage. Details: tf-perf-grade.py.
9
+ set -u
10
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ command -v python3 >/dev/null 2>&1 || { echo "PERF-UNMEASURED reason=python3 is required"; exit 2; }
12
+ exec python3 "$HERE/tf-perf-grade.py" "$@"
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bash
2
+ # tf-phase.sh — mark which TechieFlow command is running (Sitting 4b, 2026-09-05).
3
+ #
4
+ # bash .tfcore/utils/tf-phase.sh start <command> [App] # step 0 of every task: prints the start
5
+ # # time and writes the marker
6
+ # bash .tfcore/utils/tf-phase.sh show # prints the marker, or "none"
7
+ # bash .tfcore/utils/tf-phase.sh end # removes the marker
8
+ # bash .tfcore/utils/tf-phase.sh goal [App] # tf-goal.sh only: an unclaimed marker the first
9
+ # # command's `start` claims, keeping its started
10
+ #
11
+ # The marker is .tfcore/.session/phase.json:
12
+ # {"cmd":"build-phase","app":"MyApp","started":"2026-09-05T16:19:38Z"}
13
+ # Who reads it:
14
+ # - .tfcore/hooks/guard-db.sh: a migration runner is allowed only while the marker says
15
+ # build-phase or fix-issues (MISS-TechieFlow-20260905-09: day-1 patched a database).
16
+ # - tf-emit.sh: a run record that leaves `started` out gets it from here, so the start
17
+ # time is a measurement, never reconstructed.
18
+ # A marker older than 24 h is ignored by the readers (a killed session must not keep a
19
+ # grant alive). The next `start` overwrites; `end` is optional tidiness.
20
+ # Never committed: .tfcore/.session/ is git-ignored. Exit 0 always except a usage error (2).
21
+ set -u
22
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
23
+ DIR="$ROOT/.tfcore/.session"
24
+ FILE="$DIR/phase.json"
25
+ case "${1:-}" in
26
+ start)
27
+ CMD="${2:-}"
28
+ [[ -z "$CMD" ]] && { echo "tf-phase: usage: tf-phase.sh start <command> [App]" >&2; exit 2; }
29
+ CMD="${CMD#\*}"
30
+ APP="${3:-}"
31
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
32
+ mkdir -p "$DIR"
33
+ # A supervisor (tf-goal.sh) writes an unclaimed marker {"cmd":"goal",…} when its first cycle
34
+ # starts. The first command of that run claims it and KEEPS its started: the run began when
35
+ # the harness began reading, not when the agent got round to step 0 (MISS-TechieFlow-20260905-20:
36
+ # a 38-minute stage 2 recorded a 27-second run because step 0 ran last).
37
+ if [[ -f "$FILE" ]] && grep -q '"cmd":"goal"' "$FILE" && [[ -z "$(find "$FILE" -mmin +1440 2>/dev/null)" ]]; then
38
+ NOW="$(sed -n 's/.*"started":"\([^"]*\)".*/\1/p' "$FILE")"
39
+ echo "tf-phase: started taken from the supervisor's marker ($NOW)" >&2
40
+ fi
41
+ printf '{"cmd":"%s","app":"%s","started":"%s"}\n' "$CMD" "$APP" "$NOW" > "$FILE"
42
+ echo "$NOW"
43
+ echo "tf-phase: $CMD${APP:+ $APP} started $NOW (marker .tfcore/.session/phase.json)" >&2
44
+ # The document-check baseline (Sitting 4c, 2026-09-06): the findings the checklists and the
45
+ # status file already carry when the command starts are recorded, so the gate and the Stop
46
+ # hook block only on findings this command introduces (Schemas §7.1 decision 8: old projects
47
+ # are repaired through *amend-docs, never by whichever command happens to run next).
48
+ if [[ -x "$ROOT/.tfcore/utils/tf-doc-check.sh" || -f "$ROOT/.tfcore/utils/tf-doc-check.sh" ]]; then
49
+ docs=(); for f in "$ROOT"/docs/*-Checklist.md "$ROOT"/PROJECT-STATUS.md; do [[ -f "$f" ]] && docs+=("$f"); done
50
+ if [[ ${#docs[@]} -gt 0 ]]; then
51
+ ( cd "$ROOT" && bash .tfcore/utils/tf-doc-check.sh --root "$ROOT" --baseline-write "${docs[@]}" 2>/dev/null | tail -1 >&2 ) || true
52
+ fi
53
+ fi
54
+ ;;
55
+ goal)
56
+ # written by tf-goal.sh at the start of a run's first cycle; claimed by the first `start`
57
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
58
+ mkdir -p "$DIR"
59
+ printf '{"cmd":"goal","app":"%s","started":"%s"}\n' "${2:-}" "$NOW" > "$FILE"
60
+ echo "$NOW"
61
+ ;;
62
+ show)
63
+ if [[ -f "$FILE" ]]; then cat "$FILE"; else echo "none"; fi
64
+ ;;
65
+ end)
66
+ rm -f "$FILE"
67
+ echo "tf-phase: marker removed"
68
+ ;;
69
+ *)
70
+ sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
71
+ exit 2
72
+ ;;
73
+ esac
74
+ exit 0
@@ -16,9 +16,6 @@
16
16
  # model-only agent entries merge, command
17
17
  # entries must be complete → they carry
18
18
  # template+description+model)
19
- # Codex:
20
- # .codex/agents/*.toml + .agents/skills/techieflow-*/SKILL.md, generated
21
- # by tf-codex-bind.py. Subagent model/effort is pinned when routing is on;
22
19
  # skills remain thin loaders and do not claim to switch the main thread.
23
20
  #
24
21
  # When `enabled: false` (the default) every generated artifact is REMOVED, using
@@ -109,9 +106,18 @@ def write(relpath, content):
109
106
 
110
107
  MARK = "<!-- generated by tf-routing-bind.sh from .tfcore/routing.yaml — do not edit; edit routing.yaml and re-run -->"
111
108
 
109
+ # A phase whose task file no longer ships gets no binding, with a printed warning: the seven
110
+ # never-used commands were removed in Sitting 4c (2026-09-06) and routing.yaml is preserved per
111
+ # project, so a stale line there must never write a file reference OpenCode refuses to load
112
+ # (MISS-TechieFlow-20260906-27: OpenCode would not start on TechieBlog-oc).
113
+ for _ph in sorted(cfg["phases"]):
114
+ if not os.path.isfile(os.path.join(root, ".tfcore", "tasks", _ph + ".md")):
115
+ print("tf-routing-bind: phase %s has no task file .tfcore/tasks/%s.md; no binding written (remove the line from routing.yaml)" % (_ph, _ph))
116
+ del cfg["phases"][_ph]
117
+
112
118
  # --- Claude Code: routed wrapper commands ---------------------------------
113
119
  OWNER = {"day1-greenfield": "analyst", "day1-brownfield": "analyst",
114
- "author-brd": "analyst", "amend-docs": "analyst", "mockups": "analyst",
120
+ "amend-docs": "analyst", "mockups": "analyst",
115
121
  "split-brd": "analyst", "verify-phase": "verifier"}
116
122
  for phase, t in sorted(cfg["phases"].items()):
117
123
  m = model_for(t, "claude")
@@ -212,5 +218,4 @@ with open(manifest_path, "w", encoding="utf-8", newline="\n") as fh:
212
218
  print("tf-routing-bind: routing enabled — %d artifact(s) generated (%d stale removed)"
213
219
  % (len(generated), removed))
214
220
  PY
215
- python3 "$SELF_DIR/tf-codex-bind.py" "$ROOT" || echo "tf-routing-bind: warning: Codex bindings were not refreshed" >&2
216
221
  exit 0
@@ -8,7 +8,7 @@
8
8
  # bash .tfcore/utils/tf-routing.sh set-model <tier> <harness> <model>
9
9
  # e.g. set-model economy opencode opencode-go/deepseek-v4-flash
10
10
  # e.g. set-model frontier claude opus
11
- # e.g. set-model standard codex gpt-5.6-terra
11
+ # e.g. set-model standard opencode openai/gpt-5.6-terra
12
12
  # bash .tfcore/utils/tf-routing.sh bind re-generate bindings after editing routing.yaml by hand
13
13
  # bash .tfcore/utils/tf-routing.sh set-escalation <phase> <attempts> <tier>
14
14
  # e.g. set-escalation fix-issues 2 frontier
@@ -90,12 +90,12 @@ PY
90
90
  set-model)
91
91
  TIER="${2:-}"; HARNESS="${3:-}"; MODEL="${4:-}"
92
92
  case "$TIER" in frontier|standard|economy) ;; *)
93
- echo "usage: tf-routing.sh set-model <frontier|standard|economy> <claude|opencode|codex> <model-id>" >&2; exit 2 ;;
93
+ echo "usage: tf-routing.sh set-model <frontier|standard|economy> <claude|opencode> <model-id>" >&2; exit 2 ;;
94
94
  esac
95
- case "$HARNESS" in claude|opencode|codex) ;; *)
96
- echo "harness must be claude, opencode, or codex" >&2; exit 2 ;;
95
+ case "$HARNESS" in claude|opencode) ;; *)
96
+ echo "harness must be claude or opencode" >&2; exit 2 ;;
97
97
  esac
98
- [[ -n "$MODEL" ]] || { echo "usage: tf-routing.sh set-model <tier> <claude|opencode|codex> <model-id>" >&2; exit 2; }
98
+ [[ -n "$MODEL" ]] || { echo "usage: tf-routing.sh set-model <tier> <claude|opencode> <model-id>" >&2; exit 2; }
99
99
  python3 - "$RY" "$TIER" "$HARNESS" "$MODEL" <<'PY'
100
100
  import sys
101
101
  p, tier, harness, model = sys.argv[1:5]
@@ -199,7 +199,7 @@ print()
199
199
  print("Tier models:")
200
200
  for t in ("frontier", "standard", "economy"):
201
201
  m = cfg["tiers"].get(t, {})
202
- print(" %-9s claude: %-10s opencode: %-36s codex: %s" % (t, m.get("claude", "-"), m.get("opencode", "-"), m.get("codex", "-")))
202
+ print(" %-9s claude: %-10s opencode: %s" % (t, m.get("claude", "-"), m.get("opencode", "-")))
203
203
  print()
204
204
  print("Phases by tier:")
205
205
  for t in ("frontier", "standard", "economy", "inherit"):
@@ -226,7 +226,6 @@ if cfg["enabled"]:
226
226
  print("Invoke routed phases as:")
227
227
  print(" OpenCode: /techieflow:tasks:<phase> (same commands as always — model now pinned)")
228
228
  print(" Claude Code: /tf:<phase> (new wrappers; old commands still work, unrouted)")
229
- print(" Codex: $techieflow-<phase> (main-thread model inherited; delegated roles are pinned)")
230
229
  print()
231
230
  print("WHERE YOU SEE IT — opening the TUI looks UNCHANGED on purpose: your normal chat")
232
231
  print("(the default 'build' agent) stays on YOUR selected model. Routing becomes visible when:")
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env python3
2
+ """tf-split-brd.py — draft docs/<App>-Checklist.md from the BRD ledger (see tf-split-brd.sh).
3
+
4
+ bash .tfcore/utils/tf-split-brd.sh <App> # write the checklist (refuses if one exists)
5
+ bash .tfcore/utils/tf-split-brd.sh <App> --force # overwrite an existing checklist
6
+ bash .tfcore/utils/tf-split-brd.sh <App> --add-missing # append rows only for BRD items with no row yet
7
+ bash .tfcore/utils/tf-split-brd.sh <App> --phase 2 # a later phase of a Large project
8
+ bash .tfcore/utils/tf-split-brd.sh <App> --all-phases # every phase in docs/<App>-Phases.md
9
+
10
+ Large projects (docs/TechieFlow-Document-Schemas.md §2): phase 1 is docs/<App>-BRD.md and
11
+ docs/<App>-Checklist.md; phase n is docs/<App>-Pn-BRD.md and docs/<App>-Pn-Checklist.md. Without
12
+ --phase the phase comes from appPhase in .tfcore/core-config.yaml (1 when absent). REQ numbers run
13
+ on across phases: a new checklist starts each prefix after the highest number in every other
14
+ phase's checklist, so an id is never reused.
15
+
16
+ Reads docs/<App>-BRD.md: the header (App, Size), the Screens and flow table (screen, route),
17
+ every `**BRD-N**` ledger item with its title, screen, mockup link and acceptance line, and
18
+ the Non-functional table. Writes the checklist in the template shape: header, Goal, the one
19
+ Requirements Status table, then one section per screen holding the detail entries, plus
20
+ "RAG / AI requirements" and "Non-functional". Acceptance and perf-budget lines are copied
21
+ verbatim; an item without one gets a TODO line the checker will refuse, never an invented one.
22
+
23
+ Class is guessed, and the agent corrects it: Non-functional table -> REQ-NFR; text about
24
+ embeddings, vectors, prompts, chat or a model -> REQ-RAG; text about what a screen shows
25
+ (page, screen, list, form, dialog, button, menu, layout, theme, displays, shows) -> REQ-UI;
26
+ everything else -> REQ-FN. Exit 0 written, 1 refused, 2 could not run.
27
+ """
28
+ import os
29
+ import re
30
+ import sys
31
+
32
+ RAG_WORDS = re.compile(r"\b(embedding|embeddings|vector|rag\b|prompt template|llm|language model|retriev|semantic search|chatbot|chat completion)", re.I)
33
+ UI_WORDS = re.compile(r"\b(page|screen|list|lists|form|dialog|button|menu|layout|theme|display|displays|shows|shown|grid|table|card|panel|tab|tabs|badge|icon|responsive|mobile)\b", re.I)
34
+ STATUS_LINE = ("**Status values:** `Not Started` · `In Progress` · `Implemented` · `Verified` · `Done (pre-existing)` · "
35
+ "`Needs re-verify` · `PARTIAL` · `FAIL` · `Blocked` · `Owner-UAT` · `N/A`.")
36
+
37
+
38
+ def die(msg, code=2):
39
+ print(f"tf-split-brd: {msg}", file=sys.stderr)
40
+ sys.exit(code)
41
+
42
+
43
+ def read(p):
44
+ with open(p, encoding="utf-8", errors="replace") as f:
45
+ return f.read()
46
+
47
+
48
+ def header_field(text, name):
49
+ m = re.search(rf"(?im)^\|\s*{re.escape(name)}\s*\|\s*([^|]+?)\s*\|", text)
50
+ return m.group(1).strip() if m else ""
51
+
52
+
53
+ def section(text, name):
54
+ m = re.search(rf"(?ms)^##\s+(?:\d+[.)]\s*)?{name}[^\n]*\n(.*?)(?=^## |\Z)", text)
55
+ return m.group(1) if m else ""
56
+
57
+
58
+ def screens(text):
59
+ """[(name, route)] from the Screens and flow table; dialog rows are skipped."""
60
+ out = []
61
+ for line in section(text, "Screens and flow").splitlines():
62
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
63
+ if len(cells) < 2 or cells[0].lower() in ("screen", "") or set(cells[0]) <= set("-: "):
64
+ continue
65
+ route = cells[1].strip("` ")
66
+ if route.lower().startswith("on "):
67
+ continue
68
+ out.append((cells[0].strip("`* "), route))
69
+ return out
70
+
71
+
72
+ def ledger(text):
73
+ """Every **BRD-N** item: id, title, screen, mockup, acceptance, perf, section name."""
74
+ items = []
75
+ nfr_ids = set()
76
+ for line in section(text, "Non-functional requirements").splitlines():
77
+ m = re.match(r"^\s*\|\s*`?\**(BRD-\d+)", line)
78
+ if m:
79
+ nfr_ids.add(m.group(1))
80
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
81
+ title = cells[2] if len(cells) > 2 else cells[-1]
82
+ measure = cells[3] if len(cells) > 3 else ""
83
+ items.append({"id": m.group(1), "title": title.strip("`* "), "screen": "", "mockup": "",
84
+ "acceptance": "", "perf": measure if "perf-budget" in measure.lower() else "", "nfr": True})
85
+ lines = text.splitlines()
86
+ i = 0
87
+ while i < len(lines):
88
+ m = re.match(r"^\s*[-*]\s*\*\*(BRD-\d+)\*\*\s*[—:-]+\s*(.*)$", lines[i])
89
+ if m and m.group(1) not in nfr_ids:
90
+ rid, rest = m.group(1), m.group(2)
91
+ title = re.split(r"\s*(?:\.\s+\*|\s\*Screen|\s·\s\*|\s\*Mockup)", rest, 1)[0].strip().rstrip(".")
92
+ sm = re.search(r"\*Screen:\*\s*([^·*\n]+)", rest)
93
+ mm = re.search(r"\(((?:\./|docs/)?mockups/[^)\s]+)\)", rest) or re.search(r"((?:docs/)?mockups/[\w./-]+\.html)", rest)
94
+ acc = perf = ""
95
+ j = i + 1
96
+ while j < len(lines) and (lines[j].startswith(" ") or lines[j].strip() == ""):
97
+ if re.search(r"\*Acceptance:?\*|^\s*[-*]\s*Acceptance:", lines[j], re.I):
98
+ acc = re.sub(r"^\s*[-*]\s*\*?Acceptance:?\*?:?\s*", "", lines[j]).strip()
99
+ pm = re.search(r"perf-budget:[^\n|]+", lines[j], re.I)
100
+ if pm:
101
+ perf = pm.group(0).strip()
102
+ if lines[j].strip() == "" and j + 1 < len(lines) and not lines[j + 1].startswith(" "):
103
+ break
104
+ j += 1
105
+ items.append({"id": rid, "title": title, "screen": sm.group(1).strip() if sm else "",
106
+ "mockup": mm.group(1) if mm else "", "acceptance": acc, "perf": perf, "nfr": False})
107
+ i += 1
108
+ return [it for it in items if not it["nfr"]] + [it for it in items if it["nfr"]]
109
+
110
+
111
+ def classify(it):
112
+ if it["nfr"]:
113
+ return "NFR"
114
+ blob = it["title"] + " " + it["acceptance"]
115
+ if RAG_WORDS.search(blob):
116
+ return "RAG"
117
+ if UI_WORDS.search(blob):
118
+ return "UI"
119
+ return "FN"
120
+
121
+
122
+ def slug(s):
123
+ return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
124
+
125
+
126
+ def entry(req, it):
127
+ mock = f" · *Mockup:* {it['mockup'].replace('docs/', '')}" if it["mockup"] else ""
128
+ acc = it["acceptance"] or "TODO — write: When <actor> <does what> on <screen>, then <a result a browser robot can observe>"
129
+ if it["perf"] and it["perf"].lower() not in acc.lower():
130
+ acc = acc.rstrip(".") + "; " + it["perf"]
131
+ return (f'<a id="d-{req.lower()}"></a>\n- **{req}** — {it["title"]}. *BRD:* {it["id"]}{mock}\n'
132
+ f" - *Acceptance:* {acc}\n")
133
+
134
+
135
+ def config_phase():
136
+ """appPhase from .tfcore/core-config.yaml; 1 when absent or null."""
137
+ p = os.path.join(".tfcore", "core-config.yaml")
138
+ if os.path.isfile(p):
139
+ m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
140
+ if m:
141
+ return int(m.group(1))
142
+ return 1
143
+
144
+
145
+ def phase_names(app, phase):
146
+ """(BRD path, checklist path) for a phase: plain names for phase 1, App-Pn- otherwise."""
147
+ pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
148
+ return os.path.join("docs", f"{pfx}BRD.md"), os.path.join("docs", f"{pfx}Checklist.md")
149
+
150
+
151
+ def phases_listed(app):
152
+ """Phase numbers from the Phases table of docs/<App>-Phases.md, in order."""
153
+ p = os.path.join("docs", f"{app}-Phases.md")
154
+ if not os.path.isfile(p):
155
+ die(f"{p} does not exist; --all-phases needs the Phases document")
156
+ nums = []
157
+ for line in section(read(p), "Phases").splitlines():
158
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
159
+ if cells and cells[0].strip("`* ").isdigit():
160
+ nums.append(int(cells[0].strip("`* ")))
161
+ if not nums:
162
+ die(f"{p} has no numbered rows in its Phases table")
163
+ return nums
164
+
165
+
166
+ def other_phase_counters(app, own_cl, below=None):
167
+ """Highest REQ number per prefix across the other phases' checklists, so ids run on.
168
+ below=n counts only phases under n (an --all-phases run, which rewrites them in order);
169
+ otherwise every other phase counts, so a single-phase run can never reuse an id."""
170
+ counters = {"UI": 0, "FN": 0, "RAG": 0, "NFR": 0}
171
+ if not os.path.isdir("docs"):
172
+ return counters
173
+ for f in os.listdir("docs"):
174
+ m = re.fullmatch(rf"{re.escape(app)}(?:-P(\d+))?-Checklist\.md", f)
175
+ if not m or os.path.join("docs", f) == own_cl:
176
+ continue
177
+ if below is not None and int(m.group(1) or 1) >= below:
178
+ continue
179
+ for r in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", read(os.path.join("docs", f))):
180
+ counters[r.group(1)] = max(counters[r.group(1)], int(r.group(2)))
181
+ return counters
182
+
183
+
184
+ def main(argv):
185
+ if len(argv) < 2 or argv[1] in ("-h", "--help"):
186
+ print(__doc__)
187
+ return 0
188
+ app = argv[1]
189
+ force = "--force" in argv
190
+ add_missing = "--add-missing" in argv
191
+ phase = None
192
+ if "--phase" in argv:
193
+ i = argv.index("--phase")
194
+ if i + 1 >= len(argv) or not argv[i + 1].isdigit() or int(argv[i + 1]) < 1:
195
+ die("--phase needs a number from 1 upwards")
196
+ phase = int(argv[i + 1])
197
+ if "--all-phases" in argv:
198
+ rc = 0
199
+ for n in phases_listed(app):
200
+ rc = max(rc, run(app, n, force, add_missing, in_order=True))
201
+ return rc
202
+ return run(app, phase if phase else config_phase(), force, add_missing)
203
+
204
+
205
+ def run(app, phase, force, add_missing, in_order=False):
206
+ brd_p, cl_p = phase_names(app, phase)
207
+ if not os.path.isfile(brd_p):
208
+ die(f"{brd_p} does not exist" + (f" (phase {phase}; phase 1 is docs/{app}-BRD.md)" if phase > 1 else ""))
209
+ brd = read(brd_p)
210
+ items = ledger(brd)
211
+ if not items:
212
+ die(f"{brd_p} has no **BRD-N** items in its Requirements section")
213
+ scr = screens(brd)
214
+ routes = {s.lower(): r for s, r in scr}
215
+ size = header_field(brd, "Size") or "Small"
216
+ phase_label = header_field(brd, "Phase")
217
+
218
+ existing = read(cl_p) if os.path.isfile(cl_p) else ""
219
+ if existing and not force and not add_missing:
220
+ die(f"{cl_p} already exists; use --add-missing to append rows for new BRD items, or --force to rewrite", 1)
221
+ mapped = set(re.findall(r"\*BRD:\*\s*(BRD-\d+)", existing)) | set(re.findall(r"\((BRD-\d+)\)", existing))
222
+ counters = other_phase_counters(app, cl_p, below=phase if in_order else None)
223
+ if add_missing:
224
+ for m in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", existing):
225
+ counters[m.group(1)] = max(counters[m.group(1)], int(m.group(2)))
226
+ new_counts = {"UI": 0, "FN": 0, "RAG": 0, "NFR": 0}
227
+
228
+ rows, entries = [], {}
229
+ for it in items:
230
+ if add_missing and it["id"] in mapped:
231
+ continue
232
+ cls = classify(it)
233
+ counters[cls] += 1
234
+ new_counts[cls] += 1
235
+ req = f"REQ-{cls}-{counters[cls]:03d}"
236
+ rows.append(f"| {req} | {it['title']} | Not Started | 0% | — | [view](#d-{req.lower()}) |")
237
+ key = ("Non-functional" if cls == "NFR" else "RAG / AI requirements" if cls == "RAG"
238
+ else (it["screen"] or "Other"))
239
+ entries.setdefault(key, []).append(entry(req, it))
240
+
241
+ if add_missing and existing:
242
+ if not rows:
243
+ print(f"tf-split-brd: every BRD item already has a row in {cl_p}; nothing to add")
244
+ return 0
245
+ text = existing
246
+ m = re.search(r"(?ms)^## Requirements Status\s*\n.*?(?=^## |\Z)", text)
247
+ if not m:
248
+ die(f"{cl_p} has no Requirements Status section")
249
+ tbl_end = m.end()
250
+ # insert rows after the last table row of the status table
251
+ seg = text[m.start():tbl_end]
252
+ last_row = list(re.finditer(r"(?m)^\|\s*REQ-[^\n]*$", seg))
253
+ if last_row:
254
+ pos = m.start() + last_row[-1].end()
255
+ text = text[:pos] + "\n" + "\n".join(rows) + text[pos:]
256
+ for key, ents in entries.items():
257
+ hm = re.search(rf"(?ms)^##\s+(?:Page:\s*)?{re.escape(key)}\b[^\n]*\n.*?(?=^## |\Z)", text)
258
+ block = "\n".join(ents)
259
+ if hm:
260
+ text = text[:hm.end()].rstrip("\n") + "\n\n" + block + "\n" + text[hm.end():]
261
+ else:
262
+ heading = key if key in ("Non-functional", "RAG / AI requirements") else f"Page: {key}" + (f" (`{routes[key.lower()]}`)" if key.lower() in routes else "")
263
+ text = text.rstrip("\n") + f"\n\n## {heading}\n\n{block}\n"
264
+ with open(cl_p, "w", encoding="utf-8", newline="\n") as f:
265
+ f.write(text)
266
+ print(f"tf-split-brd: appended {len(rows)} row(s) to {cl_p}: " + ", ".join(r.split("|")[1].strip() for r in rows))
267
+ return 0
268
+
269
+ goal = re.sub(r"\s+", " ", section(brd, "Summary").strip().split("\n\n")[0]).strip() or f"Build {app} as the BRD describes."
270
+ title = f"# {app} — Checklist" + (f" (phase {phase})" if phase_label else "")
271
+ out = [title, "", "| | |", "|---|---|", f"| App | {app} |", f"| Size | {size} |"]
272
+ if phase_label:
273
+ out.append(f"| Phase | {phase_label} |")
274
+ out += ["", "## Goal", "", goal + (" This checklist is the whole work list of this phase." if phase_label else " This single checklist is the whole work list."), "",
275
+ "## Requirements Status", "",
276
+ "| ID | Requirement | Status | % | Remarks | Details |", "|----|-------------|--------|---|---------|---------|"]
277
+ out += rows
278
+ out += ["", STATUS_LINE, ""]
279
+ ordered = [s for s, _ in scr if s in entries] + [k for k in entries if k not in dict(scr) and k not in ("RAG / AI requirements", "Non-functional")]
280
+ for key in ordered:
281
+ route = routes.get(key.lower(), "")
282
+ out.append(f"## Page: {key}" + (f" (`{route}`)" if route else ""))
283
+ out.append("")
284
+ out.append("\n".join(entries[key]))
285
+ for key in ("RAG / AI requirements", "Non-functional"):
286
+ if key in entries:
287
+ out += [f"## {key}", "", "\n".join(entries[key])]
288
+ with open(cl_p, "w", encoding="utf-8", newline="\n") as f:
289
+ f.write("\n".join(out).rstrip("\n") + "\n")
290
+ n = {k: v for k, v in new_counts.items() if v}
291
+ todo = sum(1 for it in items if not it["acceptance"] and not it["nfr"])
292
+ print(f"tf-split-brd: wrote {cl_p}: {len(rows)} rows (" + ", ".join(f"{k} {v}" for k, v in n.items()) + f"), {len(ordered)} screen section(s)"
293
+ + (f"; {todo} item(s) had no acceptance line and carry a TODO the checker will refuse" if todo else ""))
294
+ print("tf-split-brd: correct any class the guess got wrong, then run: bash .tfcore/utils/tf-doc-check.sh --app " + app)
295
+ return 0
296
+
297
+
298
+ if __name__ == "__main__":
299
+ try:
300
+ sys.exit(main(sys.argv))
301
+ except SystemExit:
302
+ raise
303
+ except Exception as e:
304
+ die(str(e))
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # tf-split-brd.sh — draft docs/<App>-Checklist.md from the BRD ledger.
3
+ #
4
+ # bash .tfcore/utils/tf-split-brd.sh MyApp # write it (refuses if one exists)
5
+ # bash .tfcore/utils/tf-split-brd.sh MyApp --force # rewrite
6
+ # bash .tfcore/utils/tf-split-brd.sh MyApp --add-missing # append rows for BRD items with no row (amend-docs)
7
+ # bash .tfcore/utils/tf-split-brd.sh MyApp --phase 2 # a later phase of a Large project (docs/MyApp-P2-BRD.md)
8
+ # bash .tfcore/utils/tf-split-brd.sh MyApp --all-phases # every phase listed in docs/MyApp-Phases.md
9
+ #
10
+ # Without --phase the phase is appPhase in core-config.yaml (1 when absent).
11
+ # REQ numbers run on across phases; an id is never reused.
12
+ #
13
+ # One row per BRD item in the template shape, acceptance lines copied verbatim,
14
+ # class guessed (UI / FN / RAG / NFR) for the agent to correct. Then run
15
+ # tf-doc-check.sh --app MyApp. Thin wrapper over tf-split-brd.py; Python 3 stdlib only.
16
+ set -uo pipefail
17
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ if ! command -v python3 >/dev/null 2>&1; then
19
+ echo "tf-split-brd: python3 is required (standard library only)." >&2
20
+ exit 2
21
+ fi
22
+ exec python3 "$SELF_DIR/tf-split-brd.py" "$@"