@techierathore/techieflow 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -16
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
  7. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  8. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  9. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
  11. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  12. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  13. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  14. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  17. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  18. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  19. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  20. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  21. package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
  22. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
  23. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  24. package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
  25. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  26. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  27. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  28. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  29. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  30. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  31. package/.opencode/plugin/techieflow.js +40 -13
  32. package/.tfcore/agents/analyst.md +5 -13
  33. package/.tfcore/agents/architect.md +1 -16
  34. package/.tfcore/agents/flow-master.md +42 -215
  35. package/.tfcore/agents/verifier.md +24 -64
  36. package/.tfcore/core-config.yaml +10 -0
  37. package/.tfcore/hooks/block-git.sh +4 -4
  38. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  39. package/.tfcore/hooks/guard-build.sh +76 -0
  40. package/.tfcore/hooks/guard-db.sh +90 -0
  41. package/.tfcore/hooks/guard-metrics.sh +68 -0
  42. package/.tfcore/hooks/guard-status-html.sh +116 -71
  43. package/.tfcore/hooks/guard-status.sh +43 -16
  44. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  45. package/.tfcore/hooks/guard-verify.sh +70 -48
  46. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  47. package/.tfcore/install-manifest.yaml +0 -18
  48. package/.tfcore/routing.yaml +86 -14
  49. package/.tfcore/standards/coding-standards-core.md +61 -0
  50. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  51. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  52. package/.tfcore/tasks/_owner-language.md +26 -0
  53. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  54. package/.tfcore/tasks/_status-update-gate.md +10 -74
  55. package/.tfcore/tasks/_yolo-mode.md +20 -58
  56. package/.tfcore/tasks/amend-docs.md +21 -143
  57. package/.tfcore/tasks/build-phase.md +14 -224
  58. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  59. package/.tfcore/tasks/create-doc.md +4 -0
  60. package/.tfcore/tasks/create-project-brief.md +22 -0
  61. package/.tfcore/tasks/day1-brownfield.md +31 -505
  62. package/.tfcore/tasks/day1-greenfield.md +28 -213
  63. package/.tfcore/tasks/deploy-checklist.md +19 -0
  64. package/.tfcore/tasks/devguide.md +10 -219
  65. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  66. package/.tfcore/tasks/fix-issues.md +10 -129
  67. package/.tfcore/tasks/generate-html.md +5 -1
  68. package/.tfcore/tasks/handoff-phase.md +18 -16
  69. package/.tfcore/tasks/log-miss.md +19 -158
  70. package/.tfcore/tasks/metrics-report.md +3 -1
  71. package/.tfcore/tasks/mockups.md +12 -117
  72. package/.tfcore/tasks/refresh-status.md +9 -130
  73. package/.tfcore/tasks/split-brd.md +10 -126
  74. package/.tfcore/tasks/triage-and-fix.md +14 -0
  75. package/.tfcore/tasks/triage-issues.md +11 -152
  76. package/.tfcore/tasks/verify-phase.md +36 -572
  77. package/.tfcore/telemetry/SCHEMA.md +100 -8
  78. package/.tfcore/telemetry/tf-metrics.sh +564 -32
  79. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  80. package/.tfcore/templates/stack-questions.md +77 -0
  81. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  82. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  83. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  84. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  85. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  86. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  87. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  88. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  89. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  90. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  91. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  92. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  93. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  94. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  95. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  96. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  97. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  98. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  99. package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
  100. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  101. package/.tfcore/user-guide.md +0 -23
  102. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  107. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  108. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  109. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  110. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  111. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  112. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  113. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  114. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  115. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  116. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  117. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  118. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  119. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  120. package/.tfcore/utils/tf-brd-status.py +176 -0
  121. package/.tfcore/utils/tf-brd-status.sh +19 -0
  122. package/.tfcore/utils/tf-build-list.py +211 -0
  123. package/.tfcore/utils/tf-build-list.sh +21 -0
  124. package/.tfcore/utils/tf-build.sh +143 -0
  125. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  126. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  127. package/.tfcore/utils/tf-day1-files.py +208 -0
  128. package/.tfcore/utils/tf-day1-files.sh +20 -0
  129. package/.tfcore/utils/tf-devguide-list.py +249 -0
  130. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  131. package/.tfcore/utils/tf-doc-check.py +1369 -0
  132. package/.tfcore/utils/tf-doc-check.sh +29 -0
  133. package/.tfcore/utils/tf-emit.sh +545 -18
  134. package/.tfcore/utils/tf-fix-close.sh +115 -0
  135. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  136. package/.tfcore/utils/tf-goal.sh +284 -66
  137. package/.tfcore/utils/tf-harness.sh +4 -9
  138. package/.tfcore/utils/tf-log-miss.py +197 -0
  139. package/.tfcore/utils/tf-log-miss.sh +14 -0
  140. package/.tfcore/utils/tf-misses-md.py +208 -0
  141. package/.tfcore/utils/tf-misses-md.sh +14 -0
  142. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  143. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  144. package/.tfcore/utils/tf-model-pick.sh +475 -0
  145. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  146. package/.tfcore/utils/tf-perf-grade.py +97 -0
  147. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  148. package/.tfcore/utils/tf-phase.sh +92 -0
  149. package/.tfcore/utils/tf-render-html.py +20 -0
  150. package/.tfcore/utils/tf-routing-bind.sh +97 -6
  151. package/.tfcore/utils/tf-routing.sh +13 -7
  152. package/.tfcore/utils/tf-selfcheck.sh +323 -0
  153. package/.tfcore/utils/tf-split-brd.py +309 -0
  154. package/.tfcore/utils/tf-split-brd.sh +22 -0
  155. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  156. package/.tfcore/utils/tf-status-facts.py +358 -0
  157. package/.tfcore/utils/tf-status-facts.sh +20 -0
  158. package/.tfcore/utils/tf-triage.py +243 -0
  159. package/.tfcore/utils/tf-triage.sh +16 -0
  160. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  161. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  162. package/.tfcore/utils/tf-verify-env.sh +88 -0
  163. package/.tfcore/utils/tf-verify-list.py +270 -0
  164. package/.tfcore/utils/tf-verify-list.sh +13 -0
  165. package/.tfcore/utils/tf-verify-screens.mjs +355 -0
  166. package/.tfcore/utils/tf-verify-screens.sh +31 -0
  167. package/.tfcore/utils/tf-verify-tests.sh +166 -0
  168. package/.tfcore/utils/tf-verify-verdict.py +292 -0
  169. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  170. package/.tfcore/utils/tf-yolo.sh +59 -0
  171. package/README.md +120 -1145
  172. package/docs/TechieFlow-Installation.md +1 -5
  173. package/opencode.jsonc +37 -33
  174. package/package.json +1 -5
  175. package/scripts/install.mjs +56 -46
  176. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  177. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  178. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  179. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  180. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  181. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  182. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  183. package/.codex/config.toml +0 -46
  184. package/.codex/hooks.json +0 -63
  185. package/.codex/rules/techieflow.rules +0 -8
  186. package/.tfcore/hooks/codex-adapter.py +0 -248
  187. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  188. package/.tfcore/tasks/author-brd.md +0 -79
  189. package/.tfcore/tasks/document-project.md +0 -343
  190. package/.tfcore/tasks/execute-checklist.md +0 -86
  191. package/.tfcore/tasks/index-docs.md +0 -173
  192. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  193. package/.tfcore/tasks/shard-doc.md +0 -185
  194. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  195. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  196. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  197. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  198. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  199. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  200. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  201. package/.tfcore/utils/techieflow-doc-template.md +0 -325
  202. package/.tfcore/utils/tf-codex-bind.py +0 -196
  203. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  204. package/WORKFLOW.html +0 -1986
@@ -0,0 +1,92 @@
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
+ # The framework's own scripts, against THIS project's real files, before the command
55
+ # does anything (2026-09-09). Nine framework defects reached TfLens by being met
56
+ # mid-build, and each cost a switch to the framework repo and back. Step 0 is the one
57
+ # place every command already passes through, so the check lives here and no task file
58
+ # has to mention it -- including commands added later.
59
+ #
60
+ # It NEVER blocks: a framework defect is reported and the command carries on, exactly
61
+ # as a library gap does. Set TF_SKIP_SELFCHECK=1 to skip it (the self-tests do).
62
+ if [[ "${TF_SKIP_SELFCHECK:-0}" != "1" && -f "$ROOT/.tfcore/utils/tf-selfcheck.sh" ]]; then
63
+ sc="$(cd "$ROOT" && bash .tfcore/utils/tf-selfcheck.sh 2>/dev/null)" || true
64
+ if grep -q '^FAIL' <<<"$sc"; then
65
+ echo "tf-phase: the framework is broken on this project — log these in the feedback file and carry on:" >&2
66
+ grep '^FAIL' <<<"$sc" >&2
67
+ echo "tf-phase: entries to paste: bash .tfcore/utils/tf-selfcheck.sh --report" >&2
68
+ else
69
+ echo "tf-phase: framework self-check clean" >&2
70
+ fi
71
+ fi
72
+ ;;
73
+ goal)
74
+ # written by tf-goal.sh at the start of a run's first cycle; claimed by the first `start`
75
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
76
+ mkdir -p "$DIR"
77
+ printf '{"cmd":"goal","app":"%s","started":"%s"}\n' "${2:-}" "$NOW" > "$FILE"
78
+ echo "$NOW"
79
+ ;;
80
+ show)
81
+ if [[ -f "$FILE" ]]; then cat "$FILE"; else echo "none"; fi
82
+ ;;
83
+ end)
84
+ rm -f "$FILE"
85
+ echo "tf-phase: marker removed"
86
+ ;;
87
+ *)
88
+ sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
89
+ exit 2
90
+ ;;
91
+ esac
92
+ exit 0
@@ -479,6 +479,22 @@ def is_requirements_checklist(base, raw):
479
479
  or "SINGLE SOURCE OF TRUTH" in head)
480
480
 
481
481
 
482
+ def is_miss_list(base, raw):
483
+ """Is this THE derived miss log, or an ordinary document about misses?
484
+
485
+ `docs/{AppName}-Misses.md` is rebuilt from `misses.jsonl` on every write and is
486
+ read by agents and by the owner in markdown. Rendering it produced an HTML copy
487
+ nobody opened, re-rendered on every miss, in every project — 20 of them across
488
+ the estate before this refusal existed (owner, 2026-09-08).
489
+
490
+ Identify the document, do not guess from the suffix, exactly as the checklist
491
+ rule does: the name must match AND the content must carry the generator's own
492
+ Source row. A hand-written note called `Release-Misses.md` has neither."""
493
+ if not re.search(r"-Misses\.md$", base, re.I):
494
+ return False
495
+ return "Rewritten by `tf-misses-md.sh`" in raw[:2000]
496
+
497
+
482
498
  NEXT_CMD_H2 = re.compile(r"^##\s+Next command to run\s*$", re.I)
483
499
 
484
500
 
@@ -541,6 +557,10 @@ def render(md_path, css, head_js, body_js, out_dir=None):
541
557
  raise Refused("%s is the requirements checklist — checklists are AI-agent "
542
558
  "working documents and are NEVER rendered to HTML "
543
559
  "(html-render-shell §0)." % base)
560
+ if is_miss_list(base, raw_probe):
561
+ raise Refused("%s is the derived miss log — it is read in markdown by agents "
562
+ "and by the owner, and is NEVER rendered to HTML "
563
+ "(html-render-shell §0)." % base)
544
564
 
545
565
  raw = raw_probe
546
566
  lines = raw.split("\n")
@@ -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
@@ -44,6 +41,63 @@ root = sys.argv[1]
44
41
  ryaml = os.path.join(root, ".tfcore", "routing.yaml")
45
42
  manifest_path = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
46
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
+
47
101
  # --- parse routing.yaml (flat format — see its header) --------------------
48
102
  cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}}
49
103
  sect = tier = None
@@ -70,8 +124,37 @@ try:
70
124
  except FileNotFoundError:
71
125
  pass # no routing.yaml -> treat as disabled
72
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
+
73
134
  def model_for(t, harness):
74
- 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)]
75
158
 
76
159
  # --- remove previously generated artifacts (manifest-driven) --------------
77
160
  removed = 0
@@ -109,9 +192,18 @@ def write(relpath, content):
109
192
 
110
193
  MARK = "<!-- generated by tf-routing-bind.sh from .tfcore/routing.yaml — do not edit; edit routing.yaml and re-run -->"
111
194
 
195
+ # A phase whose task file no longer ships gets no binding, with a printed warning: the seven
196
+ # never-used commands were removed in Sitting 4c (2026-09-06) and routing.yaml is preserved per
197
+ # project, so a stale line there must never write a file reference OpenCode refuses to load
198
+ # (MISS-TechieFlow-20260906-27: OpenCode would not start on TechieBlog-oc).
199
+ for _ph in sorted(cfg["phases"]):
200
+ if not os.path.isfile(os.path.join(root, ".tfcore", "tasks", _ph + ".md")):
201
+ 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))
202
+ del cfg["phases"][_ph]
203
+
112
204
  # --- Claude Code: routed wrapper commands ---------------------------------
113
205
  OWNER = {"day1-greenfield": "analyst", "day1-brownfield": "analyst",
114
- "author-brd": "analyst", "amend-docs": "analyst", "mockups": "analyst",
206
+ "amend-docs": "analyst", "mockups": "analyst",
115
207
  "split-brd": "analyst", "verify-phase": "verifier"}
116
208
  for phase, t in sorted(cfg["phases"].items()):
117
209
  m = model_for(t, "claude")
@@ -212,5 +304,4 @@ with open(manifest_path, "w", encoding="utf-8", newline="\n") as fh:
212
304
  print("tf-routing-bind: routing enabled — %d artifact(s) generated (%d stale removed)"
213
305
  % (len(generated), removed))
214
306
  PY
215
- python3 "$SELF_DIR/tf-codex-bind.py" "$ROOT" || echo "tf-routing-bind: warning: Codex bindings were not refreshed" >&2
216
307
  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"):
@@ -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"]):
@@ -226,7 +233,6 @@ if cfg["enabled"]:
226
233
  print("Invoke routed phases as:")
227
234
  print(" OpenCode: /techieflow:tasks:<phase> (same commands as always — model now pinned)")
228
235
  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
236
  print()
231
237
  print("WHERE YOU SEE IT — opening the TUI looks UNCHANGED on purpose: your normal chat")
232
238
  print("(the default 'build' agent) stays on YOUR selected model. Routing becomes visible when:")
@@ -0,0 +1,323 @@
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.
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+) 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, total = m.group(1), *(int(x) for x in m.groups()[1:])
140
+ if work + term + blocked != total:
141
+ broken("tf-build-list",
142
+ "%s: %d rows are in no category (%d open + %d terminal + %d blocked ≠ %d total)"
143
+ % (label, total - work - term - blocked, work, term, blocked, 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
+ # ---- output -----------------------------------------------------------------------------
292
+ proj = os.path.basename(REPO.rstrip(os.sep))
293
+ print("# tf-selfcheck — %s — the framework's own scripts against this project's real files" % proj)
294
+ for l in lines:
295
+ print(l)
296
+ print()
297
+ if not findings:
298
+ print("Every framework script works on this project. Nothing to file.")
299
+ raise SystemExit(0)
300
+
301
+ print("%d framework defect(s) found. These are the FRAMEWORK's, not this project's —" % len(findings))
302
+ print("nothing here graded the application. Do not work around them silently: file them,")
303
+ print("and carry on (a framework defect blocks a row only when it stops the work).")
304
+ if REPORT:
305
+ print()
306
+ print("--- paste into docs/%s-TechieFlow-Feedback.md ---" % proj)
307
+ for i, (script, msg, hint) in enumerate(findings, 1):
308
+ print()
309
+ print("## TF-xxx — %s: %s" % (script, msg))
310
+ print()
311
+ print("- **Severity:** major")
312
+ print("- **Blocks:** no")
313
+ print("- **Repro:** `bash .tfcore/utils/tf-selfcheck.sh` in this repository")
314
+ print("- **Expected:** the script works on this project's files")
315
+ print("- **Actual:** %s" % msg)
316
+ print("- **Encountered in:** tf-selfcheck, %s" % proj)
317
+ if hint:
318
+ print("- **Consequence:** %s" % hint)
319
+ print("- **Not fixable here** — `.tfcore/` is framework-owned and is replaced on the next update.")
320
+ else:
321
+ print("Run again with --report for entries you can paste into the feedback file.")
322
+ raise SystemExit(1)
323
+ PY