@techierathore/techieflow 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
  2. package/.claude/commands/TechieFlow/agents/architect.md +0 -6
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  9. package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
  10. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  11. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  12. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  13. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  14. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  15. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  16. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  17. package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
  18. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  19. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  20. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  21. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  22. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  23. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  24. package/.opencode/plugin/techieflow.js +39 -12
  25. package/.tfcore/agents/analyst.md +0 -5
  26. package/.tfcore/agents/architect.md +0 -6
  27. package/.tfcore/agents/flow-master.md +41 -215
  28. package/.tfcore/agents/verifier.md +23 -64
  29. package/.tfcore/core-config.yaml +10 -0
  30. package/.tfcore/hooks/block-git.sh +4 -4
  31. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  32. package/.tfcore/hooks/guard-build.sh +76 -0
  33. package/.tfcore/hooks/guard-db.sh +90 -0
  34. package/.tfcore/hooks/guard-metrics.sh +56 -0
  35. package/.tfcore/hooks/guard-status-html.sh +116 -71
  36. package/.tfcore/hooks/guard-status.sh +43 -16
  37. package/.tfcore/hooks/guard-verify.sh +70 -48
  38. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  39. package/.tfcore/install-manifest.yaml +0 -18
  40. package/.tfcore/routing.yaml +25 -14
  41. package/.tfcore/standards/coding-standards-core.md +61 -0
  42. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  43. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  44. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  45. package/.tfcore/tasks/_status-update-gate.md +10 -74
  46. package/.tfcore/tasks/_yolo-mode.md +20 -58
  47. package/.tfcore/tasks/amend-docs.md +20 -143
  48. package/.tfcore/tasks/build-phase.md +14 -224
  49. package/.tfcore/tasks/day1-brownfield.md +31 -505
  50. package/.tfcore/tasks/day1-greenfield.md +28 -213
  51. package/.tfcore/tasks/deploy-checklist.md +19 -0
  52. package/.tfcore/tasks/devguide.md +10 -219
  53. package/.tfcore/tasks/fix-issues.md +10 -129
  54. package/.tfcore/tasks/log-miss.md +19 -158
  55. package/.tfcore/tasks/metrics-report.md +1 -1
  56. package/.tfcore/tasks/mockups.md +12 -117
  57. package/.tfcore/tasks/refresh-status.md +9 -130
  58. package/.tfcore/tasks/split-brd.md +10 -126
  59. package/.tfcore/tasks/triage-and-fix.md +14 -0
  60. package/.tfcore/tasks/triage-issues.md +11 -152
  61. package/.tfcore/tasks/verify-phase.md +36 -572
  62. package/.tfcore/telemetry/SCHEMA.md +43 -7
  63. package/.tfcore/telemetry/tf-metrics.sh +152 -17
  64. package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
  65. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  66. package/.tfcore/templates/stack-questions.md +77 -0
  67. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  68. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  69. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  70. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  71. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  72. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  73. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  74. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  75. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  76. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  77. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  78. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  79. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  80. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  81. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  82. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  83. package/.tfcore/user-guide.md +0 -23
  84. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  90. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  91. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  92. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  93. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  94. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  95. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  96. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  98. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  99. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  100. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  101. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  102. package/.tfcore/utils/tf-brd-status.py +176 -0
  103. package/.tfcore/utils/tf-brd-status.sh +19 -0
  104. package/.tfcore/utils/tf-build-list.py +191 -0
  105. package/.tfcore/utils/tf-build-list.sh +20 -0
  106. package/.tfcore/utils/tf-build.sh +143 -0
  107. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  108. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  109. package/.tfcore/utils/tf-day1-files.py +208 -0
  110. package/.tfcore/utils/tf-day1-files.sh +20 -0
  111. package/.tfcore/utils/tf-devguide-list.py +249 -0
  112. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  113. package/.tfcore/utils/tf-doc-check.py +1267 -0
  114. package/.tfcore/utils/tf-doc-check.sh +29 -0
  115. package/.tfcore/utils/tf-emit.sh +203 -13
  116. package/.tfcore/utils/tf-fix-close.sh +83 -0
  117. package/.tfcore/utils/tf-goal.sh +193 -62
  118. package/.tfcore/utils/tf-harness.sh +4 -9
  119. package/.tfcore/utils/tf-log-miss.py +197 -0
  120. package/.tfcore/utils/tf-log-miss.sh +14 -0
  121. package/.tfcore/utils/tf-misses-md.py +211 -0
  122. package/.tfcore/utils/tf-misses-md.sh +13 -0
  123. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  124. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  125. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  126. package/.tfcore/utils/tf-perf-grade.py +97 -0
  127. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  128. package/.tfcore/utils/tf-phase.sh +74 -0
  129. package/.tfcore/utils/tf-routing-bind.sh +10 -5
  130. package/.tfcore/utils/tf-routing.sh +6 -7
  131. package/.tfcore/utils/tf-split-brd.py +304 -0
  132. package/.tfcore/utils/tf-split-brd.sh +22 -0
  133. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  134. package/.tfcore/utils/tf-status-facts.py +309 -0
  135. package/.tfcore/utils/tf-status-facts.sh +20 -0
  136. package/.tfcore/utils/tf-triage.py +243 -0
  137. package/.tfcore/utils/tf-triage.sh +16 -0
  138. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  139. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  140. package/.tfcore/utils/tf-verify-env.sh +88 -0
  141. package/.tfcore/utils/tf-verify-list.py +270 -0
  142. package/.tfcore/utils/tf-verify-list.sh +13 -0
  143. package/.tfcore/utils/tf-verify-screens.mjs +283 -0
  144. package/.tfcore/utils/tf-verify-screens.sh +22 -0
  145. package/.tfcore/utils/tf-verify-tests.sh +123 -0
  146. package/.tfcore/utils/tf-verify-verdict.py +283 -0
  147. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  148. package/.tfcore/utils/tf-yolo.sh +59 -0
  149. package/README.md +114 -1149
  150. package/docs/TechieFlow-Installation.md +1 -5
  151. package/opencode.jsonc +13 -33
  152. package/package.json +1 -5
  153. package/scripts/install.mjs +52 -46
  154. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  155. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  156. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  157. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  158. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  159. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  160. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  161. package/.codex/config.toml +0 -46
  162. package/.codex/hooks.json +0 -63
  163. package/.codex/rules/techieflow.rules +0 -8
  164. package/.tfcore/hooks/codex-adapter.py +0 -248
  165. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  166. package/.tfcore/tasks/author-brd.md +0 -79
  167. package/.tfcore/tasks/document-project.md +0 -343
  168. package/.tfcore/tasks/execute-checklist.md +0 -86
  169. package/.tfcore/tasks/index-docs.md +0 -173
  170. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  171. package/.tfcore/tasks/shard-doc.md +0 -185
  172. package/.tfcore/utils/tf-codex-bind.py +0 -196
  173. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  174. package/WORKFLOW.html +0 -1986
@@ -39,7 +39,7 @@ Questions 1–3 are answered by `gates.jsonl`, which remains **the primary strea
39
39
  | `project_type_inferred` | bool | Present **only when `true`** — `metrics.project_type` was absent from `core-config.yaml` and `app` was assumed. Reports must label these records **unclassified**, never silently pool them. |
40
40
  | `backfilled` | bool | Present **only when `true`** — the record was reconstructed after the fact, not written at the moment of the event. Written exclusively by `tf-metrics.sh --backfill-*`. |
41
41
  | `inferred` | string[] | Present only on backfilled records. Names the fields that were **guessed rather than read**. |
42
- | `harness` | string \| null | `claude-code` \| `opencode` \| `codex` \| `null`. **Detected by `tf-emit.sh`, never declared by a task** — see below. |
42
+ | `harness` | string \| null | `claude-code` \| `opencode` \| `null`. **Detected by `tf-emit.sh`, never declared by a task** — see below. `codex` is retired (2026-09-07): nothing writes it, and records that carry it stay valid and readable. |
43
43
 
44
44
  ### `project_type` — what it is and why it exists
45
45
 
@@ -70,11 +70,11 @@ So `install-metrics.sh` now re-examines **`docs` alone**, on a later refresh, an
70
70
 
71
71
  ### `harness` — detected, never declared
72
72
 
73
- The framework runs under **three harnesses**: Claude Code (`.claude/commands/TechieFlow/`), OpenCode (agents/tasks loaded from `opencode.jsonc`), and Codex (`.agents/skills/` plus `.codex/agents/`). The task content is identical across harnesses. A task template therefore **cannot know which one is executing it** — an agent copying a literal from the markdown would stamp whichever harness the example happened to name, and every per-harness comparison would be quietly wrong.
73
+ The framework runs under **two harnesses**: Claude Code (`.claude/commands/TechieFlow/`) and OpenCode (agents/tasks loaded from `opencode.jsonc`). A third, Codex, was retired on 2026-09-07; records written before then may carry `harness: "codex"` and stay valid. The task content is identical across harnesses. A task template therefore **cannot know which one is executing it** — an agent copying a literal from the markdown would stamp whichever harness the example happened to name, and every per-harness comparison would be quietly wrong.
74
74
 
75
75
  So `tf-emit.sh` detects it and injects it. **Never write `harness` into an emit template.** Detection order:
76
76
 
77
- 1. the adapter-owned `TF_HARNESS`, then harness environment variables (`CLAUDECODE`, `CLAUDE_CODE_*` → `claude-code`; any `OPENCODE*` → `opencode`; Codex thread/session markers → `codex`);
77
+ 1. the adapter-owned `TF_HARNESS`, then harness environment variables (`CLAUDECODE`, `CLAUDE_CODE_*` → `claude-code`; any `OPENCODE*` → `opencode`);
78
78
  2. the parent process chain, bounded to 12 levels (OpenCode sets no `OPENCODE_*` variables, so the process name is the only honest signal);
79
79
  3. **`null`** if neither resolves.
80
80
 
@@ -93,7 +93,7 @@ So `tf-emit.sh` detects it and injects it. **Never write `harness` into an emit
93
93
 
94
94
  | Field | Type | Values / notes |
95
95
  |---|---|---|
96
- | `cmd` | string | `day1-brownfield` \| `day1-greenfield` \| `split-brd` \| `mockups` \| `build-phase` \| `verify-phase` \| `fix-issues` \| `triage-issues` \| `log-miss` \| `devguide` \| `productguide` \| `handoff-phase` \| `refresh-status` \| `amend-docs` |
96
+ | `cmd` | string | `day1-brownfield` \| `day1-greenfield` \| `split-brd` \| `mockups` \| `build-phase` \| `verify-phase` \| `fix-issues` \| `triage-issues` \| `log-miss` \| `devguide` \| `productguide` \| `handoff-phase` \| `refresh-status` \| `amend-docs` \| `deploy-checklist` (added 2026-09-06) \| `metrics-report` \| `generate-html` \| `render-workflow-docs` \| `triage-and-fix` \| `framework-reset` (framework maintenance, D-19; all five added 2026-09-07, after FR-37's check was built and found 27 records already carrying a value the schema did not list). A value outside this list is a defect in the emitting task, not in the reader: the reader keeps it. |
97
97
  | `mode` | string \| null | `build` \| `fix`. `build-phase` already distinguishes these (FIX mode) — capture it; the ratio is the rework metric. `null` for commands with no mode. |
98
98
  | `started` | string | ISO-8601 UTC. When the task began — the timestamp you noted at step 0, not "now minus a guess". |
99
99
  | `ended` | string | ISO-8601 UTC. Normally equal to `ts`. |
@@ -104,6 +104,7 @@ So `tf-emit.sh` detects it and injects it. **Never write `harness` into an emit
104
104
  | `files_written` | int | A count the agent **already knows**. Do not shell out to compute it. |
105
105
  | `build_result` | string \| null | `pass` \| `fail` \| `not-run`. |
106
106
  | `harness` | string \| null | Injected by `tf-emit.sh` (§1). **Do not emit it yourself.** |
107
+ | `yolo` | bool | Whether YOLO / goal mode was on for this run (D-12, FR-35; added 2026-09-05). Written by the task from the flag file at the status gate; wired into every task in Session 4c. |
107
108
 
108
109
  ---
109
110
 
@@ -120,7 +121,7 @@ So `tf-emit.sh` detects it and injects it. **Never write `harness` into an emit
120
121
  | `routed` | bool | `model == tier_model`. Present only when both are known. Routing is observed, never enforced — `routed:false` is drift made visible, not an error. |
121
122
  | `tokens_in`, `tokens_out`, `tokens_cache_read`, `tokens_cache_write` | int | Σ over assistant messages whose timestamp ∈ [`started`, `ended`]. Requires the record to carry `started` + `ended` and the session pointer (`.tfcore/.session/<harness>.json` — written by the `session-pointer.sh` hook on Claude, by the plugin on OpenCode). |
122
123
  | `cost_usd` | number \| null | Σ real per-message cost from `opencode.db` on OpenCode; **always `null` on Claude Code** (the transcript has no cost and a rate-card estimate would be an estimate presented as a measurement). |
123
- | `tokens_scope` | string | `tree` = the full session tree — OpenCode: pointer session + descendant sessions; Claude: pointer transcript + the subagent transcripts beside it (`<transcript-dir>/<session-id>/subagents/agent-*.jsonl`, a deterministic path verified 2026-08-20 via a `SubagentStop` payload's `agent_transcript_path`). `main` = Claude main thread only (no subagents dir existed). `conversation` = an exact persisted Codex rollout counter bounded by documented conversation events; measured session data, not an estimate. `none` = window could not be computed (no pointer / unreadable store / empty window) — **tokens are never estimated**. |
124
+ | `tokens_scope` | string | `tree` = the full session tree — OpenCode: pointer session + descendant sessions; Claude: pointer transcript + the subagent transcripts beside it (`<transcript-dir>/<session-id>/subagents/agent-*.jsonl`, a deterministic path verified 2026-08-20 via a `SubagentStop` payload's `agent_transcript_path`). `main` = Claude main thread only (no subagents dir existed). `conversation` = an exact persisted measured session data, not an estimate. `none` = window could not be computed (no pointer / unreadable store / empty window) — **tokens are never estimated**. |
124
125
 
125
126
  | `attempt` | int | **`runs` only, added 2026-08-21.** `1 +` the number of prior non-backfilled `run` records with the same `cmd` whose `reqs_touched` intersects this record's. Stamped only when the record carries a non-empty `reqs_touched`; absent on backfilled records and on REQ-less runs (`metrics-report`, renders). Distinct from the gate-level `attempt` in §3.1 (per REQ per verify). This is the counter `routing.yaml` `escalation:` reads **at launch** — `bash .tfcore/utils/tf-emit.sh --next-run-attempt <cmd> <REQ-ID>...` prints the value the next record would get. Advisory: telemetry records, it never switches a model (DECISIONS.md 2026-08-21). |
126
127
 
@@ -172,7 +173,7 @@ Failure:
172
173
  |---|---|---|
173
174
  | `run_id` | string | The `started` timestamp of the owning run. Ties every REQ verdict in one verify pass together. |
174
175
  | `req_id` | string | e.g. `REQ-UI-004`. |
175
- | `req_class` | string | `UI` \| `FN` \| `RAG` \| `NFR` — the prefix segment of `req_id`. |
176
+ | `req_class` | string | `UI` \| `FN` \| `RAG` \| `NFR` — the prefix segment of `req_id`. **`FR` added 2026-09-07**: the framework's own requirement lines (`docs/TechieFlow-Requirements.md`), graded by `tests/requirements/run.sh`. It is the one `req_class` that does not come from an application's checklist, and it never pools with the others — a framework line and a screen requirement are not the same unit. |
176
177
  | `attempt` | int | See §3.1. Derive it; never guess it. |
177
178
  | `verdict` | string | Mirrors the checklist vocabulary **exactly**: `Verified` \| `Needs re-verify` \| `FAIL` \| `Blocked` \| `Implemented` \| `Done (pre-existing)`. |
178
179
  | `gate` | string \| null | **The FIRST gate that failed**, or `null` on a pass. See §3.2. |
@@ -392,6 +393,8 @@ This is the only stream that carries **three record kinds**: `miss` opens, `miss
392
393
  | `found_gate` | string \| null | When `found_by == "gate"`, which gate — same vocabulary as §3.2. `null` otherwise. |
393
394
  | `found_run_id` | string \| null | `started` of the finding run. |
394
395
  | `failure_class` | string \| null | The §3.3 closed vocabulary, reused verbatim. `null` where none applies. |
396
+ | `what` | string \| null | **The one free-text field** (added 2026-09-05, reset Sitting 4a; D-10). One sentence, in the owner's words, saying what was missed. Never requirement text, file content or a secret (§9). Written into the human-readable `docs/<App>-Misses.md` beside the record by the emitter (§5.5.10, Session 5). |
397
+ | `sort` | string \| null | **Whose gap it was** (added 2026-09-07, reset Session 5; FR-32). The four questions of the miss protocol, asked in order, the first that fits: `spec` (the app's spec did not say it; fix the checklist line) · `unsaid` (the framework never said it; one requirement line plus a check) · `weak-check` (a check existed and did not catch it; fix the check) · `ignored` (it was written and not followed; a hook, or delete the rule). Required by `tf-log-miss.sh`; defaulted by `tf-triage.sh` (`weak-check` for a demoted row, `spec` for a new one, `ignored` for a code edit during triage); amendable (§5.5.7). `null` on records written before the field existed; `FIELD_SINCE` keeps them out of the denominator. |
395
398
 
396
399
  **`origin_model`, `origin_harness` and `origin_confidence` are never written by an agent** — the same rule as `harness` (§1) and for the same reason: task markdown is shared byte-identically across three harnesses, so a copied literal would stamp the wrong model on every record and quietly corrupt every per-model comparison built on it. `tf-emit.sh` resolves all three from `origin_run_id`, and **forces `origin_model` / `origin_harness` to `null` whenever the lookup fails, overwriting anything the caller supplied.** A guess is worse than a gap here, because nothing downstream can see that it was one.
397
400
 
@@ -419,7 +422,7 @@ The agent's job on a `miss` record is therefore small and honest: name what was
419
422
  | `verdict_after` | string | `Verified` \| `Needs re-verify` \| `FAIL` \| `deferred` \| `wont-fix` |
420
423
  | `reopened` | bool | `true` when this miss had already closed `Verified` and a later escape re-opened it. |
421
424
  | `cost_attribution` | string | **Derived by `tf-emit.sh`** from the fix run's `reqs_touched`: `sole` \| `shared:<n>` \| `none` — §5.5.3. |
422
- | `tokens_in`, `tokens_out`, `tokens_cache_read`, `tokens_cache_write`, `cost_usd`, `tokens_scope`, `model` | — | **Injected by `tf-emit.sh`** from the `fix_run_id` window, by exactly the §2.5 mechanism. Never written by an agent. `cost_usd` stays `null` on Claude Code and Codex, per §4. |
425
+ | `tokens_in`, `tokens_out`, `tokens_cache_read`, `tokens_cache_write`, `cost_usd`, `tokens_scope`, `model` | — | **Injected by `tf-emit.sh`** from the `fix_run_id` window, by exactly the §2.5 mechanism. Never written by an agent. `cost_usd` stays `null` on Claude Code, per §4. |
423
426
 
424
427
  ### 5.5.3 `cost_attribution` — the field the money number stands on
425
428
 
@@ -507,6 +510,7 @@ That is what keeps it inside the append-only rule rather than an edit wearing a
507
510
  | Field | Vocabulary |
508
511
  |---|---|
509
512
  | `why_missed` | §5.5.6's seven values |
513
+ | `sort` | `spec` · `unsaid` · `weak-check` · `ignored` (§5.5.1; added 2026-09-07) |
510
514
 
511
515
  - **A judgement may be completed; an observation may not.** `why_missed` is a classification an analyst can still make honestly next week. `found_gate` is a fact about a run that is over — §3.5's *"never backfill the old records with a verdict they never had"* is this same rule seen from the other side.
512
516
  - **Nothing the emitter derives is ever amendable** — `origin_model`, `origin_harness`, `origin_confidence`, `cost_attribution` and every token/cost field are excluded outright. An amend that could set them would be a hole straight through §5.5.1's central rule.
@@ -526,6 +530,8 @@ It **prints its refusal on stdout** rather than failing silently — an agent th
526
530
 
527
531
  ### 5.5.7b Closed vocabularies are enforced ON WRITE (added 2026-08-31)
528
532
 
533
+ **Since 2026-09-05 (reset Sitting 4a) the emitter also refuses:** a record with no `kind`; a record carrying a field this document does not define for its kind (the allowlist is `_ALLOWED` in `tf-emit.sh`, kept in step with these tables); and a `miss` with no `miss_id`. It also overwrites, never merely fills, `harness` when the harness is detected, every derived cost and model field on a `runs`/`gates` record, and the cost fields on a `miss-fix`. A caller cannot self-report any of them. The `_metrics-emit-gate.md` prose that used to say all this was deleted in the same sitting; the script is the rule.
534
+
529
535
  `tf-emit.sh` validates `miss_class`, `artifact`, `severity`, `found_by` and `why_missed` on a `miss`, and `verdict_after` and `fix_cmd` on a `miss-fix`, against the vocabularies above. **A record carrying a value outside one is refused and never appended**, with the reason and the allowed values printed on stdout. It still exits 0 — telemetry has no veto (§10), and neither does a refused record.
530
536
 
531
537
  This closes an asymmetry that was real and was hit: the emitter checked the `miss-amend` allowlist meticulously (§5.5.7) and checked a `miss` record's own enums **not at all**. So a single typo in `miss_class` landed permanently on an append-only stream, invented a category in every distribution built on it, and **could not be corrected** — `miss_class` is not amendable (§5.5.7 admits only closed-vocabulary judgements the emitter does not derive), and constraint 5 forbids editing the file. The only legal move left was to report it.
@@ -567,6 +573,36 @@ resolved in this direction, so the two implementations agree by construction rat
567
573
 
568
574
  ---
569
575
 
576
+ ### 5.5.9 `kind: "review"` — an owner review, and what it cost (FR-36; built 2026-09-06, Sitting 4b)
577
+
578
+ Decided in Session 2 of the reset (D-17): every phase that ends in an owner review records the review's outcome, so the cost of deviation is measurable in every phase, not only in build and verify. UAT issues stay `miss` records; a review record is the owner reading a phase's output and giving corrections.
579
+
580
+ Written by the run that applies the corrections (`day1-greenfield --stage2`, `amend-docs`; build, verify and handoff reviews are wired in Sitting 4c), after that run's own `runs.jsonl` record exists:
581
+
582
+ ```json
583
+ {"kind":"review","phase":"day1-review","reviewed_run_id":"2026-09-05T16:19:38Z",
584
+ "correction_run_id":"2026-09-06T05:02:11Z","corrections":2,
585
+ "what":"the lock screen had no way out and the acceptance lines were too dense to read"}
586
+ ```
587
+
588
+ | Field | Type | Meaning |
589
+ |---|---|---|
590
+ | `phase` | string | `day1-review` · `build-review` · `verify-review` · `handoff-review` (closed vocabulary, enforced on write) |
591
+ | `reviewed_run_id` | string | the `started` of the run that produced the reviewed output |
592
+ | `correction_run_id` | string \| null | the `started` of the run that applied the corrections; null when the owner applied them by hand |
593
+ | `corrections` | integer | how many corrections the owner gave (required) |
594
+ | `what` | string | one sentence in the owner's words; the only free text, like `miss.what` |
595
+ | `tokens_produce`, `cost_produce_usd`, `model_produce` | derived | copied from the run `reviewed_run_id` names (`tokens_out`, `cost_usd`, `model`); null when no run record matches |
596
+ | `tokens_correct`, `cost_correct_usd`, `model_correct` | derived | the same from the run `correction_run_id` names |
597
+
598
+ The two costs are the review's two numbers: what it cost to produce something the owner had to correct, and what the corrections cost. A caller's cost figure is discarded, as on `miss-fix`. Readers that count misses ignore this kind; `tf-metrics.sh` reports reviews since Session 5 (2026-09-07): the count, the corrections given, by phase, and the two token means over the records that carry them (§5.5.8).
599
+
600
+ ### 5.5.10 The readable file — `docs/<App>-Misses.md` (FR-31; built 2026-09-07, Session 5)
601
+
602
+ The stream is for scripts. The owner reads `docs/<App>-Misses.md`: one row per `miss` record, newest first, in three tables (open, fixed, will not fix), each row holding the miss id and its row, when and by whom it was found, whose gap it was (`sort`, in words) and the `what` sentence. A record without a sentence shows its class and artifact instead; a record from before ids were required still gets a row.
603
+
604
+ `tf-emit.sh` rebuilds the file and its HTML through `tf-misses-md.py` after **every** write to the misses stream, whichever door wrote it (log-miss, triage, fix-close, a hand-written record, an amend), so the file is never older than the record. It is **derived**: nothing reads it, nobody edits it, and a wrong row is corrected by a new record. A project whose stream predates the script gets the file by running `bash .tfcore/utils/tf-misses-md.sh` once. The check for FR-31 is in `tests/bugs/run.sh`: the row count equals the record count.
605
+
570
606
  ## 6. Provenance — three separations, one rule applied three times
571
607
 
572
608
  **Data from different provenances never merges.**
@@ -34,6 +34,7 @@ import re
34
34
  import subprocess
35
35
  import sys
36
36
  from collections import Counter, defaultdict, OrderedDict
37
+ from datetime import datetime
37
38
 
38
39
  STREAMS = ("runs", "gates", "sessions", "commits", "misses")
39
40
  VERDICTS = ("Verified", "Needs re-verify", "FAIL", "Blocked", "Implemented", "Done (pre-existing)")
@@ -52,7 +53,7 @@ LATE_GATES = {"perf": "2026-08-10", "assets": "2026-08-31", "mockup-parity": "20
52
53
  # fill, so counting it as "not assessed" understates the practice being measured.
53
54
  # It leaves that field's denominator and is reported separately. SCHEMA.md §5.5.6 /
54
55
  # §3.5. Keep this table in sync when an optional field is added to any stream.
55
- FIELD_SINCE = {"why_missed": "2026-08-28"}
56
+ FIELD_SINCE = {"why_missed": "2026-08-28", "sort": "2026-09-07"}
56
57
 
57
58
  # Fields a `miss-amend` record may complete, and their closed vocabularies.
58
59
  # Kept identical to tf-emit.sh's `_AMENDABLE`; the emitter enforces it on write and
@@ -62,7 +63,11 @@ AMENDABLE_FIELDS = {
62
63
  "why_missed": ("missing-checklist-item", "insufficient-verify-method",
63
64
  "code-audit-limitation", "ambiguous-acceptance",
64
65
  "dependency-not-declared", "instruction-ignored", "other"),
66
+ "sort": ("spec", "unsaid", "weak-check", "ignored"),
65
67
  }
68
+ # The four-question sort in words, for the report (SCHEMA.md §5.5.10).
69
+ SORT_WORDS = {"spec": "the app's spec did not say it", "unsaid": "the framework never said it",
70
+ "weak-check": "the check was too weak", "ignored": "said and ignored"}
66
71
  MIN_N = 3 # fewer supporting records than this -> "insufficient data", never a number
67
72
 
68
73
 
@@ -206,6 +211,7 @@ def analyse_misses(misses):
206
211
  opened = [m for m in misses if m.get("kind") == "miss" and not m.get("backfilled")]
207
212
  fixes = [m for m in misses if m.get("kind") == "miss-fix" and not m.get("backfilled")]
208
213
  amends = [m for m in misses if m.get("kind") == "miss-amend"]
214
+ reviews = [m for m in misses if m.get("kind") == "review"]
209
215
 
210
216
  # ---- fold amendments into their parents BEFORE anything is counted (§5.5.7).
211
217
  # An amend completes a field the miss left null; it can never overwrite one, so
@@ -235,8 +241,10 @@ def analyse_misses(misses):
235
241
  # reported separately — never silently, and never backfilled with a value
236
242
  # nobody assessed at the time.
237
243
  def _eligible(rec, field):
244
+ # A record written after the field existed, or one that carries it anyway (an older
245
+ # record completed by an amend is sorted, whatever its date; Session 5, 2026-09-07).
238
246
  since = FIELD_SINCE.get(field)
239
- return not since or (rec.get("ts") or "") >= since
247
+ return not since or (rec.get("ts") or "") >= since or bool(rec.get(field))
240
248
 
241
249
  # Latest fix per miss decides open/closed; a fix naming no known miss is an orphan.
242
250
  known = {m.get("miss_id") for m in opened}
@@ -349,13 +357,22 @@ def analyse_misses(misses):
349
357
  sole_tokens, sole_priced_n = tok(sole)
350
358
  shared_priced = [f for f in shared if f.get("tokens_out") is not None]
351
359
 
352
- # Dollars exist ONLY where a harness measured them. Claude Code and Codex carry
360
+ # Dollars exist ONLY where a harness measured them. Claude Code carries
353
361
  # cost_usd:null permanently (SCHEMA.md §4) and are never priced from a rate card
354
362
  # here — a pooled sum over mixed harnesses would silently under-report.
355
363
  paid = [f for f in sole if f.get("cost_usd") is not None]
356
364
  escaped = [m for m in opened if m.get("found_by") in ("owner", "production")]
357
365
  design = [m for m in opened if m.get("miss_class") == "unspecified-gap"]
358
366
 
367
+ # Owner reviews (§5.5.9): how many corrections each phase's output needed, and what
368
+ # producing it and correcting it cost. Token means divide by the records that carry the
369
+ # figure (§5.5.8), never by all of them.
370
+ def _mean(key):
371
+ xs = [r[key] for r in reviews if isinstance(r.get(key), (int, float))]
372
+ return (round(float(sum(xs)) / len(xs), 1) if len(xs) >= MIN_N else None), len(xs)
373
+ tp, tp_n = _mean("tokens_produce")
374
+ tc, tc_n = _mean("tokens_correct")
375
+
359
376
  return {
360
377
  "misses_total": len(opened),
361
378
  "miss_fixes_total": len(fixes),
@@ -381,6 +398,22 @@ def analyse_misses(misses):
381
398
  if m.get("found_by") in ("owner", "production")
382
399
  and not m.get("why_missed")
383
400
  and _eligible(m, "why_missed")),
401
+ # Whose gap (§5.5.10, FR-32): the four-question sort. Same denominator rule as
402
+ # why_missed — records that carry it, out of records that could have.
403
+ "sort_n": sum(1 for m in opened if m.get("sort")),
404
+ "sort_eligible": sum(1 for m in opened if _eligible(m, "sort")),
405
+ "sort_predates_field": sum(1 for m in opened if not _eligible(m, "sort")),
406
+ "sort": OrderedDict(
407
+ sorted(Counter(m["sort"] for m in opened if m.get("sort")).items(),
408
+ key=lambda kv: (-kv[1], kv[0]))),
409
+ # Owner reviews (§5.5.9)
410
+ "reviews_n": len(reviews),
411
+ "review_corrections": sum(r["corrections"] for r in reviews if isinstance(r.get("corrections"), int)),
412
+ "reviews_by_phase": OrderedDict(
413
+ sorted(Counter(r.get("phase") or "?" for r in reviews).items(),
414
+ key=lambda kv: (-kv[1], kv[0]))),
415
+ "review_tokens_produce_mean": tp, "review_tokens_produce_n": tp_n,
416
+ "review_tokens_correct_mean": tc, "review_tokens_correct_n": tc_n,
384
417
  "class_distribution": OrderedDict(
385
418
  sorted(Counter(m.get("miss_class") or "?" for m in opened).items(),
386
419
  key=lambda kv: (-kv[1], kv[0]))),
@@ -455,7 +488,7 @@ def analyse_phases(runs):
455
488
  confident fan-out figures largely composed of runs that could not have
456
489
  seen a subagent. Tree-scope only, with the exclusion printed.
457
490
 
458
- 3. DOLLARS. Never pooled across harness (SCHEMA.md §4). Claude and Codex
491
+ 3. DOLLARS. Never pooled across harness (SCHEMA.md §4). Claude
459
492
  carry cost_usd:null permanently; a sum over mixed records under-reports
460
493
  silently. Reported per harness or not at all.
461
494
 
@@ -464,6 +497,34 @@ def analyse_phases(runs):
464
497
  when someone asks how it was measured."""
465
498
  live = [r for r in runs if r.get("kind", "run") == "run" and not r.get("backfilled")]
466
499
 
500
+ # A run that carries both ends but no `duration_s` was silently worth ZERO TIME here,
501
+ # while its tokens still counted — so a phase's total time was a sum over some of its
502
+ # runs and its tokens a sum over others (found 2026-09-07: the reset's own 13 runs
503
+ # reported 16h49m, because 6 of them predate the field and the true figure is ~55h).
504
+ # Deriving it is arithmetic on two recorded facts, not a guess, so it is done — and
505
+ # counted, so the report can say how many of its minutes were derived rather than read.
506
+ # `ended` itself may be absent on an older record; SCHEMA.md's own rule is that `ended`
507
+ # IS the moment the record was written, which is exactly what `ts` holds.
508
+ def _secs(a, b):
509
+ try:
510
+ fmt = "%Y-%m-%dT%H:%M:%SZ"
511
+ return int((datetime.strptime(b, fmt) - datetime.strptime(a, fmt)).total_seconds())
512
+ except Exception:
513
+ return None
514
+
515
+ derived_n = 0
516
+ for r in live:
517
+ if r.get("duration_s") or not r.get("started"):
518
+ continue
519
+ end = r.get("ended") or r.get("ts")
520
+ if not end:
521
+ continue
522
+ d = _secs(r["started"], end)
523
+ if d is not None and d >= 0:
524
+ r["duration_s"] = d
525
+ r["duration_derived"] = "ended" if r.get("ended") else "ts"
526
+ derived_n += 1
527
+
467
528
  def scope_of(r):
468
529
  return r.get("tokens_scope") or "absent"
469
530
 
@@ -532,6 +593,10 @@ def analyse_phases(runs):
532
593
  "median": median(durs),
533
594
  "max": max(durs) if durs else None,
534
595
  "n": len(durs),
596
+ # how many of those minutes were computed from the record's own two
597
+ # timestamps because the record predates `duration_s` — arithmetic on
598
+ # recorded facts, but the reader is told rather than left to assume.
599
+ "derived_n": sum(1 for r in rs if r.get("duration_derived")),
535
600
  },
536
601
  "share_of_duration": pct(sum(durs), grand_dur),
537
602
  "tokens_measured_n": len(priced),
@@ -549,6 +614,27 @@ def analyse_phases(runs):
549
614
  key=lambda kv: (-kv[1]["tokens_out"], kv[0]))),
550
615
  "harnesses": OrderedDict(sorted(Counter(r.get("harness") or "?" for r in rs).items())),
551
616
  "modes": OrderedDict(sorted(Counter(r.get("mode") or "—" for r in rs).items())),
617
+ # A phase run in named modes (a reset's sessions, a build's fresh/fix passes)
618
+ # is several different jobs under one `cmd`. The per-mode split says which,
619
+ # in the order they happened. Same denominators as the phase above: a run
620
+ # with no computable window contributes no tokens, never a zero.
621
+ "by_mode": OrderedDict(
622
+ (mode, {
623
+ "runs": len([r for r in rs if (r.get("mode") or "—") == mode]),
624
+ "duration_s": sum(r.get("duration_s") or 0
625
+ for r in rs if (r.get("mode") or "—") == mode),
626
+ "tokens_out": sum(r.get("tokens_out") or 0
627
+ for r in rs if (r.get("mode") or "—") == mode and has_tokens(r)),
628
+ "tokens_unmeasured_n": len([r for r in rs
629
+ if (r.get("mode") or "—") == mode and not has_tokens(r)]),
630
+ "files_written": sum(r.get("files_written") or 0
631
+ for r in rs if (r.get("mode") or "—") == mode),
632
+ "first_started": min((r.get("started") or "" for r in rs
633
+ if (r.get("mode") or "—") == mode), default=""),
634
+ })
635
+ for mode in sorted({(r.get("mode") or "—") for r in rs},
636
+ key=lambda m: min((r.get("started") or "" for r in rs
637
+ if (r.get("mode") or "—") == m), default=""))),
552
638
  "build_result": OrderedDict(sorted(Counter(r.get("build_result") or "—" for r in rs).items())),
553
639
  "reqs_touched_total": sum(r.get("reqs_count") or 0 for r in rs),
554
640
  "files_written_total": sum(r.get("files_written") or 0 for r in rs),
@@ -919,30 +1005,50 @@ def analyse(repos):
919
1005
  live = [g for g in gates if not g.get("backfilled")]
920
1006
  back = [g for g in gates if g.get("backfilled")]
921
1007
 
1008
+ # `attempt` is DEFINED by §3.1 as 1 + the number of prior live gate records for the same
1009
+ # requirement in the same app — a count over this very stream, not a judgement. A record
1010
+ # that omits it was therefore being read as "no attempt", which drops it out of the
1011
+ # first-pass rate and reports 0% for a set of records that all passed on their first and
1012
+ # only verdict (found 2026-09-07 on the framework's own requirement lines). Derived here,
1013
+ # in stream order, for records that lack it. A record that carries one is never touched.
1014
+ _seen = {}
1015
+ for g in sorted(live, key=lambda r: (r.get("ts") or "", r.get("run_id") or "")):
1016
+ key = (g.get("app"), g.get("req_id"))
1017
+ _seen[key] = _seen.get(key, 0) + 1
1018
+ if g.get("attempt") is None:
1019
+ g["attempt"] = _seen[key]
1020
+ g["attempt_derived"] = True
1021
+
922
1022
  # REQs with ANY backfilled record are excluded from the live first-pass rate:
923
1023
  # their live `attempt` numbering restarts at 1 (SCHEMA.md §3.1).
924
- tainted = {g.get("req_id") for g in back}
1024
+ # A requirement is keyed by (app, req_id): REQ-UI-001 exists in every project, so a rollup that
1025
+ # keyed on req_id alone counted TfLens's and TechieBlog's as one requirement and its first-pass
1026
+ # rate was wrong (MISS-TechieFlow-20260907-03, Session 5).
1027
+ def rk(r):
1028
+ return (r.get("app"), r.get("req_id"))
1029
+
1030
+ tainted = {rk(g) for g in back}
925
1031
 
926
- out = {"per_repo": per_repo, "tainted_reqs": sorted(x for x in tainted if x),
1032
+ out = {"per_repo": per_repo, "tainted_reqs": sorted("%s:%s" % x for x in tainted if x[1]),
927
1033
  "live": {}, "backfilled": {}, "pooled": {},
928
1034
  "misses": analyse_misses(misses),
929
1035
  "phases": analyse_phases(runs)}
930
1036
 
931
1037
  for label, bucket in (("live", live), ("backfilled", back)):
932
1038
  for ptype, recs in sorted(seg(bucket).items()):
933
- eligible = [r for r in recs if label == "backfilled" or r.get("req_id") not in tainted]
934
- reqs = {r.get("req_id") for r in eligible}
935
- first_pass = {r.get("req_id") for r in eligible
1039
+ eligible = [r for r in recs if label == "backfilled" or rk(r) not in tainted]
1040
+ reqs = {rk(r) for r in eligible}
1041
+ first_pass = {rk(r) for r in eligible
936
1042
  if r.get("attempt") == 1 and r.get("verdict") == "Verified"}
937
1043
  failures = [r for r in recs if r.get("verdict") not in ("Verified", "Done (pre-existing)")]
938
1044
  dist = Counter(r.get("gate") or "unattributed" for r in failures)
939
- escaped_reqs = {r.get("req_id") for r in recs if r.get("gate") == "escaped"}
940
- failed_reqs = {r.get("req_id") for r in failures}
1045
+ escaped_reqs = {rk(r) for r in recs if r.get("gate") == "escaped"}
1046
+ failed_reqs = {rk(r) for r in failures}
941
1047
  out[label][ptype] = {
942
1048
  "records": len(recs),
943
1049
  "reqs_scored": len(reqs),
944
1050
  "reqs_excluded_backfill_taint":
945
- len({r.get("req_id") for r in recs if r.get("req_id") in tainted})
1051
+ len({rk(r) for r in recs if rk(r) in tainted})
946
1052
  if label == "live" else 0,
947
1053
  "first_pass_n": len(first_pass),
948
1054
  "first_pass_rate": pct(len(first_pass), len(reqs)) if len(reqs) >= MIN_N else "insufficient data (n=%d)" % len(reqs),
@@ -1154,8 +1260,9 @@ def print_phases(p, W):
1154
1260
  for cmd, m in p["phases"].items():
1155
1261
  print(" [%s] %d run(s)" % (cmd, m["runs"]))
1156
1262
  d = m["duration_s"]
1157
- print(" wall clock : total %s · median %s · max %s (n=%d timed)"
1158
- % (_hms(d["total"]), _hms(d["median"]), _hms(d["max"]), d["n"]))
1263
+ print(" wall clock : total %s · median %s · max %s (n=%d timed%s)"
1264
+ % (_hms(d["total"]), _hms(d["median"]), _hms(d["max"]), d["n"],
1265
+ ", %d derived from the record's own timestamps" % d["derived_n"] if d.get("derived_n") else ""))
1159
1266
  t = m["tokens"]
1160
1267
  print(" tokens : out %s · in %s · cache-read %s · cache-write %s"
1161
1268
  % (_k(t["out"]), _k(t["in"]), _k(t["cache_read"]), _k(t["cache_write"])))
@@ -1217,7 +1324,15 @@ def print_phases(p, W):
1217
1324
  print(" cost (%s) : $%s over %d record(s) — MEASURED, never pooled "
1218
1325
  "across harness" % (h, c["usd"], c["records"]))
1219
1326
  if m["reqs_touched_total"] or m["files_written_total"]:
1220
- print(" work : %d REQ touch(es) · %d file(s) written"
1327
+ bm = m.get("by_mode") or {}
1328
+ if len(bm) > 1:
1329
+ print(" by mode : (in the order they ran)")
1330
+ for mode, v in bm.items():
1331
+ unm = (" [%d unmeasured]" % v["tokens_unmeasured_n"]) if v["tokens_unmeasured_n"] else ""
1332
+ print(" %-22s %2d run(s) %8s %8s out %3d file(s)%s"
1333
+ % (mode, v["runs"], _hms(v["duration_s"]), _k(v["tokens_out"]),
1334
+ v["files_written"], unm))
1335
+ print(" work : %d REQ touch(es) · %d file(s) written"
1221
1336
  % (m["reqs_touched_total"], m["files_written_total"]))
1222
1337
  print("")
1223
1338
 
@@ -1225,7 +1340,7 @@ def print_phases(p, W):
1225
1340
  def print_misses(m, W):
1226
1341
  """The §5.5 block. Two exclusions are printed with the figures they bound —
1227
1342
  an exclusion the reader cannot see is indistinguishable from a bug."""
1228
- if not m["misses_total"] and not m["miss_fixes_total"]:
1343
+ if not m["misses_total"] and not m["miss_fixes_total"] and not m.get("reviews_n"):
1229
1344
  return
1230
1345
  print("")
1231
1346
  print("-" * W)
@@ -1269,9 +1384,29 @@ def print_misses(m, W):
1269
1384
  % m["escapes_missing_why"])
1270
1385
  print(" nothing recorded why. That is the most valuable record in the stream.")
1271
1386
  print(" Complete it: tf-emit.sh --amend <miss_id> why_missed <value> (§5.5.7)")
1387
+ # whose gap — the four-question sort (§5.5.10). Its own denominator, like why_missed.
1388
+ if m.get("sort"):
1389
+ print(" whose gap : (%d of %d misses sorted)" % (m["sort_n"], m["sort_eligible"]))
1390
+ for k, n in m["sort"].items():
1391
+ print(" %-12s %4d %-5s %s" % (k, n, pct(n, m["sort_n"]), SORT_WORDS.get(k, "")))
1392
+ if m.get("sort_predates_field"):
1393
+ print(" %d miss(es) predate the sort field (added %s); sort them with"
1394
+ % (m["sort_predates_field"], FIELD_SINCE["sort"]))
1395
+ print(" tf-emit.sh --amend <miss_id> sort <spec|unsaid|weak-check|ignored>")
1272
1396
  if m["amendments_applied"]:
1273
1397
  print(" amendments folded : %d field(s) completed by miss-amend records (§5.5.7)"
1274
1398
  % m["amendments_applied"])
1399
+ # owner reviews (§5.5.9): corrections per reviewed phase, and the two costs
1400
+ if m.get("reviews_n"):
1401
+ print(" owner reviews : %d review(s), %d correction(s) given — "
1402
+ % (m["reviews_n"], m["review_corrections"])
1403
+ + " ".join("%s=%d" % kv for kv in m["reviews_by_phase"].items()))
1404
+ print(" tokens to produce the reviewed output : %s (n=%d)"
1405
+ % (m["review_tokens_produce_mean"] if m["review_tokens_produce_mean"] is not None
1406
+ else "insufficient data", m["review_tokens_produce_n"]))
1407
+ print(" tokens to apply the corrections : %s (n=%d)"
1408
+ % (m["review_tokens_correct_mean"] if m["review_tokens_correct_mean"] is not None
1409
+ else "insufficient data", m["review_tokens_correct_n"]))
1275
1410
  if m["orphan_amends"]:
1276
1411
  print(" ⚠ %d miss-amend record(s) name no known miss, or a field outside the"
1277
1412
  % m["orphan_amends"])
@@ -1315,7 +1450,7 @@ def print_misses(m, W):
1315
1450
  else:
1316
1451
  print(" USD per miss : no measured dollars (%d priced records)"
1317
1452
  % m["cost_usd_records"])
1318
- print(" Claude Code and Codex carry cost_usd:null permanently and are NEVER")
1453
+ print(" Claude Code carries cost_usd:null permanently and is NEVER")
1319
1454
  print(" priced from a rate card here (SCHEMA.md §4). Real dollars come from")
1320
1455
  print(" OpenCode runs; token counts are the honest figure everywhere else.")
1321
1456
  print(" shared (apportioned): %d fix records — equal division, NOT a measurement"
@@ -150,7 +150,7 @@ sections:
150
150
  - id: architecture-considerations
151
151
  title: Architecture Considerations
152
152
  template: |
153
- - **Repository Structure:** {{repo_thoughts}}
153
+ - **Repository Structure:** decided at day-1 from the stack answer set; name no projects or folders here (owner, 2026-09-06)
154
154
  - **Service Architecture:** {{service_thoughts}}
155
155
  - **Integration Requirements:** {{integration_needs}}
156
156
  - **Security/Compliance:** {{security_requirements}}
@@ -0,0 +1,35 @@
1
+ # TechieFlow — Stack Defaults: .NET
2
+
3
+ | | |
4
+ |---|---|
5
+ | Purpose | An answer set for `TechieFlow-Stack-Questions.md`. These are the defaults the framework's author uses for their own .NET / Blazor / MAUI projects. Naming this answer set at day-1 fills in the questions it covers; the agent asks the rest. |
6
+ | Audience | Owners of .NET projects using TechieFlow. Owners in other stacks write their own answer set with the same structure. |
7
+ | Status | Drafted in Session 2 of the reset (2026-09-04) from the author's answers. Becomes a file under `.tfcore/templates/stack-defaults/` in Session 3. Configuration document read by agents; not rendered to HTML. |
8
+ | Applies to | Projects that declare `stack: dotnet` and name this answer set. Change any row for a project by overriding it in that project's Architecture Stack Decisions section; the override wins. |
9
+
10
+ ---
11
+
12
+ ## Answers
13
+
14
+ | Q | Topic | Default | Still asked at day-1? |
15
+ |---|---|---|---|
16
+ | Q1 | Configuration | Non-secret configuration lives in `appsettings.json`, layered by `appsettings.{Environment}.json`. No other configuration mechanism is created. | No |
17
+ | Q2 | Secrets in development | Project user secrets (`dotnet user-secrets`), visible to the developer, never committed. `appsettings.Development.json` is committed and therefore never holds a real secret. A `secrets.example.json` in the project lists every key the application reads. | No |
18
+ | Q3 | Database | PostgreSQL, running in Docker, for development and production. Docker is always running on the development machine because WSL, where the harness runs, depends on it. If PostgreSQL is unreachable the container is stopped: the agent starts it and continues. Only if no PostgreSQL container exists at all does the agent stop and ask the owner for connection details. It never substitutes another engine and never creates its own database container. | No, unless no container exists |
19
+ | Q4 | Authentication | Two choices only: **AppManager**, the author's shared platform for identity, roles, licences and subscriptions, which counts as part of the application and not as an external integration; or a different mechanism named by the owner. | **Yes**: "AppManager, or something else?" |
20
+ | Q5 | Logging | Serilog, always, with file logging wired at startup before anything else can fail. | No |
21
+ | Q6 | Tests | xUnit, always. A test project exists from day-1. | No |
22
+ | Q7 | Layout and naming | `src/` and `tests/` at the root. The primary executable project is named exactly `<App>`; `<App>.App` is banned. Secondary heads take a descriptive suffix (`<App>.Api`, `<App>.Desktop`). | No |
23
+ | Q8 | User interface | TrBlazeUI for every Blazor head and every MAUI Blazor Hybrid head. Native controls for WinForms, WPF and non-Blazor MAUI heads. Rendering mode is chosen per project and recorded. | Rendering mode only |
24
+ | Q9 | Hosting | Web applications run containerised on a Bluehost VPS. The owner supplies a pipeline guidance document at UAT go-ahead, and a deployment checklist is produced from it. | Asked at UAT go-ahead, as the questionnaire specifies |
25
+ | Q10 | Production secrets | Decided when the owner asks for the production pipeline, after UAT. Until then no document states where production secrets live. | Asked after UAT |
26
+ | Q11 | Standing rules and prohibitions | 1. Never add a NuGet package without a line in the Architecture document saying why. 2. Never create a second configuration mechanism. 3. When TechieFlow, TrBlazeUI, TechieRag or any other internal library lacks something, record it in that library's feedback file and hold the feature until the library is fixed. Never implement a workaround. 4. Data access is always Dapper. 5. Database migrations live in a dedicated project named `<App>Db` (console or library) using the DbUp package. The web application runs the migration at startup, or the pipeline runs it after deployment. Never a `database` folder of loose scripts at the repository root. 6. Log files are written under the build output folder (`bin/`), which is git-ignored. Never at the repository root. 7. No unnecessary folders at the repository root (`data`, `database`, `scripts` and the like). Everything sits under `src/` or `tests/` according to what it belongs to. | No |
27
+
28
+ ---
29
+
30
+ ## Notes
31
+
32
+ - Q4 is asked on purpose even with this answer set, because the choice differs per product.
33
+ - Q3's "start the container, never create one" replaces the behaviour that produced TfLens's own database container: an agent that cannot reach the expected database restarts it or asks, and never builds an alternative.
34
+ - Q11.5 replaces the `database` folder of loose migration scripts that TfLens accumulated.
35
+ - Q11.3 is a standing framework convention restated as a stack rule so that it is recorded per project and its violations can be counted.
@@ -0,0 +1,77 @@
1
+ # TechieFlow — Stack Questions
2
+
3
+ | | |
4
+ |---|---|
5
+ | Purpose | The questions the framework asks the owner about a project's technology and conventions, when it asks them, and where the answers are recorded. The framework itself is technology-neutral; these answers are what make a project's documents and code specific. |
6
+ | Audience | Framework agents (the questionnaire is asked verbatim), project owners, contributors. |
7
+ | Status | Drafted in Session 2 of the reset (2026-09-04). Becomes a template under `.tfcore/templates/` in Session 3. Configuration document read by agents; not rendered to HTML. |
8
+ | Related | `TechieFlow-Stack-Defaults-DotNet.md` (an answer set), `TechieFlow-Requirements.md` (FR-01, FR-02) |
9
+
10
+ ---
11
+
12
+ ## 1. Principle
13
+
14
+ The framework never assumes a language, a UI library, a database, an authentication scheme, a test framework or a hosting target. Before any project document is written, the agent asks the questions below and records the answers in one place: the **Stack Decisions** section of the project's Architecture document. Every later command reads its stack facts from there and nowhere else.
15
+
16
+ An owner who always works in one stack answers once by naming an **answer set**, a file that holds their standard answers. The framework ships one answer set, `TechieFlow-Stack-Defaults-DotNet.md`, described as the defaults its author uses for their own .NET projects. Any owner may copy it, change it, or write a different one. Naming an answer set fills in every question it covers; the agent then asks only the questions the set leaves open.
17
+
18
+ ---
19
+
20
+ ## 2. When the questions are asked
21
+
22
+ | Moment | Questions | Why then |
23
+ |---|---|---|
24
+ | Day-1, before any document is written | Q1 to Q8 | Every document depends on the answers. |
25
+ | When the owner gives the go-ahead for user acceptance testing (UAT) | Q9 | Hosting decisions made earlier are guesses; by UAT the application is real. |
26
+ | When the owner asks for a production deployment pipeline, after UAT | Q10 | Production secrets and infrastructure are decided by the people who run production. |
27
+
28
+ In YOLO mode the agent does not skip the day-1 questions; it takes the answer set's value where one exists and stops with a clear message where none does. A document written without a recorded answer is a defect.
29
+
30
+ ---
31
+
32
+ ## 3. The questions
33
+
34
+ **Q1. Configuration.** Where does non-secret configuration live, and how is it layered per environment?
35
+
36
+ **Q2. Secrets in development.** Where do secrets live on a developer machine, and what must never be committed?
37
+
38
+ **Q3. Database.** Which database engine in development and which in production? Is a container for the database acceptable, and under what condition? What does the agent do if the database is not reachable?
39
+
40
+ **Q4. Authentication and authorisation.** Which identity mechanism: a shared platform, the framework's built-in identity, an external provider, or none? Who owns roles, licences and subscriptions?
41
+
42
+ **Q5. Logging.** Which logging library, which sinks, and is file logging mandatory?
43
+
44
+ **Q6. Tests.** Which test framework? Is a test project mandatory from day one? Which categories of test are expected (unit, integration, browser)?
45
+
46
+ **Q7. Solution layout and naming.** Folder layout for source and tests; naming rule for the primary executable project and for secondary heads.
47
+
48
+ **Q8. User interface.** Which UI framework or component library for each kind of head (web, mobile, desktop)? Which rendering mode by default?
49
+
50
+ **Q9. Hosting and deployment.** Where does the application run, in what form (container, service, static files), and is there a deployment guidance document to follow? Is a deployment checklist required?
51
+
52
+ **Q10. Production secrets and pipeline.** Where do production secrets live, and how does the pipeline inject them?
53
+
54
+ **Q11. Standing rules and prohibitions.** Anything an agent must always or never do in this repository that is not already enforced by a hook. Recorded as a list; the framework treats each item as a rule and tracks whether it is honoured. Two rules are present by default in every project, whatever the stack, and an owner may remove them only by saying so:
55
+
56
+ 1. Log files are written under the build output folder, which is git-ignored. Never at the repository root.
57
+ 2. No unnecessary folders at the repository root (`data`, `database`, `scripts` and the like). Everything sits under the source folder or the tests folder according to what it belongs to.
58
+
59
+ ---
60
+
61
+ ## 4. Where the answers go
62
+
63
+ The Architecture document gains a section, **Stack Decisions**, placed first, one row per question:
64
+
65
+ | Q | Decision | Source | Date |
66
+ |---|---|---|---|
67
+ | Q1 | `appsettings.json` plus `appsettings.{Environment}.json` | answer set: DotNet | 2026-09-04 |
68
+ | Q3 | PostgreSQL in a container for development and production | owner, day-1 | 2026-09-04 |
69
+ | … | | | |
70
+
71
+ "Source" is either the answer set name or "owner" with the moment it was asked. Q9 and Q10 rows are added when they are answered; until then they read "not yet asked (UAT)" and "not yet asked (production)". The Coding Standards document references this section instead of restating it.
72
+
73
+ ---
74
+
75
+ ## 5. What this replaces
76
+
77
+ Before this document, the framework had one standing technology decision (Serilog file logging) written into the day-1 tasks, the BRD template and the Architecture template, and no place for any other. Projects therefore invented their own arrangements at build time; TfLens ended with settings in three places. This questionnaire moves every such decision to day-1 and to one recorded location.
@@ -1,7 +1,7 @@
1
1
  # {AppName} — project session memory (all harnesses)
2
2
 
3
3
  <!-- This file is COMMITTED (unlike CLAUDE.md, which is gitignored and imports it).
4
- OpenCode and Codex auto-load AGENTS.md; Claude Code loads it through CLAUDE.md's
4
+ OpenCode auto-loads AGENTS.md; Claude Code loads it through CLAUDE.md's
5
5
  @AGENTS.md import. Keep everything in here harness-neutral — Claude-only
6
6
  material (permissions, tool preference) lives in app-claude-md-tmpl.md. -->
7
7
 
@@ -13,9 +13,9 @@ ALWAYS read and follow:
13
13
 
14
14
  ## Hard rules (non-negotiable — the harness enforces #1)
15
15
 
16
- 1. **Git is manual — agents NEVER run `git` or `gh`.** Not to commit, and not to read (`status`/`log`/`diff`/`grep`/`blame`). All harnesses enforce this mechanically: Claude Code via `.claude/settings.json` and `block-git.sh`; OpenCode via `opencode.jsonc` plus its plugin bridge; Codex via `.codex/rules/techieflow.rules` plus `.codex/hooks.json`. A blocked git call is the policy working, not an obstacle to route around. Evidence for status updates / "what changed" = the checklist Requirements Status table + the working-tree files (+ mtimes) + a fresh `dotnet build` (`.tfcore/tasks/_status-update-gate.md`). The OWNER commits, in a separate terminal.
16
+ 1. **Git is manual — agents NEVER run `git` or `gh`.** Not to commit, and not to read (`status`/`log`/`diff`/`grep`/`blame`). All harnesses enforce this mechanically: Claude Code via `.claude/settings.json` and `block-git.sh`; OpenCode via `opencode.jsonc` plus its plugin bridge. A blocked git call is the policy working, not an obstacle to route around. Evidence for status updates / "what changed" = the checklist Requirements Status table + the working-tree files (+ mtimes) + a fresh `dotnet build` (`.tfcore/tasks/_status-update-gate.md`). The OWNER commits, in a separate terminal.
17
17
  2. **Run the app yourself — the test harness is fully set up.** Headless Playwright + Chromium live in WSL; the Windows/MAUI dotnet bridge is rung #4 of the build ladder; MAUI Android/iOS/Mac Catalyst are driven over the Appium bridge (`core-config.yaml → runtimeVerification.appium`). NEVER ask the owner to boot the app, run a build, or execute a command — "can't run on Linux/WSL", "it targets Windows", "it's MAUI", "Playwright needs a GUI", "the dependent service is down" are BANNED excuses (`.tfcore/tasks/_smoke-test-policy.md`). Asking the owner is the LAST resort, only after the build ladder + `verify-phase §3a` escalation genuinely fail — and even then you still run the test yourself once they reply.
18
- 3. **The framework tree is invisible to search — a search that finds nothing is NOT proof a file is missing.** `.tfcore/`, `.claude/`, `.codex/`, `.opencode/` and `.agents/skills/` are hidden dot-directories AND gitignored here (deliberate — deployed framework copies are never committed). File search skips hidden paths *and* honours `.gitignore`, so it takes **both** (`rg -uu`; `--hidden` alone is not enough), and `git grep`/`git ls-files` see nothing at all. Confirm a framework file by **reading its literal path** — every one has a single canonical location, and whatever needs it names that path. Never write "not present in this tree" into a verdict, a checklist Remarks cell, a BRD row, or a blocker without having tried the path and failed: that false negative propagates into the docs and the next agent treats it as fact. If a framework file really is absent, the repo needs `update-framework.sh <repo>` run once on this machine — report that instead of reimplementing what it does. (`.tfcore/tasks/_status-update-gate.md` §"The framework tree is INVISIBLE to search".)
18
+ 3. **The framework tree is invisible to search — a search that finds nothing is NOT proof a file is missing.** `.tfcore/`, `.claude/`, `.opencode/` and `.agents/skills/` are hidden dot-directories AND gitignored here (deliberate — deployed framework copies are never committed). File search skips hidden paths *and* honours `.gitignore`, so it takes **both** (`rg -uu`; `--hidden` alone is not enough), and `git grep`/`git ls-files` see nothing at all. Confirm a framework file by **reading its literal path** — every one has a single canonical location, and whatever needs it names that path. Never write "not present in this tree" into a verdict, a checklist Remarks cell, a BRD row, or a blocker without having tried the path and failed: that false negative propagates into the docs and the next agent treats it as fact. If a framework file really is absent, the repo needs `update-framework.sh <repo>` run once on this machine — report that instead of reimplementing what it does. (`.tfcore/tasks/_status-update-gate.md` §"The framework tree is INVISIBLE to search".)
19
19
  4. **Native-head automation binds to the app's own window.** Drive a MAUI head only through a session attached to the app under test (Windows: launched PID → its top-level window handle; Android/iOS/Catalyst: the app's package/bundle id), interact element-by-element via `AutomationId`, and NEVER inject global keyboard/mouse input — it lands in whatever window happens to have focus, not the app (`verify-phase.md §3b`).
20
20
 
21
21
  ## Project basics
@@ -41,7 +41,6 @@ never silently worked around.
41
41
 
42
42
  ## Slash-command syntax (READ ME if a `/agent *command` invocation fails)
43
43
 
44
- **Codex:** use repository skills such as `$techieflow-build`, `$techieflow-verify`, and `$techieflow-refresh-status`, or ask for the skill by name in plain language. Skills load the canonical `.tfcore/tasks/*.md` workflow. When a task calls for fan-out, delegate to the registered `tf_builder`, `tf_test_writer`, `tf_explorer`, `trblazeui`, or `techierag` Codex role and wait for its result. Defining a role alone does not authorize delegation; the user request or applicable skill must call for it. Project config and hooks require repository trust; review changed hooks with `/hooks`.
45
44
 
46
45
  **Claude Code** registers TechieFlow-native agents under the path-derived namespace `TechieFlow:agents:<name>`. The short `/<agent>` form does NOT always resolve. When in doubt use the full form:
47
46