@techierathore/techieflow 1.0.3 → 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 (95) 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 +26 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  8. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  9. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  10. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  12. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +2 -2
  14. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  15. package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
  16. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
  17. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  18. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  19. package/.opencode/plugin/techieflow.js +2 -2
  20. package/.tfcore/agents/analyst.md +5 -8
  21. package/.tfcore/agents/architect.md +1 -10
  22. package/.tfcore/agents/flow-master.md +1 -0
  23. package/.tfcore/agents/verifier.md +1 -0
  24. package/.tfcore/hooks/guard-metrics.sh +13 -1
  25. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  26. package/.tfcore/routing.yaml +61 -0
  27. package/.tfcore/tasks/_owner-language.md +26 -0
  28. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  29. package/.tfcore/tasks/amend-docs.md +6 -5
  30. package/.tfcore/tasks/build-phase.md +1 -1
  31. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  32. package/.tfcore/tasks/create-doc.md +4 -0
  33. package/.tfcore/tasks/create-project-brief.md +22 -0
  34. package/.tfcore/tasks/day1-brownfield.md +2 -2
  35. package/.tfcore/tasks/day1-greenfield.md +2 -2
  36. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  37. package/.tfcore/tasks/generate-html.md +5 -1
  38. package/.tfcore/tasks/handoff-phase.md +18 -16
  39. package/.tfcore/tasks/metrics-report.md +2 -0
  40. package/.tfcore/tasks/verify-phase.md +1 -1
  41. package/.tfcore/telemetry/SCHEMA.md +57 -1
  42. package/.tfcore/telemetry/tf-metrics.sh +416 -19
  43. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  44. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  45. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  46. package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
  47. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  48. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  49. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  50. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  51. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  52. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  53. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  54. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  55. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  56. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  57. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  58. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  59. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  60. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  61. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  62. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  63. package/.tfcore/utils/tf-build-list.py +23 -3
  64. package/.tfcore/utils/tf-build-list.sh +3 -2
  65. package/.tfcore/utils/tf-doc-check.py +106 -4
  66. package/.tfcore/utils/tf-emit.sh +353 -16
  67. package/.tfcore/utils/tf-fix-close.sh +34 -2
  68. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  69. package/.tfcore/utils/tf-goal.sh +93 -6
  70. package/.tfcore/utils/tf-misses-md.py +14 -17
  71. package/.tfcore/utils/tf-misses-md.sh +5 -4
  72. package/.tfcore/utils/tf-model-pick.sh +475 -0
  73. package/.tfcore/utils/tf-phase.sh +18 -0
  74. package/.tfcore/utils/tf-render-html.py +20 -0
  75. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  76. package/.tfcore/utils/tf-routing.sh +7 -0
  77. package/.tfcore/utils/tf-selfcheck.sh +323 -0
  78. package/.tfcore/utils/tf-split-brd.py +6 -1
  79. package/.tfcore/utils/tf-status-facts.py +61 -12
  80. package/.tfcore/utils/tf-verify-screens.mjs +85 -13
  81. package/.tfcore/utils/tf-verify-screens.sh +12 -3
  82. package/.tfcore/utils/tf-verify-tests.sh +60 -17
  83. package/.tfcore/utils/tf-verify-verdict.py +11 -2
  84. package/README.md +15 -5
  85. package/opencode.jsonc +24 -0
  86. package/package.json +1 -1
  87. package/scripts/install.mjs +4 -0
  88. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  89. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  90. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  91. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  92. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  93. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  94. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  95. package/.tfcore/utils/techieflow-doc-template.md +0 -325
@@ -0,0 +1,65 @@
1
+ <!-- tf-schema
2
+ doc: brief
3
+ file: docs/{App}-Brief.md
4
+ header: App, Date
5
+ section: What it is | required | max 150
6
+ section: Who it is for | required | max 150
7
+ section: Must do | required
8
+ section: Out of scope | required | max 150
9
+ section: Open questions | optional | max 150
10
+ rule: brief-must-do
11
+ rule: no-glossary
12
+ target-lines: 60
13
+ max-lines: 120
14
+ -->
15
+ <!-- Authoring notes (agent only; never visible text).
16
+
17
+ The brief is the input to day-1: `*day1-greenfield` and `*day1-brownfield` read it
18
+ and turn it into the Architecture, the BRD and the mockups. Its whole job is to be
19
+ ONE PAGE the owner can read and correct in two minutes, before anything expensive
20
+ is built on it.
21
+
22
+ It replaced a 221-line inherited template (2026-09-08) whose twelve sections —
23
+ executive summary, problem statement, proposed solution, goals and metrics, KPIs,
24
+ post-MVP vision, expansion opportunities — produced a document nobody read and
25
+ nothing checked. What the framework's own review says a brief is: "a one-page
26
+ brief: product, users, must-do, out of scope". That is what this template holds.
27
+
28
+ Each "Must do" line becomes one or more BRD items at day-1, so write them as
29
+ things the product does, one per line, numbered. Not features to build: outcomes
30
+ a user gets. If a line needs a paragraph to explain it, it is two lines.
31
+
32
+ "Open questions" is where a decision the owner has not taken goes, so day-1 asks
33
+ rather than assumes. An empty brief with three honest open questions is worth more
34
+ than a full one built on guesses. -->
35
+
36
+ # {App} — Brief
37
+
38
+ | | |
39
+ |---|---|
40
+ | App | {App} |
41
+ | Date | {YYYY-MM-DD} |
42
+
43
+ ## What it is
44
+
45
+ {Two or three sentences, at most 150 words. What the product does, in the words you
46
+ would use to a person who has never heard of it. No technology.}
47
+
48
+ ## Who it is for
49
+
50
+ {Who uses it and what they are trying to get done. Name each role. At most 150 words.}
51
+
52
+ ## Must do
53
+
54
+ 1. {One thing the product does, as an outcome a user gets.}
55
+ 2. {Another.}
56
+ 3. {Another.}
57
+
58
+ ## Out of scope
59
+
60
+ {What this is deliberately not doing, so day-1 does not build it. At most 150 words.}
61
+
62
+ ## Open questions
63
+
64
+ {A decision not yet taken, one per line, so day-1 asks instead of assuming. Delete the
65
+ section when there are none.}
@@ -0,0 +1,85 @@
1
+ <!-- tf-schema
2
+ doc: decision-request
3
+ file: docs/{App}-Decision-Request.md
4
+ header: App, Written, Waiting on
5
+ section: What happened | required | max 250
6
+ section: What I need you to decide | required
7
+ section: What I do when you answer | required | max 200
8
+ section: Copy this back to me | required | max 250
9
+ entries: What I need you to decide |
10
+ per-entry: 150 300
11
+ rule: entry-decision-options
12
+ rule: entry-recommendation
13
+ rule: paste-back-block
14
+ rule: no-glossary
15
+ target-lines: 120
16
+ max-lines: 260
17
+ -->
18
+ <!-- Authoring notes (agent only; never visible text).
19
+
20
+ WHEN TO WRITE ONE. A command has hit a choice only the owner can make and the
21
+ answer changes what gets built. Write this file, render it to HTML, and say one
22
+ line in the terminal: which file to open, and that it holds N decisions. Never
23
+ argue the decision out in the terminal — that fills the context and buries the
24
+ question. Everything the owner needs is in the file.
25
+
26
+ WHO READS IT. The owner, and nobody else. No task ever loads a Decision Request
27
+ as input, and it is never named in core-config.yaml. The way the answer comes
28
+ back is the owner pasting the block from §"Copy this back to me" — that pasted
29
+ prompt is the input, and the file's job is done the moment it is answered.
30
+
31
+ HOW IT IS WRITTEN. Plain, simple English, the way you would explain it to
32
+ someone who has not read a line of the framework. No coined terms. No framework
33
+ shorthand. No requirement ids in the question itself — name the thing, not its
34
+ number. If a word needs defining, it is the wrong word: change it. A glossary
35
+ section is refused by `rule: no-glossary`, because a document that has to teach
36
+ its own vocabulary before it can ask its question was written for the wrong
37
+ reader.
38
+
39
+ WHAT IT IS NOT. Not a defect report — that is the feedback file. Not an
40
+ architecture decision record — that is DECISIONS.md, written after the fact.
41
+ This file asks a question and then stops existing: when it is answered, it moves
42
+ unchanged to docs/OldDocs/ and the live name is free for the next one.
43
+
44
+ ONE LIVE FILE. All open decisions go in this one file as separate entries. Never
45
+ a dated copy, never a -v2. -->
46
+
47
+ # {App} — decisions I need from you
48
+
49
+ | | |
50
+ |---|---|
51
+ | App | {App} |
52
+ | Written | {YYYY-MM-DD} |
53
+ | Waiting on | {N} decisions. Nothing has been changed yet. |
54
+
55
+ ## What happened
56
+
57
+ {Two short paragraphs, at most 250 words. What was being done, what stopped it, and
58
+ why the choice is yours rather than something to be worked out. Plain words.}
59
+
60
+ ## What I need you to decide
61
+
62
+ ### {1. The short name of the choice}
63
+
64
+ {One paragraph: what the choice actually is, in plain words.}
65
+
66
+ | Option | What happens | What it costs |
67
+ |---|---|---|
68
+ | **A — {name}** | {what the project looks like afterwards} | {work, and what is given up} |
69
+ | **B — {name}** | {what the project looks like afterwards} | {work, and what is given up} |
70
+
71
+ **My recommendation: {A}** — {one sentence saying why.}
72
+
73
+ ## What I do when you answer
74
+
75
+ {The steps, in order, one line each. What gets edited, what gets re-checked, roughly
76
+ how long. So the answer is given with the consequences in view.}
77
+
78
+ ## Copy this back to me
79
+
80
+ {One fenced block per decision. Each block is a complete instruction that stands on
81
+ its own — the owner pastes it and nothing else needs saying.}
82
+
83
+ ```
84
+ {App}: decision 1 — go with option A. {The one thing that changes.}
85
+ ```
@@ -1,31 +1,86 @@
1
- <!-- ============================================================
2
- ONE FEEDBACK FILE PER LIBRARY — never a combined file.
3
- TrBlazeUI and TechieRag are separate codebases with separate
4
- teams; each team receives (or pulls) only its own file:
5
- docs/{AppName}-TrBlazeUI-Feedback.md (issue IDs TR-NNN)
6
- docs/{AppName}-TechieRag-Feedback.md (issue IDs TR-RAG-NNN)
7
- A future library {LibName} gets docs/{AppName}-{LibName}-Feedback.md
8
- with its own ID prefix (WORKFLOW.html §9.1).
9
- Create a library's file on the FIRST issue for that library;
10
- substitute {Library} and the ID prefix below.
11
- ============================================================ -->
12
-
13
- # {Library} Feedback — surfaced during {AppName}
14
-
15
- ## Summary (filled by /flow-master on consolidation)
16
- - 0 blockers, 0 major, 0 minor, 0 nice-to-have
17
- - Last consolidated: {YYYY-MM-DD}
18
-
19
- ## Issues
20
-
21
- <!-- Append entries as gaps are found. IDs are append-only, never renumbered.
22
- TrBlazeUI → TR-NNN · TechieRag → TR-RAG-NNN -->
23
-
24
- ### TR-001 — <short title>
25
- - **Severity:** blocker | major | minor | nice-to-have
26
- - **Repro:** <minimal code snippet>
27
- - **Expected:** …
28
- - **Actual:** …
29
- - **Encountered in:** REQ-UI-013
30
- - **Workaround:** <what you did in app code>
31
- - **Suggested fix:** <hypothesis for the library team>
1
+ <!-- tf-schema
2
+ doc: feedback
3
+ file: docs/{App}-{Upstream}-Feedback.md
4
+ header: App, Upstream, Updated
5
+ section: Summary | required | max 200
6
+ section: Entries | required
7
+ section: Replies from* | optional
8
+ section: Resolution status* | optional
9
+ entries: Entries |
10
+ per-entry: 120 250
11
+ rule: entry-feedback-fields
12
+ rule: entry-blocks-line
13
+ rule: feedback-blocking-count
14
+ rule: no-glossary
15
+ -->
16
+ <!-- Authoring notes (agent only; never visible text).
17
+
18
+ ONE FEEDBACK FILE PER UPSTREAM — never a combined file. Each upstream owns a
19
+ separate codebase and receives only its own file:
20
+
21
+ docs/{App}-TrBlazeUI-Feedback.md ids TR-NNN (the UI library)
22
+ docs/{App}-TechieRag-Feedback.md ids TR-RAG-NNN (the RAG library)
23
+ docs/{App}-TechieFlow-Feedback.md ids TF-NNN (the framework itself)
24
+
25
+ A future library {LibName} gets docs/{App}-{LibName}-Feedback.md with its own
26
+ prefix. Create the file on the FIRST entry for that upstream.
27
+
28
+ REPORTING IS NEVER REFUSED. If something upstream is wrong, file it — whatever
29
+ the severity, whatever the day. What this schema constrains is the SHAPE, not
30
+ the right to report:
31
+
32
+ 1. Every entry says whether it BLOCKS the work, in one word. That is the field
33
+ the owner reads first, and it decides whether the run stops.
34
+ 2. A non-blocking entry is filed and the work CONTINUES. Never stop, never ask,
35
+ never write a page of analysis about it mid-run. One entry, then carry on.
36
+ 3. 250 words for the REPORT, hard: the eight fields, and nothing else. An entry is
37
+ a bug report, not an essay. Evidence goes in a code block, not in prose.
38
+ Working-out that is genuinely worth keeping goes under a `#### Detail` heading
39
+ inside the entry, and is not counted — so a thorough analysis costs nothing and
40
+ an unreadable entry is still refused.
41
+ 4. Plain English. No coined terms, no framework shorthand, no word the reader
42
+ would have to look up. If you find yourself writing a glossary, the words
43
+ are wrong — change the words. `rule: no-glossary` refuses the section.
44
+ 5. A decision for the owner does NOT go in this file. It goes in a Decision
45
+ Request (`app-decision-request-tmpl.md`), which is written for the owner and
46
+ carries the prompt to paste back.
47
+
48
+ "Replies from {Upstream}" holds the upstream team's answers, newest block first.
49
+ Ids are append-only and never renumbered. A fixed entry keeps its body and gains
50
+ a resolution line; the record of what was wrong is the point. -->
51
+
52
+ # {Upstream} feedback — found while building {App}
53
+
54
+ | | |
55
+ |---|---|
56
+ | App | {App} |
57
+ | Upstream | {Upstream} |
58
+ | Updated | {YYYY-MM-DD} |
59
+
60
+ ## Summary
61
+
62
+ {N} entries: {n} blocking now, {n} filed and not blocking, {n} fixed upstream.
63
+
64
+ {One sentence naming what is blocked right now, or the sentence "Nothing is blocked."}
65
+
66
+ ## Entries
67
+
68
+ ### {TF-001} — {one line saying what is wrong}
69
+
70
+ - **Severity:** blocker | major | minor
71
+ - **Blocks:** yes — {the row or command that cannot proceed} | no — {what was done instead, and the work carried on}
72
+ - **Repro:** {the shortest command or snippet that shows it}
73
+ - **Expected:** {what should have happened}
74
+ - **Actual:** {what happened}
75
+ - **Encountered in:** {REQ-UI-013, or the command that was running}
76
+ - **Workaround:** {what was done in this project, or "none"}
77
+ - **Suggested fix:** {one concrete change for the upstream team}
78
+
79
+ #### Detail
80
+
81
+ {Optional, and not counted against the 250 words: the root cause, the reasoning, the
82
+ longer version of the fix. Everything above must stand on its own without it.}
83
+
84
+ ## Replies from {Upstream}
85
+
86
+ <!-- The upstream team's answers, newest block first. Left in full: this is the record. -->
@@ -23,7 +23,7 @@ Each rendered HTML file is:
23
23
  - A **single self-contained `.html` file**, no external CSS, no external JS except the two `<script>` tags below (mermaid + svg-pan-zoom from jsdelivr CDN).
24
24
  - Sibling to the source MD (same filename, `.html` extension).
25
25
  - **Light + dark theme with a persisted toggle** (§2 + §3 + §7). Default is chosen by **time of day** on first open — light between 07:00 and 19:00 local, softened dark otherwise — then the user's explicit toggle choice (saved in `localStorage` as `tf-theme`) wins on every later visit. Light is a **warm off-white** (never harsh bright white); dark is a softened near-black. Monospace for code/pre, sans for body.
26
- - **Only human-readable docs are ever rendered to HTML** (BRD, Architecture, UsageGuide, DevGuide, PROJECT-STATUS). The **checklists are AI-agent working documents — never render them to HTML** (they are read by agents in markdown; an HTML mirror just wastes tokens and drifts). If you are about to emit a `*-Checklist.html`, STOP — that is banned.
26
+ - **Only human-readable docs are ever rendered to HTML** (BRD, Architecture, UsageGuide, DevGuide, ProductGuide, PROJECT-STATUS, a Decision Request, a feedback file). **An agent working document is never rendered to HTML** — it is read in markdown, and an HTML mirror wastes tokens and drifts from the source. Two are banned by name, and `tf-render-html.py` refuses both with exit 2: the **requirements checklist** `{App}-Checklist.md`, and the **miss log** `{App}-Misses.md`. If you are about to emit a `*-Checklist.html` or a `*-Misses.html`, STOP — that is banned. The test for anything else: if the only reader is an agent, it stays markdown.
27
27
  - Working anchor links from any auto-generated or hand-written TOC to its target heading.
28
28
  - Copy buttons on every **code** `<pre>` — but NOT on `pre.mermaid` (a button injected there corrupts the diagram source mermaid reads → "Syntax error"; mermaid blocks get copy/export from their own diagram toolbar).
29
29
  - Mermaid toolbar on every rendered diagram.
@@ -4,8 +4,10 @@
4
4
  bash .tfcore/utils/tf-build-list.sh <App> [--phase N] [--prompts]
5
5
 
6
6
  Reads the phase's checklist (appPhase in core-config.yaml, or --phase) and prints:
7
- - the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist), FRESH
8
- (every open row), or NOTHING (every row terminal or Blocked)
7
+ - the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist, and they
8
+ come FIRST in the list), FRESH (nothing failing), or NOTHING (every row terminal or
9
+ Blocked). The working list is every open row in both FIX and FRESH: the mode says
10
+ what to build first, never what to leave out.
9
11
  - the working list, grouped into clusters by the checklist's page section, each
10
12
  naming its builder from the row prefix: REQ-UI -> trblazeui, REQ-RAG -> techierag,
11
13
  REQ-FN / REQ-NFR -> builder
@@ -113,8 +115,17 @@ def main(argv):
113
115
 
114
116
  fix_rows = [r for r in rows if r["status"] in FIX]
115
117
  open_rows = [r for r in rows if r["status"] not in TERMINAL | BLOCKED]
118
+ # Repair before new work, but never INSTEAD of it. A row can sit at `Needs re-verify`
119
+ # permanently -- TfLens REQ-FN-067 and -070 need a repository that emits events.ndjson
120
+ # and none exists, so every honest verify run re-confirms that gate rather than clearing
121
+ # it. FIX used to win outright, which pinned the mode forever: the twelve rows a later
122
+ # *amend-docs added were never once printed, and a pass that trusted the working list
123
+ # would have met build-phase's ending condition with twelve requirements at 0%.
124
+ # So the working list is every open row, and FIX only says which ones come first.
125
+ # tests/regression/run.sh tf_019.
116
126
  if fix_rows:
117
- mode, work = "FIX", fix_rows
127
+ rest = [r for r in open_rows if r not in fix_rows]
128
+ mode, work = "FIX", fix_rows + rest
118
129
  elif open_rows:
119
130
  mode, work = "FRESH", open_rows
120
131
  else:
@@ -124,6 +135,15 @@ def main(argv):
124
135
  print(f"# tf-build-list — {app} — {cl} — phase {phase}")
125
136
  print(f"Mode: {mode} — {len(work)} row(s) to build; {terminal} terminal, {len(blocked)} Blocked, {len(rows)} total"
126
137
  + (f" — {', '.join(r['id'] for r in work[:8])}{' …' if len(work) > 8 else ''}" if work else ""))
138
+ # Every row is in exactly one of the four counts, and the arithmetic is printed so a
139
+ # reader can check it rather than trust it. A row missing from all of them is the
140
+ # failure this line exists to make impossible.
141
+ if len(work) + terminal + len(blocked) != len(rows):
142
+ print(f"⚠ {len(rows) - len(work) - terminal - len(blocked)} row(s) are in no category — "
143
+ "the checklist carries a status this script does not know; fix the row or the script")
144
+ if fix_rows and len(work) > len(fix_rows):
145
+ print(f"Order: {len(fix_rows)} failing row(s) first ({', '.join(r['id'] for r in fix_rows)}), "
146
+ f"then {len(work) - len(fix_rows)} not yet started")
127
147
  if blocked:
128
148
  print("Blocked (library gaps, pass through unless the feedback entry is resolved): " + ", ".join(r["id"] for r in blocked))
129
149
  if not work:
@@ -6,8 +6,9 @@
6
6
  # bash .tfcore/utils/tf-build-list.sh MyApp --phase 2 # a later phase of a Large project
7
7
  #
8
8
  # Reads the phase's checklist (appPhase in core-config.yaml unless --phase). Rows FAIL /
9
- # PARTIAL / In Progress / Needs re-verify make it FIX mode over exactly those rows; else every
10
- # open row is FRESH; Blocked rows pass through. Clusters follow the checklist's page sections,
9
+ # PARTIAL / In Progress / Needs re-verify make it FIX mode, which puts those rows FIRST and
10
+ # then lists every other open row; FRESH is the same list when nothing is failing; Blocked rows
11
+ # pass through. Clusters follow the checklist's page sections,
11
12
  # each with its builder from the prefix (UI -> trblazeui, RAG -> techierag, FN/NFR -> builder).
12
13
  # Prompts are filled from .tfcore/templates/v4custom/build-subagent-prompt.md.
13
14
  # Writes nothing. Thin wrapper over tf-build-list.py; Python 3 stdlib only.
@@ -52,12 +52,20 @@ DOC_KINDS = [
52
52
  ("-devguide.md", "devguide", "app-devguide-tmpl.md"),
53
53
  ("-productguide.md", "productguide", "app-productguide-tmpl.md"),
54
54
  ("-phases.md", "phases", "app-phases-tmpl.md"),
55
+ ("-brief.md", "brief", "app-brief-tmpl.md"),
56
+ ("-decision-request.md", "decision-request", "app-decision-request-tmpl.md"),
57
+ ("-feedback.md", "feedback", "app-library-feedback-tmpl.md"),
55
58
  ]
56
59
  SIZED_DOCS = {"brd", "architecture", "uidesign", "checklist", "usageguide", "devguide", "productguide"}
57
60
  # Large projects: these four split by phase. Phase 1 keeps the plain name (App-BRD.md);
58
61
  # phase 2 onward are App-P2-BRD.md and so on. docs/TechieFlow-Document-Schemas.md §2.
59
62
  PHASED_DOCS = {"brd", "checklist", "uidesign", "devguide"}
60
63
  PHASED_SUFFIX = {"brd": "BRD", "checklist": "Checklist", "uidesign": "UIDesign", "devguide": "DevGuide"}
64
+ # A feedback entry's heading opens with its id: TF-016, TR-010, TR-RAG-002.
65
+ ENTRY_ID = re.compile(r"^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+\b")
66
+ # A closed entry carries its resolution banner near the top of its body. The banner is not
67
+ # always first: an entry may open with a renumbering or scope note, so the window is generous.
68
+ FEEDBACK_CLOSED = re.compile(r"(?i)(✅|\bfixed upstream\b|\bwill not fix\b|\bwont-fix\b|is \*\*closed\*\*)")
61
69
  DOC_NAME = re.compile(
62
70
  r"^(.+?)(?:-P(\d+))?-(BRD|Deployment-Checklist(?:-[\w]+)?|Checklist|UIDesign|DevGuide|Architecture|Coding-Standards|UsageGuide|Usage-Guide|ProductGuide|Phases)\.md$",
63
71
  re.I)
@@ -429,6 +437,9 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
429
437
  continue
430
438
  if doc == "checklist" and re.search(r"<a id=['\"]d-req-", txt, re.I):
431
439
  continue # a page group holding detail entries
440
+ if doc == "feedback" and ENTRY_ID.match(h.strip()):
441
+ continue # an entry written at ## level: the layout every feedback file shipped
442
+ # with before this schema existed. Read as an entry, not a stranger.
432
443
  extra = "; bugs and feedback go to the misses stream, not the checklist" if doc == "checklist" else ""
433
444
  rep.fail(rel, f'section "{h}" is not in the template{extra}')
434
445
 
@@ -438,6 +449,9 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
438
449
  d = schema.declared(key)
439
450
  if not d:
440
451
  continue
452
+ if doc == "feedback" and d[1] != "required":
453
+ continue # correspondence blocks are appended over time and sit either side
454
+ # of the entries; where they land carries no meaning
441
455
  idx = order_of[norm_heading(d[0])]
442
456
  if idx < last_idx:
443
457
  rep.fail(rel, f'section "{h}" comes after "{last_name}"; the template order is the other way round')
@@ -464,7 +478,9 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
464
478
  for key, h, txt in present:
465
479
  d = schema.declared(key)
466
480
  if d and d[2]:
467
- n = word_count(txt)
481
+ # A `####` block inside a section is detail the reader chooses to open, and is not
482
+ # counted — the same rule as a feedback entry's `#### Detail` (owner, 2026-09-08).
483
+ n = word_count(re.split(r"(?m)^####\s", txt)[0])
468
484
  if n > d[2]:
469
485
  rep.fail(rel, f'section "{h}" is {n} words; at most {d[2]}')
470
486
  if schema.max_lines:
@@ -479,8 +495,14 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
479
495
  if schema.entries:
480
496
  sec_txt = section_text(present, schema.entries[0])
481
497
  prefix = schema.entries[1]
498
+ pairs = list(split_sections(sec_txt, 3)) if sec_txt is not None else []
499
+ if doc == "feedback":
500
+ # Entries may sit under "Entries" as ###, or stand at ## level as every
501
+ # feedback file wrote them before this schema. Both are read the same way.
502
+ pairs += [(h, t) for _k, h, t in present if ENTRY_ID.match(h.strip())]
503
+ sec_txt = sec_txt if sec_txt is not None else ""
482
504
  if sec_txt is not None:
483
- for h3, etxt in split_sections(sec_txt, 3):
505
+ for h3, etxt in pairs:
484
506
  if h3 is None:
485
507
  continue
486
508
  if prefix and not h3.lower().startswith(prefix.lower()):
@@ -489,8 +511,16 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
489
511
  ename = h3[len(prefix):].strip() if prefix else h3
490
512
  ename = re.sub(r"\s*\(.*$", "", ename).strip("` ").strip()
491
513
  entry_names.append(ename)
514
+ if doc == "feedback" and FEEDBACK_CLOSED.search(etxt[:2500]):
515
+ continue # a closed entry is the record of what was wrong and why.
516
+ # History may be as long as it needs to be; the shape and the
517
+ # word cap bind the LIVE ones, which are what the owner reads.
492
518
  if schema.per_entry:
493
- n = word_count(etxt)
519
+ # The cap binds the REPORT, not the working-out. Anything under a `####`
520
+ # heading inside the entry is detail a reader chooses to open, and is not
521
+ # counted: a thorough analysis is worth having, an entry the owner cannot
522
+ # read at a glance is not (owner, 2026-09-08).
523
+ n = word_count(re.split(r"(?m)^####\s", etxt)[0])
494
524
  if n > schema.per_entry[1]:
495
525
  rep.fail(rel, f'entry "{ename}" is {n} words; maximum {schema.per_entry[1]} (target {schema.per_entry[0]})')
496
526
  elif n > schema.per_entry[0]:
@@ -549,6 +579,29 @@ def check_entry_rule(rule, ename, etxt, rel, root, path, rep):
549
579
  elif rule == "entry-expected":
550
580
  if not re.search(r"(?im)^\s*[-*]?\s*\**expected", etxt):
551
581
  rep.fail(rel, f'entry "{ename}" has no "Expected:" line')
582
+ elif rule == "entry-feedback-fields":
583
+ # A feedback entry is a bug report, not an essay. Same eight fields every time,
584
+ # so the upstream team reads them in one shape and the owner reads "Blocks" first.
585
+ for field in ("Severity", "Blocks", "Repro", "Expected", "Actual",
586
+ "Encountered in", "Workaround", "Suggested fix"):
587
+ if not re.search(r"(?im)^\s*[-*]?\s*\**%s\**\s*:" % re.escape(field), etxt):
588
+ rep.fail(rel, f'entry "{ename}" has no "{field}:" line')
589
+ elif rule == "entry-blocks-line":
590
+ # The one word that decides whether the run stops. "no" also has to say what was
591
+ # done instead, because a non-blocking entry is filed and the work carries on.
592
+ m = re.search(r"(?im)^\s*[-*]?\s*\**Blocks\**\s*:\s*(.+)$", etxt)
593
+ if m:
594
+ val = m.group(1).strip().strip("*` ").lower()
595
+ if not re.match(r"^(yes|no)\b", val):
596
+ rep.fail(rel, f'entry "{ename}" says "Blocks: {m.group(1).strip()[:40]}"; it must start "yes" or "no"')
597
+ elif val.startswith("no") and len(val) < 8:
598
+ rep.fail(rel, f'entry "{ename}" says "Blocks: no" and stops; say what was done instead and that the work carried on')
599
+ elif rule == "entry-decision-options":
600
+ if not has_table_with(etxt, "option", "what happens"):
601
+ rep.fail(rel, f'decision "{ename}" has no options table (Option, What happens, What it costs)')
602
+ elif rule == "entry-recommendation":
603
+ if not re.search(r"(?im)^\s*\**My recommendation", etxt):
604
+ rep.fail(rel, f'decision "{ename}" has no "My recommendation:" line — the owner is owed one, with the reason in a sentence')
552
605
 
553
606
 
554
607
  def check_doc_rule(rule, c, rep):
@@ -782,6 +835,50 @@ def check_doc_rule(rule, c, rep):
782
835
  if re.search(r"\b(and|or)\b|,|/", tgt):
783
836
  rep.fail(rel, f'Hosting target "{tgt}" names more than one target; one document per hosting target')
784
837
 
838
+ elif rule == "brief-must-do":
839
+ txt = section_text(present, "Must do")
840
+ if txt is None:
841
+ return
842
+ n = len(re.findall(r"(?m)^\s*\d+[.)]\s+\S", txt))
843
+ if n == 0:
844
+ rep.fail(rel, 'the "Must do" section is not a numbered list; day-1 turns each line into requirements, so one line is one thing the product does')
845
+ elif n > 25:
846
+ rep.fail(rel, f'the "Must do" section has {n} lines; a brief that long is a BRD. Keep the page, and let day-1 expand it')
847
+
848
+ elif rule == "no-glossary":
849
+ # A document written for the owner explains itself in plain words or it is
850
+ # written wrong. A section that defines the document's own vocabulary before it
851
+ # can ask its question is the tell: the fix is to change the words, never to
852
+ # teach them (owner, 2026-09-08).
853
+ for _key, h, _txt in present:
854
+ if re.search(r"(?i)\b(glossary|terminology|words this document uses|"
855
+ r"terms? (used|you)|what these words mean|definitions)\b", h):
856
+ rep.fail(rel, f'section "{h}" defines this document\'s own vocabulary; '
857
+ f'write it in plain English instead — if a word needs a glossary, use a different word')
858
+
859
+ elif rule == "feedback-blocking-count":
860
+ txt = section_text(present, "Summary")
861
+ if txt is None:
862
+ return
863
+ if not re.search(r"(?i)\bblocking\b|\bnothing is blocked\b", txt):
864
+ rep.fail(rel, 'the Summary must say how many entries are blocking right now, '
865
+ 'or "Nothing is blocked" — it is the first thing the owner reads')
866
+
867
+ elif rule == "paste-back-block":
868
+ # `present` is built from the prose-only copy, which drops fenced blocks — and a
869
+ # fenced block is exactly what this rule looks for. Re-split the uncommented source.
870
+ raw_present = [(norm_heading(h), h, t) for h, t in split_sections(nocomment, 2) if h is not None]
871
+ txt = section_text(raw_present, "Copy this back to me")
872
+ if txt is None:
873
+ return
874
+ blocks = re.findall(r"^```", txt, re.M)
875
+ if len(blocks) < 2:
876
+ rep.fail(rel, 'the "Copy this back to me" section has no fenced block; '
877
+ 'the owner answers by pasting it, so it must be there to paste')
878
+ n_dec = len(c.get("entry_names") or [])
879
+ if n_dec and len(blocks) // 2 < n_dec:
880
+ rep.fail(rel, f'{n_dec} decisions but {len(blocks) // 2} paste-back block(s); one per decision')
881
+
785
882
  elif rule == "phases-table":
786
883
  txt = section_text(present, "Phases")
787
884
  if txt is None:
@@ -1148,7 +1245,7 @@ def check_mockups(root: str, rep: Report):
1148
1245
  # ----------------------------------------------------------------------------
1149
1246
  def app_files(root: str, app: str):
1150
1247
  docs = os.path.join(root, "docs")
1151
- names = [f"{app}-Phases.md", f"{app}-BRD.md", f"{app}-Architecture.md", f"{app}-UIDesign.md", f"{app}-Checklist.md",
1248
+ names = [f"{app}-Brief.md", f"{app}-Phases.md", f"{app}-BRD.md", f"{app}-Architecture.md", f"{app}-UIDesign.md", f"{app}-Checklist.md",
1152
1249
  f"{app}-Coding-Standards.md", f"{app}-UsageGuide.md", f"{app}-DevGuide.md", f"{app}-ProductGuide.md"]
1153
1250
  # later phases of a Large project: App-P2-BRD.md, App-P2-Checklist.md, … in phase order
1154
1251
  extra = []
@@ -1160,6 +1257,11 @@ def app_files(root: str, app: str):
1160
1257
  names += [f for _p, _o, f in sorted(extra)]
1161
1258
  if os.path.isdir(docs):
1162
1259
  names += sorted(f for f in os.listdir(docs) if re.match(rf"^{re.escape(app)}-Deployment-Checklist(-[\w]+)?\.md$", f))
1260
+ # the two documents written for a reader outside this project: the owner's open
1261
+ # decisions, and whatever this project has reported upstream (one file per upstream)
1262
+ names += sorted(f for f in os.listdir(docs)
1263
+ if re.match(rf"^{re.escape(app)}-Decision-Request\.md$", f)
1264
+ or re.match(rf"^{re.escape(app)}-[\w.]+-Feedback\.md$", f))
1163
1265
  out = [os.path.join(docs, n) for n in names if os.path.exists(os.path.join(docs, n))]
1164
1266
  ps = os.path.join(root, "PROJECT-STATUS.md")
1165
1267
  if os.path.exists(ps):