@techierathore/techieflow 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -10
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  8. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  9. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  12. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  13. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  14. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
  15. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  16. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  17. package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
  18. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
  19. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  20. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  21. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  22. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  23. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  24. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  25. package/.opencode/plugin/techieflow.js +181 -9
  26. package/.tfcore/agents/analyst.md +5 -8
  27. package/.tfcore/agents/architect.md +1 -10
  28. package/.tfcore/agents/flow-master.md +1 -0
  29. package/.tfcore/agents/verifier.md +1 -0
  30. package/.tfcore/hooks/block-git.sh +4 -2
  31. package/.tfcore/hooks/guard-build.sh +5 -1
  32. package/.tfcore/hooks/guard-db.sh +6 -4
  33. package/.tfcore/hooks/guard-metrics.sh +13 -1
  34. package/.tfcore/hooks/guard-status-html.sh +86 -2
  35. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  36. package/.tfcore/hooks/guard-verify.sh +8 -3
  37. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  38. package/.tfcore/routing.yaml +61 -0
  39. package/.tfcore/standards/owner-words.txt +23 -0
  40. package/.tfcore/tasks/_owner-language.md +27 -0
  41. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  42. package/.tfcore/tasks/_status-update-gate.md +4 -3
  43. package/.tfcore/tasks/amend-docs.md +6 -5
  44. package/.tfcore/tasks/build-phase.md +1 -1
  45. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  46. package/.tfcore/tasks/create-doc.md +4 -0
  47. package/.tfcore/tasks/create-project-brief.md +22 -0
  48. package/.tfcore/tasks/day1-brownfield.md +3 -3
  49. package/.tfcore/tasks/day1-greenfield.md +3 -3
  50. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  51. package/.tfcore/tasks/fix-issues.md +2 -2
  52. package/.tfcore/tasks/generate-html.md +7 -3
  53. package/.tfcore/tasks/handoff-phase.md +20 -18
  54. package/.tfcore/tasks/metrics-report.md +2 -0
  55. package/.tfcore/tasks/mockups.md +1 -1
  56. package/.tfcore/tasks/productguide.md +2 -2
  57. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  58. package/.tfcore/tasks/triage-issues.md +2 -2
  59. package/.tfcore/tasks/verify-phase.md +1 -1
  60. package/.tfcore/telemetry/SCHEMA.md +61 -1
  61. package/.tfcore/telemetry/install-metrics.sh +3 -2
  62. package/.tfcore/telemetry/tf-metrics.sh +435 -21
  63. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  64. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  65. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  66. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  67. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  68. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  69. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  70. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  71. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  72. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  73. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  74. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  75. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  76. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  77. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  78. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  79. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  80. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  81. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  82. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  83. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  84. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  90. package/.tfcore/utils/tf-assets.sh +97 -12
  91. package/.tfcore/utils/tf-brd-status.py +14 -5
  92. package/.tfcore/utils/tf-build-list.py +99 -11
  93. package/.tfcore/utils/tf-build-list.sh +5 -3
  94. package/.tfcore/utils/tf-build.sh +88 -15
  95. package/.tfcore/utils/tf-ci-repro.py +685 -0
  96. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  97. package/.tfcore/utils/tf-devguide-list.py +47 -22
  98. package/.tfcore/utils/tf-doc-check.py +242 -10
  99. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  100. package/.tfcore/utils/tf-emit.sh +413 -19
  101. package/.tfcore/utils/tf-feedback.sh +13 -0
  102. package/.tfcore/utils/tf-fix-close.sh +113 -31
  103. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  104. package/.tfcore/utils/tf-goal.sh +105 -18
  105. package/.tfcore/utils/tf-harness.sh +9 -4
  106. package/.tfcore/utils/tf-lock.sh +29 -0
  107. package/.tfcore/utils/tf-log-miss.py +34 -8
  108. package/.tfcore/utils/tf-login.mjs +167 -0
  109. package/.tfcore/utils/tf-misses-md.py +24 -21
  110. package/.tfcore/utils/tf-misses-md.sh +5 -4
  111. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  112. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  113. package/.tfcore/utils/tf-model-pick.sh +475 -0
  114. package/.tfcore/utils/tf-owner-text.py +233 -0
  115. package/.tfcore/utils/tf-owner-text.sh +14 -0
  116. package/.tfcore/utils/tf-phase.sh +33 -1
  117. package/.tfcore/utils/tf-portable.sh +230 -0
  118. package/.tfcore/utils/tf-render-html.py +80 -2
  119. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  120. package/.tfcore/utils/tf-routing.sh +7 -0
  121. package/.tfcore/utils/tf-selfcheck.sh +356 -0
  122. package/.tfcore/utils/tf-split-brd.py +17 -2
  123. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  124. package/.tfcore/utils/tf-status-facts.py +133 -44
  125. package/.tfcore/utils/tf-triage.py +62 -16
  126. package/.tfcore/utils/tf-triage.sh +2 -0
  127. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  128. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  129. package/.tfcore/utils/tf-verify-env.sh +33 -4
  130. package/.tfcore/utils/tf-verify-list.py +75 -8
  131. package/.tfcore/utils/tf-verify-screens.mjs +254 -43
  132. package/.tfcore/utils/tf-verify-screens.sh +18 -4
  133. package/.tfcore/utils/tf-verify-tests.sh +272 -28
  134. package/.tfcore/utils/tf-verify-verdict.py +59 -11
  135. package/.tfcore/utils/tf_defect.py +27 -0
  136. package/.tfcore/utils/tf_feedback.py +209 -0
  137. package/.tfcore/utils/tf_roadmap.py +35 -0
  138. package/README.md +19 -5
  139. package/docs/TechieFlow-Installation.md +5 -3
  140. package/opencode.jsonc +24 -0
  141. package/package.json +1 -1
  142. package/scripts/install.mjs +4 -0
  143. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  144. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  145. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  146. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  147. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  148. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  149. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  150. package/.tfcore/utils/techieflow-doc-template.md +0 -325
@@ -41,6 +41,63 @@ root = sys.argv[1]
41
41
  ryaml = os.path.join(root, ".tfcore", "routing.yaml")
42
42
  manifest_path = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
43
43
 
44
+ # --- migration: a project's routing.yaml is never overwritten -------------
45
+ # `update-framework.sh` leaves a project's own routing.yaml alone on purpose, so a block added to
46
+ # the framework default after the project was scaffolded would never reach it. Both delivery
47
+ # routes run THIS script, so the migration lives here: a missing block is APPENDED, with the
48
+ # Claude aliases filled in (opus/sonnet/haiku exist on every account) and the OpenCode lines left
49
+ # empty, because guessing a model id for a provider this machine may not have would produce a
50
+ # fallback that fails at the moment it is needed. Existing lines are never touched, and a file
51
+ # that already has the block is left exactly as it is.
52
+ MIGRATIONS = [
53
+ ("fallbacks:", """
54
+ # Which models this tier drops to while its own is limited, in order, comma-separated.
55
+ # Added by tf-routing-bind.sh. The opencode lines are OpenCode Go models
56
+ # (https://opencode.ai/docs/go); change them if this project uses a different provider —
57
+ # `opencode models` prints what this machine can reach.
58
+ # The "limited until <time>" record is kept in this project, at
59
+ # .tfcore/.session/model-cooldown.json. Uncomment to move it (relative to the project root, or
60
+ # absolute); several projects sharing one file learn about a limit from each other.
61
+ # cooldown_file: .tfcore/.session/model-cooldown.json
62
+ # Full explanation: docs/TechieFlow-Routing-Guide.md §9.
63
+ fallbacks:
64
+ frontier:
65
+ claude: opus, haiku
66
+ opencode: opencode-go/glm-5.3, opencode-go/kimi-k3, openai/gpt-5.6-sol
67
+ standard:
68
+ claude: haiku
69
+ opencode: opencode-go/mimo-v2.5, opencode-go/qwen3.8-max, openai/gpt-5.6-luna
70
+ economy:
71
+ claude: sonnet
72
+ opencode: opencode-go/deepseek-v4-flash, opencode-go/glm-5.3-flash, openai/gpt-5.6-luna
73
+ """),
74
+ ("billing:", """
75
+ # How each harness is paid for, so a cost figure knows what it means (SCHEMA.md §2.5b).
76
+ # subscription = a flat fee, marginal dollars are zero; metered = an API key, real money;
77
+ # local = no bill; auto = work it out per model from OpenCode's own sign-in file.
78
+ # Add a provider by name here to override the guess, e.g. " myserver: local".
79
+ billing:
80
+ claude: subscription
81
+ opencode: auto
82
+ """),
83
+ ]
84
+ try:
85
+ _txt = open(ryaml, encoding="utf-8").read()
86
+ _added = []
87
+ for key, block in MIGRATIONS:
88
+ if not re.search(r"^%s\s*$" % re.escape(key), _txt, re.M):
89
+ if not _txt.endswith("\n"):
90
+ _txt += "\n"
91
+ _txt += block
92
+ _added.append(key.rstrip(":"))
93
+ if _added:
94
+ with open(ryaml, "w", encoding="utf-8", newline="\n") as _fh:
95
+ _fh.write(_txt)
96
+ print("tf-routing-bind: added %s to .tfcore/routing.yaml (nothing existing was changed)"
97
+ % " and ".join(_added))
98
+ except FileNotFoundError:
99
+ pass
100
+
44
101
  # --- parse routing.yaml (flat format — see its header) --------------------
45
102
  cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}}
46
103
  sect = tier = None
@@ -67,8 +124,37 @@ try:
67
124
  except FileNotFoundError:
68
125
  pass # no routing.yaml -> treat as disabled
69
126
 
127
+ # A tier resolves to its own model UNLESS that model is on cooldown — a usage limit recorded by
128
+ # tf-goal.sh in the per-machine file tf-model-pick.sh owns. Binding the fallback here is what stops
129
+ # the sub-agents failing behind a main agent that already moved (Routing-Guide §8, 2026-09-06).
130
+ _PICK = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
131
+ _picked = {}
132
+
133
+
70
134
  def model_for(t, harness):
71
- return cfg["tiers"].get(t, {}).get(harness)
135
+ declared = cfg["tiers"].get(t, {}).get(harness)
136
+ if not declared or t == "inherit":
137
+ return declared
138
+ if (t, harness) not in _picked:
139
+ got = declared
140
+ if os.path.isfile(_PICK):
141
+ try:
142
+ import subprocess
143
+ out = subprocess.run(["bash", _PICK, "pick", t, harness],
144
+ capture_output=True, text=True, timeout=20)
145
+ cand = (out.stdout or "").strip().splitlines()
146
+ cand = cand[-1].strip() if cand else ""
147
+ if cand and cand != "inherit":
148
+ got = cand
149
+ elif cand == "inherit":
150
+ got = None # every model in the chain is limited: bind nothing, let it wait
151
+ except Exception:
152
+ pass
153
+ if got != declared:
154
+ print("tf-routing-bind: tier %s (%s) is on %s — %s is on cooldown"
155
+ % (t, harness, got or "no model: the whole chain is limited", declared))
156
+ _picked[(t, harness)] = got
157
+ return _picked[(t, harness)]
72
158
 
73
159
  # --- remove previously generated artifacts (manifest-driven) --------------
74
160
  removed = 0
@@ -210,6 +210,13 @@ sub = ["%s=%s" % (k, v) for k, v in sorted(cfg["subagents"].items())]
210
210
  if sub:
211
211
  print("Subagents: " + ", ".join(sub))
212
212
  print()
213
+ print("When a model is limited (docs/TechieFlow-Routing-Guide.md §9):")
214
+ print(" what each tier would run right now, and what is parked:")
215
+ print(" bash .tfcore/utils/tf-model-pick.sh status")
216
+ print(" the order a tier falls back in: ... chain <tier> <claude|opencode>")
217
+ print(" unpark by hand: ... clear <harness> [model]")
218
+ print(" The supervisor parks a model by itself on a usage limit; tf-goal.sh --no-fallback stops it.")
219
+ print()
213
220
  print("Escalation (ADVISORY — you apply it when you launch; nothing switches a model mid-run):")
214
221
  if cfg["escalation"]:
215
222
  for ph in sorted(cfg["escalation"]):
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow — the framework's own scripts, run against THIS project's real files.
3
+ #
4
+ # bash .tfcore/utils/tf-selfcheck.sh [<repo>] [--report]
5
+ #
6
+ # WHY IT EXISTS. Nine framework defects reached TfLens in three weeks. Every one was
7
+ # found mid-build, in the application's repository, and could only be fixed in the
8
+ # framework's — so each cost a switch between repos, a feedback entry, a maintenance
9
+ # sitting and a re-deploy. The defects were not exotic: a checklist with a row that
10
+ # could never clear, a BRD carrying the anchors its own link checker demands, a metrics
11
+ # file holding the framework's own verdicts. They were simply never run against a real
12
+ # project's real files before the project ran them (MISS-TechieFlow-20260909-01).
13
+ #
14
+ # So this runs them here, first, and says which are broken ON THIS PROJECT. A FAIL is a
15
+ # FRAMEWORK defect, never the project's: nothing here grades the application.
16
+ #
17
+ # Run it before a build pass, or any time. It writes nothing, boots nothing, and never
18
+ # runs git. Python 3 standard library only.
19
+ #
20
+ # --report prints a ready-to-paste entry for the project's framework-feedback file, so
21
+ # filing costs one paste instead of a page.
22
+ #
23
+ # EXIT: 0 every framework script works here · 1 at least one is broken · 3 cannot run.
24
+
25
+ set -uo pipefail
26
+ REPO="."; REPORT=0
27
+ while [[ $# -gt 0 ]]; do
28
+ case "$1" in
29
+ --report) REPORT=1; shift ;;
30
+ -h|--help) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
31
+ *) REPO="$1"; shift ;;
32
+ esac
33
+ done
34
+ [[ -d "$REPO" ]] || { echo "tf-selfcheck: not a directory: $REPO" >&2; exit 3; }
35
+ command -v python3 >/dev/null 2>&1 || { echo "tf-selfcheck: python3 required" >&2; exit 3; }
36
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37
+
38
+ TF_REPO="$REPO" TF_UTILS="$HERE" TF_REPORT="$REPORT" python3 - <<'PY'
39
+ import json, os, re, subprocess, sys
40
+
41
+ REPO = os.path.abspath(os.environ["TF_REPO"])
42
+ UTILS = os.environ["TF_UTILS"]
43
+ TELEM = os.path.join(os.path.dirname(UTILS), "telemetry")
44
+ REPORT = os.environ["TF_REPORT"] == "1"
45
+ findings = [] # (script, what is wrong, the one-line fix hint)
46
+ lines = []
47
+
48
+
49
+ def ok(script, msg):
50
+ lines.append("ok %-20s %s" % (script, msg))
51
+
52
+
53
+ def broken(script, msg, hint=""):
54
+ lines.append("FAIL %-20s %s" % (script, msg))
55
+ findings.append((script, msg, hint))
56
+
57
+
58
+ def skip(script, msg):
59
+ lines.append("-- %-20s %s" % (script, msg))
60
+
61
+
62
+ def run(cmd, cwd=REPO, timeout=120):
63
+ try:
64
+ p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
65
+ return p.returncode, (p.stdout or "") + (p.stderr or "")
66
+ except Exception as e:
67
+ return 99, str(e)
68
+
69
+
70
+ def app_names():
71
+ """Every application this repo holds a checklist for. The checklist is the framework's
72
+ single source of truth, so its name is the app's name — no config value is trusted,
73
+ because core-config.yaml is rsynced between projects and is the thing most likely to
74
+ be stale (the same reason tf-gitignore-audit detects the stack from the tree)."""
75
+ d = os.path.join(REPO, "docs")
76
+ out = []
77
+ if os.path.isdir(d):
78
+ for f in sorted(os.listdir(d)):
79
+ m = re.match(r"^(.+?)(?:-P(\d+))?-Checklist\.md$", f)
80
+ if m and "Deployment" not in f:
81
+ out.append((m.group(1), int(m.group(2) or 1), os.path.join(d, f)))
82
+ return out
83
+
84
+
85
+ # ---- 1. the deployed copy is intact ----------------------------------------------------
86
+ # A framework script that will not even parse is the cheapest possible thing to catch and
87
+ # the most expensive to meet mid-build.
88
+ # Parsed in-process and the shells checked in ONE bash, not one subprocess per file:
89
+ # 54 scripts meant 54 process launches and ten seconds, which is enough to make a check
90
+ # that should run at the top of every command feel like a tax and get skipped.
91
+ bad_parse, shells = [], []
92
+ for folder in (UTILS, TELEM):
93
+ if not os.path.isdir(folder):
94
+ continue
95
+ for f in sorted(os.listdir(folder)):
96
+ full = os.path.join(folder, f)
97
+ if f.endswith(".py"):
98
+ try:
99
+ import ast
100
+ ast.parse(open(full, encoding="utf-8", errors="replace").read(), filename=full)
101
+ except SyntaxError:
102
+ bad_parse.append(f)
103
+ elif f.endswith(".sh"):
104
+ shells.append(full)
105
+ if shells:
106
+ script = "for f in " + " ".join('"%s"' % x for x in shells) + \
107
+ '; do bash -n "$f" 2>/dev/null || echo "$f"; done'
108
+ rc, out = run(["bash", "-c", script])
109
+ bad_parse += [os.path.basename(x) for x in out.split() if x.strip()]
110
+ if bad_parse:
111
+ broken("deployed copy", "%d script(s) do not parse: %s" % (len(bad_parse), ", ".join(bad_parse[:6])),
112
+ "re-run update-framework.sh; if it persists the framework shipped a broken script")
113
+ else:
114
+ ok("deployed copy", "every framework script parses")
115
+
116
+ # ---- 2. the working list accounts for every checklist row ------------------------------
117
+ # TF-019: a row can sit at `Needs re-verify` forever, which pinned the mode and dropped
118
+ # every not-started row out of the list AND out of the counts, so a build pass could report
119
+ # a phase finished with rows at 0%. The arithmetic is the check: every row lands in exactly
120
+ # one of open / terminal / Blocked / roadmap.
121
+ apps = app_names()
122
+ if not apps:
123
+ skip("tf-build-list", "no checklist in docs/ yet")
124
+ for app, phase, path in apps:
125
+ rc, out = run(["bash", os.path.join(UTILS, "tf-build-list.sh"), app] +
126
+ ([] if phase <= 1 else ["--phase", str(phase)]))
127
+ label = "%s p%d" % (app, phase)
128
+ if rc not in (0, 2):
129
+ broken("tf-build-list", "%s: exited %d" % (label, rc), out.strip().splitlines()[0][:120] if out.strip() else "")
130
+ continue
131
+ m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (?:(\d+) roadmap, )?(\d+) total", out)
132
+ if not m:
133
+ if "has no REQ rows" in out:
134
+ skip("tf-build-list", "%s: checklist has no requirement rows yet" % label)
135
+ else:
136
+ broken("tf-build-list", "%s: no counts line to check" % label,
137
+ "the output shape changed; the arithmetic can no longer be verified")
138
+ continue
139
+ mode, work, term, blocked, roadmap, total = m.group(1), *(int(x or 0) for x in m.groups()[1:])
140
+ if work + term + blocked + roadmap != total:
141
+ broken("tf-build-list",
142
+ "%s: %d rows are in no category (%d open + %d terminal + %d blocked + %d roadmap ≠ %d total)"
143
+ % (label, total - work - term - blocked - roadmap, work, term, blocked, roadmap, total),
144
+ "a build pass trusting this list would leave those rows at 0%")
145
+ else:
146
+ ok("tf-build-list", "%s: %d rows, all accounted for (%s)" % (label, total, mode))
147
+
148
+ # ---- 3. the BRD this project actually wrote can be read --------------------------------
149
+ # TF-017: the ledger regex required **BRD-N** immediately after the bullet, and a real BRD
150
+ # carries an <a id> anchor there because §9 cross-links need one. It matched 0 of 179 items.
151
+ sys.path.insert(0, UTILS)
152
+ for app, phase, _cl in apps:
153
+ brd = os.path.join(REPO, "docs", "%s-BRD.md" % app if phase <= 1 else "%s-P%d-BRD.md" % (app, phase))
154
+ if not os.path.isfile(brd):
155
+ continue
156
+ text = open(brd, encoding="utf-8", errors="replace").read()
157
+ # A struck-through item (~~**BRD-3**~~ *(removed …)*) is deliberately not a live
158
+ # requirement, and the ledger is right to skip it. Counting it as unread would make
159
+ # this check cry wolf on every mature BRD -- and a check that cries wolf is the one
160
+ # that gets skimmed (TF-012 failed 8 of 10 screens identically and trained exactly that).
161
+ # UNIQUE ids, not occurrences: an id named again in a cross-reference is the same
162
+ # requirement, and counting mentions made every BRD look short by however many times
163
+ # it referred to itself.
164
+ written_ids = {m.group(1) for m in re.finditer(r"\*\*(BRD-\d+)\*\*", text)
165
+ if not re.search(r"~~\s*$", text[max(0, m.start() - 3):m.start()])}
166
+ written = len(written_ids)
167
+ try:
168
+ import importlib.util
169
+ spec = importlib.util.spec_from_file_location("_sb", os.path.join(UTILS, "tf-split-brd.py"))
170
+ mod = importlib.util.module_from_spec(spec)
171
+ spec.loader.exec_module(mod)
172
+ read = len({(i["id"] if isinstance(i, dict) else i[0]) for i in mod.ledger(text)})
173
+ except Exception as e:
174
+ broken("tf-split-brd", "%s: could not read the BRD at all (%s)" % (app, type(e).__name__),
175
+ "*amend-docs --add-missing cannot append checklist rows")
176
+ continue
177
+ if written and read < written:
178
+ broken("tf-split-brd", "%s: reads %d of %d BRD items" % (app, read, written),
179
+ "new requirements must be added to the checklist by hand until this is fixed")
180
+ elif written:
181
+ ok("tf-split-brd", "%s: reads all %d BRD items" % (app, written))
182
+
183
+ # ---- 4. the streams this project's figures are built on --------------------------------
184
+ # TF-015: a run whose `ended` precedes its `started` was stored with a negative or invented
185
+ # duration, and the two readers of the stream then disagreed about the same record.
186
+ runs_p = os.path.join(REPO, "docs", "metrics", "runs.jsonl")
187
+ if not os.path.isfile(runs_p):
188
+ skip("runs.jsonl", "no telemetry in this project yet")
189
+ else:
190
+ neg = impossible = total_r = badline = 0
191
+ for l in open(runs_p, encoding="utf-8", errors="replace"):
192
+ l = l.strip()
193
+ if not l:
194
+ continue
195
+ try:
196
+ r = json.loads(l)
197
+ except Exception:
198
+ badline += 1
199
+ continue
200
+ total_r += 1
201
+ d = r.get("duration_s")
202
+ if isinstance(d, (int, float)) and d < 0:
203
+ neg += 1
204
+ s, e = r.get("started"), r.get("ended")
205
+ if isinstance(s, str) and isinstance(e, str) and e < s:
206
+ impossible += 1
207
+ if badline:
208
+ broken("runs.jsonl", "%d line(s) are not valid JSON" % badline, "those records are invisible to every figure")
209
+ if impossible or neg:
210
+ # NOT a failure. The emitter refuses these now and the reader discards them and
211
+ # prints how many -- so nothing is wrong with the framework here, and nothing can
212
+ # be done about the records either: the stream is append-only and the true start
213
+ # times are gone. Reported once as history, because calling it broken on every
214
+ # command for the life of the project is how a check earns being skimmed.
215
+ skip("runs.jsonl", "%d old record(s) end before they start — discarded from every "
216
+ "duration figure, and the report says so" % impossible)
217
+ elif total_r:
218
+ ok("runs.jsonl", "%d run record(s), none impossible" % total_r)
219
+
220
+ # ---- 5. the framework's own verdicts are not counted as this project's -----------------
221
+ # TF-020: an FR verdict grades a framework requirement, not a screen, and pooling it into
222
+ # an application segment raises that segment's first-pass rate with nothing on the output
223
+ # to show it happened. SCHEMA §3 states the rule; the rollup has to apply it.
224
+ gates_p = os.path.join(REPO, "docs", "metrics", "gates.jsonl")
225
+ if not os.path.isfile(gates_p):
226
+ skip("tf-metrics", "no gate records in this project yet")
227
+ else:
228
+ has_fr = False
229
+ for l in open(gates_p, encoding="utf-8", errors="replace"):
230
+ if '"req_class"' in l and '"FR"' in l:
231
+ has_fr = True
232
+ break
233
+ rc, out = run(["bash", os.path.join(TELEM, "tf-metrics.sh"), "--rollup", REPO, "--json"])
234
+ try:
235
+ data = json.loads(out)
236
+ except Exception:
237
+ broken("tf-metrics", "--rollup did not return readable JSON (exit %d)" % rc,
238
+ "no figure this project publishes can be checked against the reference")
239
+ data = None
240
+ if data is not None:
241
+ segs = data.get("live") or {}
242
+ if has_fr and "framework-requirement" not in segs:
243
+ broken("tf-metrics", "framework verdicts are pooled into %s" % ", ".join(sorted(segs) or ["nothing"]),
244
+ "this project's first-pass rate is overstated")
245
+ else:
246
+ ok("tf-metrics", "rollup reads; %d segment(s), framework verdicts separate" % len(segs))
247
+
248
+ # ---- 6. amendments to the miss log still fold -------------------------------------------
249
+ # TF-006 and TF-018: a field added after a record was written can only be completed by an
250
+ # amendment, and an amendment the reader will not fold is a fact silently lost.
251
+ miss_p = os.path.join(REPO, "docs", "metrics", "misses.jsonl")
252
+ if not os.path.isfile(miss_p):
253
+ skip("misses.jsonl", "no miss records in this project yet")
254
+ else:
255
+ ids, amends, orphan = set(), 0, 0
256
+ for l in open(miss_p, encoding="utf-8", errors="replace"):
257
+ l = l.strip()
258
+ if not l:
259
+ continue
260
+ try:
261
+ r = json.loads(l)
262
+ except Exception:
263
+ continue
264
+ if r.get("kind") == "miss":
265
+ ids.add(r.get("miss_id"))
266
+ elif r.get("kind") == "miss-amend":
267
+ amends += 1
268
+ if r.get("miss_id") not in ids:
269
+ orphan += 1
270
+ if orphan:
271
+ broken("misses.jsonl", "%d amendment(s) name a miss that is not on this stream" % orphan,
272
+ "the information in them is not reaching any figure")
273
+ else:
274
+ ok("misses.jsonl", "%d miss(es), %d amendment(s), none orphaned" % (len(ids), amends))
275
+
276
+ # ---- 7. the checks that read this project's documents run at all -----------------------
277
+ for name, cmd in (("tf-doc-check", ["bash", os.path.join(UTILS, "tf-doc-check.sh")] +
278
+ (["--app", apps[0][0]] if apps else []) + ["--warn"]),
279
+ ("tf-gitignore-audit", ["bash", os.path.join(UTILS, "tf-gitignore-audit.sh"), REPO, "--dry-run"])):
280
+ if name == "tf-doc-check" and not apps:
281
+ skip(name, "no application documents yet")
282
+ continue
283
+ rc, out = run(cmd)
284
+ if rc in (99,) or "Traceback" in out:
285
+ broken(name, "crashed on this project's files",
286
+ (out.strip().splitlines() or [""])[-1][:120])
287
+ else:
288
+ first = next((l for l in out.splitlines() if l.strip()), "")
289
+ ok(name, "runs (exit %d)" % rc)
290
+
291
+ # ---- 8. the upstream feedback files read as they are ------------------------------------
292
+ # 2026-09-11: PROJECT-STATUS counted every ### heading as an entry and nothing as fixed, so
293
+ # TfLens read "62 open of 62" and its agent told the owner two problems fixed two days earlier
294
+ # were open (MISS-TechieFlow-20260911-04). The one reader is run here, on the real files, and
295
+ # whatever an upstream team has fixed and this project has not re-checked is named below.
296
+ waiting = []
297
+ sys.path.insert(0, UTILS)
298
+ try:
299
+ import tf_feedback
300
+ fb = tf_feedback.files(REPO)
301
+ if not fb:
302
+ skip("tf_feedback", "no upstream feedback files in this project")
303
+ else:
304
+ n = 0
305
+ for f in fb:
306
+ for e in tf_feedback.entries(f):
307
+ n += 1
308
+ if e["state"] == "fixed":
309
+ waiting.append((e["id"], e["since"], os.path.basename(f)))
310
+ ok("tf_feedback", "%d feedback file(s), %d entries read; %d fixed upstream, not yet re-checked"
311
+ % (len(fb), n, len(waiting)))
312
+ except Exception as e:
313
+ broken("tf_feedback", "crashed on this project's feedback files", str(e)[:120])
314
+
315
+ # ---- output -----------------------------------------------------------------------------
316
+ proj = os.path.basename(REPO.rstrip(os.sep))
317
+ print("# tf-selfcheck — %s — the framework's own scripts against this project's real files" % proj)
318
+ for l in lines:
319
+ print(l)
320
+ print()
321
+ if waiting:
322
+ # Not a defect and not a failure: work this project owes. An upstream fix counts only once it
323
+ # has been re-checked here; until then it must never be reported to the owner as open.
324
+ print("Fixed upstream, not yet re-checked here — never report these as open problems:")
325
+ for rid, since, f in waiting:
326
+ print(" %-12s fixed %-10s %s" % (rid, since or "", f))
327
+ print("Re-check each with its entry's \"Verify from here\" step, then close it:")
328
+ print(' bash .tfcore/utils/tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"')
329
+ print()
330
+ if not findings:
331
+ print("Every framework script works on this project. Nothing to file.")
332
+ raise SystemExit(0)
333
+
334
+ print("%d framework defect(s) found. These are the FRAMEWORK's, not this project's —" % len(findings))
335
+ print("nothing here graded the application. Do not work around them silently: file them,")
336
+ print("and carry on (a framework defect blocks a row only when it stops the work).")
337
+ if REPORT:
338
+ print()
339
+ print("--- paste into docs/%s-TechieFlow-Feedback.md ---" % proj)
340
+ for i, (script, msg, hint) in enumerate(findings, 1):
341
+ print()
342
+ print("## TF-xxx — %s: %s" % (script, msg))
343
+ print()
344
+ print("- **Severity:** major")
345
+ print("- **Blocks:** no")
346
+ print("- **Repro:** `bash .tfcore/utils/tf-selfcheck.sh` in this repository")
347
+ print("- **Expected:** the script works on this project's files")
348
+ print("- **Actual:** %s" % msg)
349
+ print("- **Encountered in:** tf-selfcheck, %s" % proj)
350
+ if hint:
351
+ print("- **Consequence:** %s" % hint)
352
+ print("- **Not fixable here** — `.tfcore/` is framework-owned and is replaced on the next update.")
353
+ else:
354
+ print("Run again with --report for entries you can paste into the feedback file.")
355
+ raise SystemExit(1)
356
+ PY
@@ -85,7 +85,12 @@ def ledger(text):
85
85
  lines = text.splitlines()
86
86
  i = 0
87
87
  while i < len(lines):
88
- m = re.match(r"^\s*[-*]\s*\*\*(BRD-\d+)\*\*\s*[—:-]+\s*(.*)$", lines[i])
88
+ # The anchor is not decoration: a BRD's §9 screen inventory links to each item as
89
+ # [BRD-21](#brd-21), markdown generates no id for bold text inside a list item, and
90
+ # tf-doc-check refuses a broken link -- so a real BRD carries `<a id="brd-N"></a>`
91
+ # here and this regex matched NONE of TfLens's 179 items. tests/regression tf_017.
92
+ m = re.match(r"^\s*[-*]\s*(?:<a id=[\"\']?[^\"\'>]*[\"\']?\s*>\s*</a>\s*)?"
93
+ r"\*\*(BRD-\d+)\*\*\s*[—:-]+\s*(.*)$", lines[i])
89
94
  if m and m.group(1) not in nfr_ids:
90
95
  rid, rest = m.group(1), m.group(2)
91
96
  title = re.split(r"\s*(?:\.\s+\*|\s\*Screen|\s·\s\*|\s\*Mockup)", rest, 1)[0].strip().rstrip(".")
@@ -218,7 +223,17 @@ def run(app, phase, force, add_missing, in_order=False):
218
223
  existing = read(cl_p) if os.path.isfile(cl_p) else ""
219
224
  if existing and not force and not add_missing:
220
225
  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))
226
+ # A BRD item already has a row when any row names it: on its `*BRD:*` detail line, or anywhere on
227
+ # its status-table line. The table form used to be read only as a bracket holding exactly one id,
228
+ # so "(BRD-76, Phase 3)" and "(BRD-118, BRD-120, Phase 3)" were invisible and --add-missing
229
+ # appended 44 rows to TfLens phase 3 for 13 new items, 31 of them copies of Verified rows (TF-025).
230
+ mapped = {b for line in re.findall(r"(?m)^.*\*BRD:\*.*$", existing) for b in re.findall(r"BRD-\d+", line)}
231
+ mapped |= {b for row in re.findall(r"(?m)^\|\s*REQ-.*$", existing) for b in re.findall(r"BRD-\d+", row)}
232
+ # ...and on the requirement's own line in its detail entry, with the lines indented under it. A
233
+ # checklist migrated from an older plan names its items there, as "(BRD-1, BRD-2)" or
234
+ # "*(BRD-84, BRD-85)*", with no *BRD:* line: AppManager's run appended a row for all 88 items (TF-001).
235
+ for block in re.findall(r"(?m)^\s*[-*]\s*(?:<a id=[^>]*>\s*</a>\s*)?\*\*REQ-[A-Z]+-\d{3}\*\*.*(?:\n[ \t]+\S.*)*", existing):
236
+ mapped |= set(re.findall(r"BRD-\d+", block))
222
237
  counters = other_phase_counters(app, cl_p, below=phase if in_order else None)
223
238
  if add_missing:
224
239
  for m in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", existing):
@@ -12,6 +12,7 @@
12
12
  # 4. the build verdict from tf-build.sh (skipped with --no-build)
13
13
  # Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
14
14
  set -uo pipefail
15
+ source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # GNU-only commands, for a stock Mac
15
16
  APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
16
17
  shift
17
18
  BUILD=1; PHASE=""
@@ -22,7 +23,7 @@ while [[ $# -gt 0 ]]; do
22
23
  *) shift ;;
23
24
  esac
24
25
  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
+ [[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:[[:space:]]*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
26
27
  [[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
27
28
  ST="PROJECT-STATUS.md"
28
29
 
@@ -31,19 +32,20 @@ echo
31
32
  echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
32
33
  if [[ -f "$ST" ]]; then
33
34
  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)"
35
+ awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '^[[:space:]]*/' | sed 's/^[[:space:]]*/- next: /' | head -2
36
+ echo "- written: $(tf_date_from -u "@$(tf_stat_mtime "$ST")" +%Y-%m-%dT%H:%M:%SZ)"
36
37
  else
37
38
  echo "- no PROJECT-STATUS.md; treat as never written"
38
39
  fi
39
40
  echo
40
41
  echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
41
42
  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)
43
+ # xargs runs ls only when find listed something (GNU xargs -r; BSD xargs has no -r)
44
+ NEWER=(); _found="$(find src tests -type f -newer "$ST" \
45
+ -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null)"
46
+ [[ -n "$_found" ]] && tf_read_lines NEWER < <(printf '%s\n' "$_found" | xargs ls -t 2>/dev/null)
45
47
  echo "- ${#NEWER[@]} file(s)"
46
- printf -- '- %s\n' "${NEWER[@]:0:30}"
48
+ printf -- '- %s\n' ${NEWER[@]+"${NEWER[@]:0:30}"}
47
49
  [[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
48
50
  else
49
51
  echo "- no status file to compare against"
@@ -51,9 +53,9 @@ fi
51
53
  echo
52
54
  echo "## 3. Checklist rows that look interrupted ($CL)"
53
55
  if [[ -f "$CL" ]]; then
54
- grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
56
+ grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
55
57
  | 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)
58
+ n=$(grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
57
59
  [[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
58
60
  else
59
61
  echo "- $CL does not exist"