@techierathore/techieflow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
  2. package/.claude/commands/TechieFlow/agents/architect.md +84 -0
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
  9. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
  11. package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
  12. package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
  13. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
  14. package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
  17. package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
  18. package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
  19. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
  20. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
  21. package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
  22. package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
  23. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
  24. package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
  25. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
  26. package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
  27. package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
  28. package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
  29. package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
  30. package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
  31. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
  32. package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
  33. package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
  34. package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
  35. package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
  36. package/.claude/commands/generate-html.md +124 -0
  37. package/.codex/config.toml +46 -0
  38. package/.codex/hooks.json +63 -0
  39. package/.codex/rules/techieflow.rules +8 -0
  40. package/.opencode/command/generate-html.md +129 -0
  41. package/.opencode/plugin/techieflow.js +350 -0
  42. package/.tfcore/TOKEN-GUIDE.md +59 -0
  43. package/.tfcore/agents/analyst.md +97 -0
  44. package/.tfcore/agents/architect.md +84 -0
  45. package/.tfcore/agents/flow-master.md +217 -0
  46. package/.tfcore/agents/verifier.md +72 -0
  47. package/.tfcore/checklists/architect-checklist.md +438 -0
  48. package/.tfcore/core-config.yaml +47 -0
  49. package/.tfcore/data/brainstorming-techniques.md +36 -0
  50. package/.tfcore/data/elicitation-methods.md +154 -0
  51. package/.tfcore/data/techieflow-kb.md +807 -0
  52. package/.tfcore/data/technical-preferences.md +3 -0
  53. package/.tfcore/enhanced-ide-development-workflow.md +248 -0
  54. package/.tfcore/hooks/block-git.sh +301 -0
  55. package/.tfcore/hooks/codex-adapter.py +248 -0
  56. package/.tfcore/hooks/guard-artifacts.sh +101 -0
  57. package/.tfcore/hooks/guard-status-html.sh +102 -0
  58. package/.tfcore/hooks/guard-status.sh +124 -0
  59. package/.tfcore/hooks/guard-verify.sh +108 -0
  60. package/.tfcore/hooks/metrics-session.sh +129 -0
  61. package/.tfcore/hooks/session-pointer.sh +52 -0
  62. package/.tfcore/hooks/sweep-artifacts.sh +198 -0
  63. package/.tfcore/install-manifest.yaml +231 -0
  64. package/.tfcore/routing.yaml +105 -0
  65. package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
  66. package/.tfcore/tasks/_smoke-test-policy.md +98 -0
  67. package/.tfcore/tasks/_status-update-gate.md +78 -0
  68. package/.tfcore/tasks/_yolo-mode.md +77 -0
  69. package/.tfcore/tasks/advanced-elicitation.md +117 -0
  70. package/.tfcore/tasks/amend-docs.md +149 -0
  71. package/.tfcore/tasks/author-brd.md +79 -0
  72. package/.tfcore/tasks/build-phase.md +231 -0
  73. package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
  74. package/.tfcore/tasks/create-doc.md +101 -0
  75. package/.tfcore/tasks/day1-brownfield.md +507 -0
  76. package/.tfcore/tasks/day1-greenfield.md +224 -0
  77. package/.tfcore/tasks/devguide.md +224 -0
  78. package/.tfcore/tasks/document-project.md +343 -0
  79. package/.tfcore/tasks/execute-checklist.md +86 -0
  80. package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
  81. package/.tfcore/tasks/fix-issues.md +134 -0
  82. package/.tfcore/tasks/generate-html.md +115 -0
  83. package/.tfcore/tasks/handoff-phase.md +126 -0
  84. package/.tfcore/tasks/index-docs.md +173 -0
  85. package/.tfcore/tasks/kb-mode-interaction.md +75 -0
  86. package/.tfcore/tasks/log-miss.md +160 -0
  87. package/.tfcore/tasks/metrics-report.md +143 -0
  88. package/.tfcore/tasks/mockups.md +122 -0
  89. package/.tfcore/tasks/productguide.md +105 -0
  90. package/.tfcore/tasks/refresh-status.md +135 -0
  91. package/.tfcore/tasks/render-workflow-docs.md +117 -0
  92. package/.tfcore/tasks/shard-doc.md +185 -0
  93. package/.tfcore/tasks/split-brd.md +131 -0
  94. package/.tfcore/tasks/triage-issues.md +157 -0
  95. package/.tfcore/tasks/verify-phase.md +579 -0
  96. package/.tfcore/telemetry/SCHEMA.md +678 -0
  97. package/.tfcore/telemetry/install-metrics.sh +390 -0
  98. package/.tfcore/telemetry/pre-commit +94 -0
  99. package/.tfcore/telemetry/tf-metrics.sh +1415 -0
  100. package/.tfcore/templates/architecture-tmpl.yaml +650 -0
  101. package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
  102. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
  103. package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
  104. package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
  105. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
  106. package/.tfcore/templates/market-research-tmpl.yaml +252 -0
  107. package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
  108. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
  109. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
  110. package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
  111. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
  112. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
  113. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
  114. package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
  115. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
  116. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
  117. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
  118. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
  119. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
  120. package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
  121. package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
  122. package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
  123. package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
  124. package/.tfcore/user-guide.md +633 -0
  125. package/.tfcore/utils/techieflow-doc-template.md +325 -0
  126. package/.tfcore/utils/tf-assets.sh +310 -0
  127. package/.tfcore/utils/tf-codex-bind.py +196 -0
  128. package/.tfcore/utils/tf-codex-telemetry.py +60 -0
  129. package/.tfcore/utils/tf-emit.sh +1075 -0
  130. package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
  131. package/.tfcore/utils/tf-goal.sh +421 -0
  132. package/.tfcore/utils/tf-harness.sh +160 -0
  133. package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
  134. package/.tfcore/utils/tf-mockup-parity.sh +65 -0
  135. package/.tfcore/utils/tf-opencode-audit.py +126 -0
  136. package/.tfcore/utils/tf-perf.sh +241 -0
  137. package/.tfcore/utils/tf-render-html.py +715 -0
  138. package/.tfcore/utils/tf-render-html.sh +28 -0
  139. package/.tfcore/utils/tf-routing-bind.sh +216 -0
  140. package/.tfcore/utils/tf-routing.sh +247 -0
  141. package/.tfcore/utils/tf-yolo.sh +119 -0
  142. package/.tfcore/working-in-the-brownfield.md +606 -0
  143. package/LICENSE +201 -0
  144. package/README.md +1212 -0
  145. package/WORKFLOW.html +1986 -0
  146. package/docs/TechieFlow-Installation.md +299 -0
  147. package/opencode.jsonc +929 -0
  148. package/package.json +60 -0
  149. package/scripts/install.mjs +992 -0
  150. package/scripts/test-install.mjs +505 -0
  151. package/scripts/validate.mjs +155 -0
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow telemetry — per-repo setup. Called AUTOMATICALLY by
3
+ # update-framework.sh, scaffold-brownfield.sh and scaffold-greenfield.sh.
4
+ #
5
+ # You do NOT normally run this yourself. Refreshing the framework sets up
6
+ # telemetry as part of the same command — there is no separate install step.
7
+ #
8
+ # Run it directly only to CORRECT the project classification:
9
+ # .tfcore/telemetry/install-metrics.sh <repo> --type library
10
+ #
11
+ # WHAT IT DOES (idempotent, non-interactive, safe on every re-run):
12
+ # 1. creates docs/metrics/ and seeds the four empty streams
13
+ # 2. writes docs/metrics/README.md (for a human who finds the files)
14
+ # 3. records metrics.project_type in .tfcore/core-config.yaml — auto-detected
15
+ # ONCE, printed loudly, then never touched again (core-config.yaml is a
16
+ # preserved file, so the classification survives every framework refresh)
17
+ # 4. installs .git/hooks/pre-commit (and retires the old post-commit)
18
+ # 5. warns if a .gitignore pattern would swallow docs/metrics/
19
+ #
20
+ # IT NEVER INVOKES GIT. The hooks directory is located by reading the filesystem
21
+ # (.git/ as a directory, or the `gitdir:` pointer when .git is a file), so this
22
+ # script runs identically whether a human or an agent triggered the refresh, and
23
+ # .tfcore/hooks/block-git.sh stays exactly as it is. Installing a hook is a file
24
+ # copy; it is not a git operation.
25
+ #
26
+ # Telemetry has no veto: this script prints warnings, never aborts its caller.
27
+ set -uo pipefail
28
+
29
+ TEMPLATE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
30
+
31
+ TARGET=""
32
+ PTYPE=""
33
+ DRY=0
34
+ QUIET=0
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --type) PTYPE="${2:-}"; shift 2 ;;
38
+ --dry-run) DRY=1; shift ;;
39
+ --quiet) QUIET=1; shift ;;
40
+ -h|--help) sed -n '2,20p' "$0"; exit 0 ;;
41
+ *) TARGET="$1"; shift ;;
42
+ esac
43
+ done
44
+
45
+ say() { [[ $QUIET -eq 1 ]] || echo "$@"; }
46
+
47
+ [[ -n "$TARGET" ]] || { echo "usage: install-metrics.sh <repo> [--type app|library|docs|framework] [--dry-run]" >&2; exit 1; }
48
+ [[ -d "$TARGET" ]] || { echo "not a directory: $TARGET" >&2; exit 1; }
49
+ cd "$TARGET" || exit 1
50
+ TARGET="$(pwd -P)"
51
+
52
+ [[ -d .tfcore ]] || { say " telemetry — no .tfcore/ here, skipped"; exit 0; }
53
+
54
+ CFG=".tfcore/core-config.yaml"
55
+
56
+ # --- 1. project_type -----------------------------------------------------
57
+ # Read whatever is already recorded. Once set, it is NEVER re-guessed — the
58
+ # owner's classification always wins over the heuristic.
59
+ EXISTING=""
60
+ if [[ -f "$CFG" ]]; then
61
+ EXISTING="$(sed -n '/^metrics:[[:space:]]*$/,/^[^[:space:]#]/p' "$CFG" 2>/dev/null \
62
+ | sed -n 's/^[[:space:]]\{1,\}project_type:[[:space:]]*["'"'"']\{0,1\}\([a-z]*\).*/\1/p' | head -1)"
63
+ fi
64
+
65
+ # Heuristic, run ONCE per repo and then never again (the answer is preserved in
66
+ # core-config.yaml). Kept deliberately cheap: build output and package caches are
67
+ # PRUNED and the walk is depth-limited, because an unpruned scan of a large repo on
68
+ # a /mnt/c mount takes minutes — and this runs inside every framework refresh.
69
+ _scan() { # _scan <maxdepth> <find-expr...> -> prints the first match, then stops
70
+ find . -maxdepth "$1" \
71
+ \( -type d \( -name bin -o -name obj -o -name node_modules -o -name .git \
72
+ -o -name .vs -o -name artifacts -o -name TestResults \) -prune \) \
73
+ -o "${@:2}" -print 2>/dev/null | head -1
74
+ }
75
+
76
+ _csprojs() {
77
+ find . -maxdepth 5 \
78
+ \( -type d \( -name bin -o -name obj -o -name node_modules -o -name .git \
79
+ -o -name .vs -o -name artifacts \) -prune \) \
80
+ -o -name '*.csproj' -print 2>/dev/null | head -60
81
+ }
82
+
83
+ detect_type() {
84
+ # framework: this repo DEPLOYS the framework rather than consuming it
85
+ [[ -f scaffold-brownfield.sh && -d .tfcore/tasks ]] && { echo framework; return; }
86
+
87
+ local projects packable heads name
88
+ projects="$(_csprojs)"
89
+
90
+ # docs: a markdown/spec repo — no source at all
91
+ [[ -z "$projects" ]] && { echo docs; return; }
92
+
93
+ # Packable = genuinely published. IsPackable is usually <IsPackable>false</IsPackable>
94
+ # on test projects, so only an explicit `true` counts.
95
+ packable="$(printf '%s\n' "$projects" | xargs -r grep -lE \
96
+ '<(PackageId>|GeneratePackageOnBuild>[[:space:]]*true|IsPackable>[[:space:]]*true)' 2>/dev/null)"
97
+
98
+ # A shipped executable/hostable head: console/desktop (Exe), MAUI, or ASP.NET/Blazor host.
99
+ heads="$(printf '%s\n' "$projects" | xargs -r grep -lE \
100
+ '<OutputType>[[:space:]]*Exe|<UseMaui>[[:space:]]*true|Sdk="Microsoft\.NET\.Sdk\.(Web|BlazorWebAssembly)"' 2>/dev/null)"
101
+
102
+ # THE DECIDING TEST: is a packaged project the product, or merely a helper?
103
+ # A library repo's package carries the repo's own name (TrBlazeUI.Components);
104
+ # an app's packable support libs do not (AstroLyfe ships AstroCore, SwissEphStd
105
+ # and is still an app). Name-match beats "has any packable project", which would
106
+ # misclassify every app that factors a NuGet-published helper out of its solution.
107
+ name="$(ls docs/*-Checklist.md 2>/dev/null | head -1)"
108
+ name="$(basename "${name:-$PWD}" 2>/dev/null)"; name="${name%-Checklist.md}"
109
+ [[ -z "$name" ]] && name="$(basename "$PWD")"
110
+ if [[ -n "$packable" ]] && printf '%s\n' "$packable" \
111
+ | grep -qiE "/${name}(\.[A-Za-z0-9._-]+)?\.csproj$"; then
112
+ echo library; return
113
+ fi
114
+
115
+ # A shipped head means it is an app, however many helper packages ride along.
116
+ [[ -n "$heads" ]] && { echo app; return; }
117
+
118
+ # No head at all, but something is published -> a library.
119
+ [[ -n "$packable" ]] && { echo library; return; }
120
+
121
+ # Last resorts: runtime screens -> app; otherwise assume app and let the owner correct.
122
+ [[ -n "$(_scan 6 \( -name '*.razor' -o -name '*.xaml' \))" ]] && { echo app; return; }
123
+ echo app
124
+ }
125
+
126
+ GUESSED=0
127
+ UPGRADED=0
128
+ if [[ -n "$PTYPE" ]]; then
129
+ :
130
+ elif [[ "$EXISTING" == "docs" ]]; then
131
+ # `docs` AT SCAFFOLD TIME IS NOT A CLASSIFICATION — IT IS THE ABSENCE OF ONE.
132
+ #
133
+ # scaffold-greenfield.sh runs this script at scaffold time, when the repo is by
134
+ # definition docs-only: the day-1 documents exist and src/ does not. detect_type()
135
+ # correctly answers `docs`, writes it, and the "auto-detected once, then never
136
+ # re-guessed" rule freezes it there. So EVERY greenfield project is born labelled
137
+ # `docs` and stays that way until somebody notices — which cost TfLens 225 gate
138
+ # records (visual gates included) filed under a project_type whose definition says
139
+ # gates cannot fire, and whose figures never pool with the apps it belongs beside.
140
+ #
141
+ # So `docs` alone is re-examined on a later refresh, and ONLY upgrades: once real
142
+ # heads or a published package appear, the tree has answered the question that was
143
+ # unanswerable at scaffold time. Everything else is left exactly as it was —
144
+ # app/library/framework are never re-guessed, and an owner's `--type` always wins
145
+ # (it takes the branch above). A genuine docs repo never grows a head, so it is
146
+ # never touched.
147
+ REDETECTED="$(detect_type)"
148
+ if [[ -n "$REDETECTED" && "$REDETECTED" != "docs" ]]; then
149
+ PTYPE="$REDETECTED"; GUESSED=1; UPGRADED=1
150
+ else
151
+ PTYPE="$EXISTING"
152
+ fi
153
+ elif [[ -n "$EXISTING" ]]; then
154
+ PTYPE="$EXISTING"
155
+ else
156
+ PTYPE="$(detect_type)"
157
+ GUESSED=1
158
+ fi
159
+
160
+ case "$PTYPE" in
161
+ app|library|docs|framework) ;;
162
+ *) echo " ⚠ telemetry — invalid project_type '$PTYPE' (app|library|docs|framework); leaving unset" >&2; PTYPE="$EXISTING" ;;
163
+ esac
164
+
165
+ # THE FRAMEWORK REPO CLASSIFIES ITSELF, AND NOTHING IS WRITTEN DOWN.
166
+ #
167
+ # This file is the template. scaffold-greenfield/brownfield copy .tfcore/ with
168
+ # `rsync -a --ignore-existing`, so core-config.yaml lands in a brand-new app
169
+ # verbatim — and an existing classification always wins over the heuristic
170
+ # (EXISTING above). Writing `project_type: framework` here would therefore stamp
171
+ # `framework` on every app scaffolded afterwards, silently, with no way for the
172
+ # owner to notice until a report pooled them wrongly.
173
+ #
174
+ # So the readers detect it structurally instead (tf-emit.sh _project_type(),
175
+ # tf-metrics.sh project_type()): scaffold-brownfield.sh beside .tfcore/tasks/ IS
176
+ # the framework template and nothing else is. Same answer, nothing to leak.
177
+ if [[ "$PTYPE" == "framework" && -f scaffold-brownfield.sh && -d .tfcore/tasks ]]; then
178
+ say " $CFG — project_type: framework, DETECTED from the tree, not recorded"
179
+ say " (this file is the scaffold template — writing it here would"
180
+ say " misclassify every app created from it)"
181
+ elif [[ -n "$PTYPE" && "$EXISTING" != "$PTYPE" ]]; then
182
+ if [[ $DRY -eq 1 ]]; then
183
+ if [[ $GUESSED -eq 1 ]]; then
184
+ say " $CFG — WOULD set metrics.project_type: $PTYPE (auto-detected)"
185
+ else
186
+ say " $CFG — WOULD set metrics.project_type: $PTYPE"
187
+ fi
188
+ elif [[ -n "$EXISTING" ]]; then
189
+ python3 - "$CFG" "$PTYPE" <<'PY' 2>/dev/null
190
+ import re, sys
191
+ p, t = sys.argv[1], sys.argv[2]
192
+ s = open(p, encoding="utf-8").read()
193
+ s = re.sub(r"(^metrics:[ \t]*\n(?:[ \t]+.*\n)*?[ \t]+project_type:[ \t]*)\S+",
194
+ lambda m: m.group(1) + t, s, count=1, flags=re.M)
195
+ open(p, "w", encoding="utf-8").write(s)
196
+ PY
197
+ if [[ $UPGRADED -eq 1 ]]; then
198
+ say " $CFG — metrics.project_type UPGRADED 'docs' → '$PTYPE'"
199
+ say " ('docs' was recorded at scaffold time, before this repo had any"
200
+ say " source. Records already written carry the old value — see"
201
+ say " .tfcore/telemetry/SCHEMA.md §1.)"
202
+ else
203
+ say " $CFG — metrics.project_type changed to '$PTYPE'"
204
+ fi
205
+ else
206
+ {
207
+ echo ""
208
+ echo "# metrics: telemetry classification. PRESERVED per-project — the framework"
209
+ echo "# scripts never overwrite core-config.yaml, so this survives every refresh."
210
+ echo "# app | library | docs | framework — see .tfcore/telemetry/SCHEMA.md §1."
211
+ echo "# Gate-catch metrics are only comparable between projects of the SAME type:"
212
+ echo "# a library has no screens, so it can never fail the visual-truth gate."
213
+ echo "metrics:"
214
+ echo " project_type: $PTYPE"
215
+ } >> "$CFG"
216
+ if [[ $GUESSED -eq 1 ]]; then
217
+ say " $CFG — metrics.project_type: $PTYPE (AUTO-DETECTED — correct it with:"
218
+ say " .tfcore/telemetry/install-metrics.sh . --type app|library|docs|framework)"
219
+ else
220
+ say " $CFG — metrics.project_type: $PTYPE"
221
+ fi
222
+ fi
223
+ fi
224
+
225
+ # --- 2. docs/metrics/ + the five streams ---------------------------------
226
+ if [[ $DRY -eq 1 ]]; then
227
+ [[ -d docs/metrics ]] && say " docs/metrics/ — present" \
228
+ || say " docs/metrics/ — WOULD create + seed runs/gates/sessions/commits/misses.jsonl"
229
+ else
230
+ mkdir -p docs/metrics
231
+ NEW=()
232
+ for s in runs gates sessions commits misses; do
233
+ [[ -f "docs/metrics/$s.jsonl" ]] || { : > "docs/metrics/$s.jsonl"; NEW+=("$s"); }
234
+ done
235
+ [[ ${#NEW[@]} -gt 0 ]] && say " docs/metrics/ — seeded ${NEW[*]}" || say " docs/metrics/ — streams present"
236
+ fi
237
+
238
+ # --- 3. docs/metrics/README.md -------------------------------------------
239
+ if [[ $DRY -eq 0 ]]; then
240
+ cat > docs/metrics/README.md <<'MD'
241
+ # docs/metrics — development telemetry
242
+
243
+ Append-only JSONL. **Tracked by git on purpose** — this is the project's own
244
+ development history, and it is the one thing the framework cannot reconstruct
245
+ after the fact.
246
+
247
+ | File | One record per | Written by |
248
+ |---|---|---|
249
+ | `runs.jsonl` | framework command run | the task, at completion |
250
+ | `gates.jsonl` | REQ verdict per verify run — **the primary stream** | `verify-phase` §6a, `triage-issues` |
251
+ | `sessions.jsonl` | agent session | the `SessionEnd` hook |
252
+ | `commits.jsonl` | commit | the repo's own `pre-commit` hook |
253
+ | `misses.jsonl` | a requirement/behaviour an agent MISSED, and what fixing it cost | `verify-phase`, `build-phase`, `triage-issues`, `fix-issues`, `amend-docs`, `*log-miss` |
254
+
255
+ `misses.jsonl` is the one stream with **three** record kinds — `miss` (opened: what
256
+ was missed, which phase/agent/model let it through, who found it), `miss-fix`
257
+ (closed: the repair run and its token/cost window, linked by `miss_id`) and
258
+ `miss-amend` (completes a field the `miss` left empty — it can fill a `null` and
259
+ can never overwrite a value, so it adds to the history without revising it). It is
260
+ what makes "how much did that miss cost to fix" answerable. SCHEMA.md §5.5.
261
+
262
+ Schema, enums, and every known limitation: `.tfcore/telemetry/SCHEMA.md`.
263
+ Report: `/TechieFlow:agents:flow-master *metrics <AppName>` (OpenCode: `/flow-master *metrics <AppName>`) → `METRICS.md`.
264
+
265
+ **All five files are created empty, on purpose, and an empty one is not a fault.**
266
+ The installer seeds the set so every repo has the same shape and no writer has to
267
+ guess whether its stream exists. A stream stays at zero bytes until something
268
+ actually happens: `gates.jsonl` until the first `*verify`, `runs.jsonl` until the
269
+ first framework command, `sessions.jsonl` until the first agent session ends, and
270
+ `commits.jsonl` until your first commit after telemetry was installed, and
271
+ `misses.jsonl` until an agent misses something (which it will). Commit
272
+ these empty files along with the rest — a tracked empty stream is what makes the
273
+ first record a one-line diff instead of a new file appearing from nowhere.
274
+
275
+ **Never edit these files by hand, never sort them, never compact them.** They are
276
+ a log. Rewriting one destroys exactly the history it exists to keep. To correct or
277
+ complete a record, append another one: a later `gates.jsonl` record supersedes an
278
+ earlier verdict, a `miss-fix` closes a `miss`, and
279
+ `bash .tfcore/utils/tf-emit.sh --amend <miss_id> <field> <value>` fills a field a
280
+ `miss` left empty (it refuses to overwrite one that is not empty). If nothing fits
281
+ what you need to correct, say so rather than editing — that is a framework defect
282
+ worth reporting, and it is how the amend path came to exist.
283
+
284
+ **No secrets, no content, no client data** — records carry IDs, counts, durations,
285
+ verdicts and file paths at most. Never requirement text, prompt text, file
286
+ contents, or commit subjects. Assume every line here could become public.
287
+
288
+ ## Working on more than one machine
289
+
290
+ `.gitattributes` gives these streams `merge=union`, so two machines appending to
291
+ the same file keep **both** sides' lines instead of conflicting. Pull, push, carry
292
+ on — you never hand-resolve a log, which is the one way records get silently
293
+ dropped. Union merge can leave a record duplicated or out of chronological order;
294
+ every consumer sorts on `ts` and de-duplicates commits on `sha`, so neither costs
295
+ you anything.
296
+
297
+ **`commits.jsonl` needs no collecting.** The `pre-commit` hook *reconciles*: it
298
+ writes a record for every commit reachable from HEAD that the file does not
299
+ already have, then stages that one file so the records ship **inside** the commit
300
+ you are making. So after you pull another machine's work, your next commit here
301
+ records all of it. The commit log is itself an append-only log that push and pull
302
+ already replicate everywhere; this stream is a projection of it.
303
+
304
+ Three things worth knowing:
305
+
306
+ - **It stages exactly one path** — `docs/metrics/commits.jsonl`, nothing else. On
307
+ a partial commit (`git commit -- <paths>`) it writes the record but does **not**
308
+ stage, so it can never smuggle a file into a commit you deliberately scoped.
309
+ - **The lag is one commit, and it is committed rather than pending.** At
310
+ pre-commit time HEAD is still the previous commit, so the record for the commit
311
+ you are making ships in the next one. Your working tree is clean when the commit
312
+ finishes — that is the whole reason this is a pre-commit hook and not a
313
+ post-commit one.
314
+ - The hook lives in `.git/hooks/`, which is **not** part of the repository, so
315
+ every clone needs its own. `update-framework.sh <repo>` installs it, and
316
+ `tf-metrics.sh --report` warns when the clone you are standing in has none. If
317
+ you already have your own `pre-commit` hook, the installer leaves it alone and
318
+ tells you — add `bash .tfcore/telemetry/pre-commit` to it if you want both.
319
+
320
+ Merge commits, `--no-verify`, rebases and cherry-picks skip the hook entirely.
321
+ Nothing is lost: reconciling means the next ordinary commit — here or on any
322
+ machine that pulls — notices those commits are missing and writes them.
323
+
324
+ To fill in a machine's history immediately rather than waiting for a commit:
325
+
326
+ .tfcore/telemetry/tf-metrics.sh --backfill-commits .
327
+
328
+ Idempotent — already-recorded shas are skipped — so run it as often as you like.
329
+ It is also why the hook is optional: delete it and reconcile by hand instead.
330
+ MD
331
+ fi
332
+
333
+ # --- 4. pre-commit hook — located WITHOUT invoking git -------------------
334
+ # Since 2026-08-11 the commit-telemetry hook is **pre**-commit, so the record
335
+ # ships INSIDE the commit and the working tree is clean afterwards. The old
336
+ # post-commit hook wrote its line after the commit was sealed, which left
337
+ # commits.jsonl permanently dirty with no reachable clean state. Our own
338
+ # post-commit is therefore REMOVED here; a foreign one is never touched.
339
+ HOOKS_DIR=""
340
+ if [[ -d .git ]]; then
341
+ HOOKS_DIR=".git/hooks"
342
+ elif [[ -f .git ]]; then
343
+ # worktree / submodule: ".git" is a file containing "gitdir: <path>"
344
+ GD="$(sed -n 's/^gitdir:[[:space:]]*//p' .git | head -1)"
345
+ [[ -n "$GD" ]] && { [[ "$GD" = /* ]] || GD="$TARGET/$GD"; [[ -d "$GD" ]] && HOOKS_DIR="$GD/hooks"; }
346
+ fi
347
+
348
+ if [[ -z "$HOOKS_DIR" ]]; then
349
+ say " pre-commit — not a work tree yet; commits.jsonl stays empty (re-run the updater after init)"
350
+ else
351
+ SRC="$TEMPLATE/.tfcore/telemetry/pre-commit"
352
+ DEST="$HOOKS_DIR/pre-commit"
353
+ OLD="$HOOKS_DIR/post-commit"
354
+
355
+ # 4a. retire OUR post-commit. Identified by the template's own marker, so a
356
+ # hook you wrote yourself is left exactly where it is.
357
+ if [[ -f "$OLD" ]] && grep -q 'TechieFlow telemetry' "$OLD" 2>/dev/null; then
358
+ if [[ $DRY -eq 1 ]]; then
359
+ say " $OLD — WOULD remove (superseded by pre-commit)"
360
+ else
361
+ rm -f "$OLD" && say " $OLD — removed (superseded by pre-commit)"
362
+ fi
363
+ fi
364
+
365
+ # 4b. install pre-commit. A pre-commit hook is where people put lint and test
366
+ # gates, so unlike the old post-commit path this NEVER replaces one it did
367
+ # not write. Losing someone's lint gate to a telemetry install would be a
368
+ # far worse outcome than not collecting commit records.
369
+ if [[ -f "$DEST" ]] && ! grep -q 'TechieFlow telemetry' "$DEST" 2>/dev/null; then
370
+ echo " ⚠ $DEST already exists and is NOT the TechieFlow hook — left untouched."
371
+ echo " Commit telemetry is not installed here. To have both, add this line to"
372
+ echo " your hook: bash .tfcore/telemetry/pre-commit"
373
+ echo " (or reconcile by hand: tf-metrics.sh --backfill-commits .)"
374
+ elif [[ -f "$DEST" ]] && cmp -s "$SRC" "$DEST"; then
375
+ say " $DEST — already current"
376
+ elif [[ $DRY -eq 1 ]]; then
377
+ say " $DEST — WOULD install the commit-telemetry hook"
378
+ else
379
+ mkdir -p "$HOOKS_DIR" 2>/dev/null
380
+ cp "$SRC" "$DEST" && chmod +x "$DEST" && say " $DEST — installed"
381
+ fi
382
+ fi
383
+
384
+ # --- 5. the data must stay TRACKED --------------------------------------
385
+ if [[ -f .gitignore ]] && tr -d '\r' < .gitignore | grep -qE '^/?docs/?$|^/?docs/metrics'; then
386
+ echo " ⚠ .gitignore has a pattern that would swallow docs/metrics/ — the telemetry data"
387
+ echo " MUST be tracked. Add an explicit negation: !docs/metrics/ and !docs/metrics/**"
388
+ fi
389
+
390
+ exit 0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow telemetry — pre-commit hook TEMPLATE.
3
+ # Installed into <app>/.git/hooks/pre-commit by .tfcore/telemetry/install-metrics.sh.
4
+ # It REPLACES the older post-commit hook (which that installer removes).
5
+ #
6
+ # WHY PRE-COMMIT, WHEN THE DESIGN SAID POST-COMMIT (owner decision, 2026-08-11).
7
+ #
8
+ # post-commit can only write the record for commit N *after* N exists, so that
9
+ # line can never be inside N. The file was therefore dirty the instant every
10
+ # commit finished — permanently, with no reachable clean state: committing the
11
+ # pending line creates a new commit, whose record is then pending in turn.
12
+ # "Just commit it first" is circular advice, and on a repo worked from more
13
+ # than one machine it also blocks `git pull` every time.
14
+ #
15
+ # Writing before the commit is sealed fixes that, at the price this hook is
16
+ # honest about: it stages one file into the commit you are making.
17
+ #
18
+ # WHAT IT STAGES, AND NOTHING ELSE: docs/metrics/commits.jsonl
19
+ #
20
+ # Not `git add -A`, not a directory, not anything you did not already have.
21
+ # If that file is unchanged the add is a no-op. It cannot introduce source
22
+ # changes into a commit, because it only ever touches this one path.
23
+ #
24
+ # THE LAG MOVES BUT DOES NOT GO AWAY. At pre-commit time HEAD is still the
25
+ # PREVIOUS commit, so this records everything up to N-1; commit N's own record
26
+ # ships inside N+1. The difference from before is that the record is *committed*
27
+ # rather than *pending* — the working tree is clean when the commit finishes.
28
+ #
29
+ # EVERY SKIP IS SELF-HEALING, because this reconciles from the log rather than
30
+ # appending one line. Merge commits (git runs pre-merge-commit instead),
31
+ # `--no-verify`, rebase, cherry-pick and partial commits all record nothing here
32
+ # — and the next ordinary commit, on this machine or any other that pulls, finds
33
+ # them missing from the file and writes them. Nothing needs to be caught up by
34
+ # hand, ever.
35
+ #
36
+ # NEVER STORES THE COMMIT SUBJECT — only its conventional-commit prefix, if any.
37
+ #
38
+ # IT NEVER FAILS YOUR COMMIT. A pre-commit hook that exits non-zero ABORTS the
39
+ # commit, and telemetry has no veto in this framework (.tfcore/tasks/
40
+ # _metrics-emit-gate.md). Every path here ends in `exit 0`.
41
+
42
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
43
+ [[ -n "$ROOT" ]] || exit 0
44
+ EMIT="$ROOT/.tfcore/utils/tf-emit.sh"
45
+ [[ -f "$EMIT" ]] || exit 0
46
+ STREAM="docs/metrics/commits.jsonl"
47
+
48
+ # --- 1. reconcile the stream against the log ------------------------------
49
+ RECONCILED=0
50
+ METRICS="$ROOT/.tfcore/telemetry/tf-metrics.sh"
51
+ LIMIT="${TF_COMMIT_RECONCILE_LIMIT:-500}"
52
+ if [[ -f "$METRICS" ]] && command -v python3 >/dev/null 2>&1; then
53
+ bash "$METRICS" --backfill-commits "$ROOT" --limit "$LIMIT" --quiet >/dev/null 2>&1 && RECONCILED=1
54
+ fi
55
+
56
+ if [[ $RECONCILED -eq 0 ]]; then
57
+ # Fallback: record HEAD only, in pure bash. Reached when python3 is absent.
58
+ # HEAD is the PREVIOUS commit here — that is the one we can describe.
59
+ SHA="$(git rev-parse --short HEAD 2>/dev/null)"
60
+ if [[ -n "$SHA" ]] && ! grep -q "\"sha\":\"$SHA\"" "$ROOT/$STREAM" 2>/dev/null; then
61
+ BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || BRANCH="detached"
62
+ WHEN="$(TZ=UTC0 git log -1 --date=format-local:%Y-%m-%dT%H:%M:%SZ --format=%cd 2>/dev/null)"
63
+ SUBJ="$(git log -1 --format=%s 2>/dev/null)"
64
+ STAT="$(git show --shortstat --format= HEAD 2>/dev/null | tail -1)"
65
+
66
+ # Conventional-commit prefix ONLY — the rest of the subject is discarded here
67
+ # and never leaves this script.
68
+ PREFIX="$(printf '%s' "$SUBJ" | sed -n 's/^\(feat\|fix\|docs\|chore\|refactor\|test\|build\)\([(!: ].*\)\?$/\1/p' | head -1)"
69
+ FILES="$(printf '%s' "$STAT" | grep -oE '[0-9]+ files? changed' | grep -oE '^[0-9]+')"
70
+ INS="$(printf '%s' "$STAT" | grep -oE '[0-9]+ insertions?' | grep -oE '^[0-9]+')"
71
+ DEL="$(printf '%s' "$STAT" | grep -oE '[0-9]+ deletions?' | grep -oE '^[0-9]+')"
72
+ : "${FILES:=0}" "${INS:=0}" "${DEL:=0}"
73
+ if [[ -n "$PREFIX" ]]; then PJ="\"$PREFIX\""; else PJ="null"; fi
74
+ TS_FIELD=""
75
+ [[ -n "$WHEN" ]] && TS_FIELD="\"ts\":\"$WHEN\","
76
+
77
+ printf '{%s"kind":"commit","sha":"%s","files":%s,"insertions":%s,"deletions":%s,"subject_prefix":%s,"branch":"%s"}\n' \
78
+ "$TS_FIELD" "$SHA" "$FILES" "$INS" "$DEL" "$PJ" "$BRANCH" \
79
+ | TF_METRICS_ROOT="$ROOT" bash "$EMIT" commits >/dev/null 2>&1
80
+ fi
81
+ fi
82
+
83
+ # --- 2. stage it, but ONLY on a full commit -------------------------------
84
+ # A partial commit (`git commit -- <paths>`, and some GUI flows) runs this hook
85
+ # against a TEMPORARY index; staging into it means quietly adding a file the
86
+ # author explicitly scoped out, and git's handling of hook-made changes to that
87
+ # temp index is not something to rely on. So: write the record either way, stage
88
+ # only when this is a normal, whole-index commit. The unstaged record is not
89
+ # lost — the next ordinary commit reconciles and stages it.
90
+ case "${GIT_INDEX_FILE:-}" in
91
+ ""|.git/index|*/.git/index) git add -- "$STREAM" >/dev/null 2>&1 ;;
92
+ esac
93
+
94
+ exit 0