@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.
- package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
- package/.claude/commands/TechieFlow/agents/architect.md +0 -6
- package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +39 -12
- package/.tfcore/agents/analyst.md +0 -5
- package/.tfcore/agents/architect.md +0 -6
- package/.tfcore/agents/flow-master.md +41 -215
- package/.tfcore/agents/verifier.md +23 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +56 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +25 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +20 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +1 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +43 -7
- package/.tfcore/telemetry/tf-metrics.sh +152 -17
- package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +191 -0
- package/.tfcore/utils/tf-build-list.sh +20 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1267 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +203 -13
- package/.tfcore/utils/tf-fix-close.sh +83 -0
- package/.tfcore/utils/tf-goal.sh +193 -62
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +211 -0
- package/.tfcore/utils/tf-misses-md.sh +13 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +74 -0
- package/.tfcore/utils/tf-routing-bind.sh +10 -5
- package/.tfcore/utils/tf-routing.sh +6 -7
- package/.tfcore/utils/tf-split-brd.py +304 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +309 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +283 -0
- package/.tfcore/utils/tf-verify-screens.sh +22 -0
- package/.tfcore/utils/tf-verify-tests.sh +123 -0
- package/.tfcore/utils/tf-verify-verdict.py +283 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +114 -1149
- package/docs/TechieFlow-Installation.md +27 -7
- package/opencode.jsonc +13 -33
- package/package.json +4 -7
- package/scripts/install.mjs +73 -46
- package/scripts/npm-cleanup.mjs +137 -0
- package/scripts/npm-postinstall.mjs +49 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
- package/scripts/test-install.mjs +0 -505
- 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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
96
|
-
echo "harness must be claude
|
|
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
|
|
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:
|
|
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" "$@"
|