@techierathore/techieflow 1.0.1 → 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 (178) 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 +27 -7
  151. package/opencode.jsonc +13 -33
  152. package/package.json +4 -7
  153. package/scripts/install.mjs +73 -46
  154. package/scripts/npm-cleanup.mjs +137 -0
  155. package/scripts/npm-postinstall.mjs +49 -0
  156. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  157. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  158. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  159. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  160. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  161. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  162. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  163. package/.codex/config.toml +0 -46
  164. package/.codex/hooks.json +0 -63
  165. package/.codex/rules/techieflow.rules +0 -8
  166. package/.tfcore/hooks/codex-adapter.py +0 -248
  167. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  168. package/.tfcore/tasks/author-brd.md +0 -79
  169. package/.tfcore/tasks/document-project.md +0 -343
  170. package/.tfcore/tasks/execute-checklist.md +0 -86
  171. package/.tfcore/tasks/index-docs.md +0 -173
  172. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  173. package/.tfcore/tasks/shard-doc.md +0 -185
  174. package/.tfcore/utils/tf-codex-bind.py +0 -196
  175. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  176. package/WORKFLOW.html +0 -1986
  177. package/scripts/test-install.mjs +0 -505
  178. package/scripts/validate.mjs +0 -163
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env python3
2
+ """tf-log-miss.py — one miss record from one sentence (see tf-log-miss.sh).
3
+
4
+ bash .tfcore/utils/tf-log-miss.sh <App> --what "<the owner's sentence>" --sort spec|unsaid|weak-check|ignored
5
+ [--req REQ-UI-014 | --new "<title>" --acceptance "<When … on <screen>, then …>" [--prefix FN] [--section "<screen>"]]
6
+ [--class <miss_class>] [--why <why_missed>] [--artifact <artifact>] [--severity blocker|major|minor]
7
+ [--found-by owner|production] [--evidence <path>] [--fixed [--fix-run <ISO>]] [--started <ISO>]
8
+
9
+ First the sort (FR-32): --sort answers the four questions, in order, and is required.
10
+ spec the app's spec did not say it -> fix the checklist line; the framework is untouched
11
+ unsaid the framework never said it -> one requirement line plus a check; no prose in a task
12
+ weak-check a check existed and did not catch it -> fix the check (a review becomes a script)
13
+ ignored it was written and not followed -> a hook or script, or delete the rule
14
+ Then, in order: the duplicate check (an open miss of the same class on the row is reported, not
15
+ re-logged; a null why_missed or sort on it is completed with --amend); the origin lookup
16
+ (tf-emit.sh --origin-of); the miss record with the sentence as `what` and the sort; the checklist line
17
+ (the row demoted to Needs re-verify with "⚠ miss <date>: <sentence>", or a new Not Started row with
18
+ BRD-pending when nothing owns it; nothing when the repo has no checklist); with --fixed, a miss-fix
19
+ record instead of a demotion; then the run record (cmd log-miss). The emitter rewrites the readable
20
+ docs/<App>-Misses.md. Prints the report block. Exit 0 · 2 could not run.
21
+ """
22
+ import datetime
23
+ import importlib.util
24
+ import json
25
+ import os
26
+ import re
27
+ import subprocess
28
+ import sys
29
+
30
+ HERE = os.path.dirname(os.path.abspath(__file__))
31
+ EMIT = os.path.join(HERE, "tf-emit.sh")
32
+ CLASSES = {"missed-requirement", "partial-implementation", "unspecified-gap", "regression", "wrong-behaviour", "standards-violation", "spec-contradiction", "other"}
33
+ DEFAULT_ARTIFACT = {"unspecified-gap": "brd", "spec-contradiction": "brd", "standards-violation": "src"}
34
+ SORTS = ("spec", "unsaid", "weak-check", "ignored")
35
+ SORT_QUESTIONS = """tf-log-miss: --sort is missing. Answer the four questions in order and pass the first that fits:
36
+ 1. Did the app's spec say it clearly? no -> --sort spec (fix the checklist line)
37
+ 2. Did the framework say it anywhere? no -> --sort unsaid (one requirement line plus a check)
38
+ 3. Was there a check, and did it fail to catch it? yes -> --sort weak-check (fix the check)
39
+ 4. Was it written and ignored anyway? yes -> --sort ignored (a hook, or delete the rule)"""
40
+
41
+
42
+ def die(m):
43
+ print(f"tf-log-miss: {m}", file=sys.stderr)
44
+ sys.exit(2)
45
+
46
+
47
+ def opt(argv, name, default=None):
48
+ return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else default
49
+
50
+
51
+ def q(*args):
52
+ return subprocess.run(["bash", EMIT, *args], capture_output=True, text=True).stdout.strip()
53
+
54
+
55
+ def emit(stream, rec):
56
+ """Append one record. Returns (ok, reason) — the reason is what the emitter refused it for,
57
+ so the caller can say the miss was NOT recorded instead of claiming it was (miss 06 of
58
+ 2026-09-07: a refused artifact value printed 'Miss logged' and an id that existed nowhere)."""
59
+ p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
60
+ msg = (p.stdout + p.stderr).strip()
61
+ ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
62
+ reason = ""
63
+ if not ok:
64
+ reason = msg.splitlines()[0][:200] if msg else "no reason printed"
65
+ print(f" {stream}: not written — {reason}")
66
+ return ok, reason
67
+
68
+
69
+ def main(argv):
70
+ if len(argv) < 2 or argv[1] in ("-h", "--help") or "--what" not in argv:
71
+ print(__doc__)
72
+ return 0 if "-h" in argv or "--help" in argv else 2
73
+ app = argv[1]
74
+ what = opt(argv, "--what", "").strip()
75
+ if not what:
76
+ die("--what needs the sentence")
77
+ sort = opt(argv, "--sort")
78
+ if not sort:
79
+ print(SORT_QUESTIONS, file=sys.stderr)
80
+ return 2
81
+ if sort not in SORTS:
82
+ die(f"--sort must be one of {', '.join(SORTS)}")
83
+ req = (opt(argv, "--req") or "").upper() or None
84
+ new_title = opt(argv, "--new")
85
+ cls = opt(argv, "--class") or ("unspecified-gap" if new_title else "wrong-behaviour")
86
+ if cls not in CLASSES:
87
+ die(f"--class must be one of {', '.join(sorted(CLASSES))}")
88
+ why = opt(argv, "--why")
89
+ artifact = opt(argv, "--artifact") or DEFAULT_ARTIFACT.get(cls, "src")
90
+ severity = opt(argv, "--severity", "major")
91
+ found_by = opt(argv, "--found-by", "owner")
92
+ fixed = "--fixed" in argv
93
+ started = opt(argv, "--started") or ""
94
+ if not started:
95
+ mk = subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
96
+ m = re.search(r'"started":"([^"]*)"', mk)
97
+ started = m.group(1) if m else datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
98
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
99
+
100
+ spec = importlib.util.spec_from_file_location("tf_checklist_edit", os.path.join(HERE, "tf-checklist-edit.py"))
101
+ ce = importlib.util.module_from_spec(spec)
102
+ spec.loader.exec_module(ce)
103
+ phase = 1
104
+ cfg = os.path.join(".tfcore", "core-config.yaml")
105
+ if os.path.isfile(cfg):
106
+ m = re.search(r"(?m)^appPhase:\s*(\d+)", open(cfg, encoding="utf-8").read())
107
+ phase = int(m.group(1)) if m else 1
108
+ cl = os.path.join("docs", f"{app}-Checklist.md" if phase <= 1 else f"{app}-P{phase}-Checklist.md")
109
+ has_cl = os.path.isfile(cl)
110
+
111
+ # 1. duplicate check
112
+ if req:
113
+ opened = q("--open-miss", req)
114
+ if opened and opened.split()[-1] == cls:
115
+ mid = opened.split()[0]
116
+ line = f"already logged as {mid}, still open."
117
+ for fld, val in (("why_missed", why), ("sort", sort)):
118
+ if val:
119
+ r = q("--amend", mid, fld, val)
120
+ line += f" {fld} completed with {val}." if "refus" not in r.lower() else f" ({fld} not amended: {r.split('—')[-1].strip()[:90]})"
121
+ print(f"# Miss not re-logged — {app}\n{line}")
122
+ return 0
123
+
124
+ # 2. the checklist line
125
+ doc_line = "no checklist in this repo — record only"
126
+ if has_cl:
127
+ if req:
128
+ if fixed:
129
+ doc_line = f"{req} left as it is (--fixed)"
130
+ else:
131
+ prior = ce.demote(cl, req, what, evidence=opt(argv, "--evidence"), source=found_by,
132
+ prefix="⚠ prod bug" if found_by == "production" else "⚠ miss")
133
+ doc_line = f"{req} demoted to Needs re-verify (was {prior['status']})" if prior else f"{req} is not a row of {cl}; record only"
134
+ elif new_title:
135
+ acc = opt(argv, "--acceptance") or die("--new needs --acceptance")
136
+ req = ce.add_row(cl, opt(argv, "--prefix", "FN").upper(), new_title, acc, section=opt(argv, "--section"),
137
+ mockup=opt(argv, "--mockup"), remark=f"logged via *log-miss {ce.TODAY}")
138
+ doc_line = f"new Not Started row {req} (BRD-pending; *amend-docs gives it its BRD item)"
139
+ else:
140
+ doc_line = "no owning row and no --new: record only"
141
+
142
+ # 3. the miss
143
+ mid = q("--next-miss-id")
144
+ rec = {"kind": "miss", "miss_id": mid, "req_id": req, "req_class": req.split("-")[1] if req else None, "miss_class": cls,
145
+ "artifact": artifact, "severity": severity, "why_missed": why, "origin_phase": "day1-greenfield" if cls in ("unspecified-gap", "spec-contradiction") else "build-phase",
146
+ "origin_agent": "analyst" if cls in ("unspecified-gap", "spec-contradiction") else "flow-master",
147
+ "found_by": found_by, "found_phase": "log-miss", "found_gate": None, "found_run_id": started, "failure_class": "other", "what": what[:300],
148
+ "sort": sort}
149
+ attributed = "inferred, no run record found"
150
+ if req and cls not in ("unspecified-gap", "spec-contradiction"):
151
+ o = q("--origin-of", req).split()
152
+ if len(o) >= 3:
153
+ rec["origin_run_id"], rec["origin_phase"], rec["origin_agent"] = o[0], o[1], o[2]
154
+ attributed = f"linked to run {o[0]}"
155
+ ok, refusal = emit("misses", rec)
156
+
157
+ # 4. --fixed
158
+ fix_line = ""
159
+ if fixed and ok:
160
+ fa = q("--next-fix-attempt", mid) or "1"
161
+ fr = {"kind": "miss-fix", "miss_id": mid, "req_id": req, "fix_cmd": "log-miss", "fix_attempt": int(fa) if fa.isdigit() else 1,
162
+ "verdict_after": "Verified", "reopened": False}
163
+ if opt(argv, "--fix-run"):
164
+ fr["fix_run_id"] = opt(argv, "--fix-run")
165
+ fix_ok, _ = emit("misses", fr)
166
+ fix_line = ("closed in the same run" + (f" against run {fr['fix_run_id']}" if "fix_run_id" in fr else " (fix run unknown: costed none)")) if fix_ok else "miss-fix not written"
167
+
168
+ # 5. the run record
169
+ run = {"kind": "run", "app": app, "cmd": "log-miss", "mode": None, "started": started, "ended": now,
170
+ "reqs_touched": [req] if req else [], "reqs_count": 1 if req else 0, "subagents": [], "files_written": 1 if has_cl else 0, "build_result": "not-run"}
171
+ emit("runs", run)
172
+
173
+ if not ok:
174
+ print(f"# Miss NOT recorded — {app}")
175
+ print(f"Refused : {refusal}")
176
+ print(f"Nothing carries the id {mid}: the stream has no record and docs/{app}-Misses.md is unchanged.")
177
+ print(f"Docs : {doc_line}")
178
+ print("Next : fix the value the emitter named and run the same command again.")
179
+ return 2
180
+
181
+ print(f"# Miss logged — {app}")
182
+ print(f"{mid} {cls} / {artifact} / {severity}")
183
+ print(f"Why missed : {why or 'not assessed'}")
184
+ print(f"Whose gap : {sort} — " + {"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",
185
+ "weak-check": "the check was too weak; fix the check", "ignored": "said and ignored; a hook, or delete the rule"}[sort])
186
+ print(f"Owning REQ : {req or 'none'}")
187
+ print(f"Attributed : {rec['origin_phase']} ({rec['origin_agent']}) — {attributed}")
188
+ print(f"Found by : {found_by}")
189
+ print(f"Docs : {doc_line}; docs/{app}-Misses.md rewritten")
190
+ if fix_line:
191
+ print(f"Fix : {fix_line}")
192
+ print(f"Next : *fix-issues {app}" + (f" {req}" if req and not fixed else ""))
193
+ return 0
194
+
195
+
196
+ if __name__ == "__main__":
197
+ sys.exit(main(sys.argv))
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # tf-log-miss.sh — one miss record from one sentence (Sitting 4c, 2026-09-06).
3
+ #
4
+ # bash .tfcore/utils/tf-log-miss.sh <App> --what "<the owner's sentence>" [--req REQ-UI-014 | --new "<title>" --acceptance "<When … on <screen>, then …>"]
5
+ # [--class <miss_class>] [--why <why_missed>] [--artifact <artifact>] [--severity blocker|major|minor]
6
+ # [--found-by owner|production] [--evidence <path>] [--fixed [--fix-run <ISO>]] [--started <ISO>]
7
+ #
8
+ # Duplicate check, origin lookup, the miss record carrying the sentence, the checklist line (a demotion,
9
+ # or a new Not Started row with BRD-pending), a miss-fix instead of a demotion with --fixed, then the run
10
+ # record. Never boots, never fixes, never argues. Details: tf-log-miss.py.
11
+ set -u
12
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ command -v python3 >/dev/null 2>&1 || { echo "tf-log-miss: python3 is required" >&2; exit 2; }
14
+ exec python3 "$HERE/tf-log-miss.py" "$@"
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """tf-misses-md.py — the readable miss list, docs/<App>-Misses.md (Session 5, 2026-09-07; FR-31).
3
+
4
+ python3 .tfcore/utils/tf-misses-md.py [--root <repo>] [--app <App>] [--no-html] [--quiet]
5
+
6
+ Rebuilds docs/<App>-Misses.md from docs/metrics/misses.jsonl: one row per `miss` record, newest first,
7
+ in three tables (open, fixed, will not fix), each row holding the miss id, the owning row, when and
8
+ by whom it was found, whose gap it was (the `sort` field, folded from any miss-amend) and the `what`
9
+ sentence. Then renders the sibling HTML through tf-render-html.py. tf-emit.sh calls this after every
10
+ write to the misses stream, so the file is never older than the record; run it by hand once on a
11
+ project whose stream predates it. The file is derived: never edit it, edit nothing, log a miss.
12
+ Exit 0 always (telemetry has no veto); the reason for a skipped write is printed unless --quiet.
13
+ """
14
+ import datetime
15
+ import glob
16
+ import json
17
+ import os
18
+ import re
19
+ import subprocess
20
+ import sys
21
+
22
+ HERE = os.path.dirname(os.path.abspath(__file__))
23
+
24
+ SORT_WORDS = {
25
+ "spec": "the app's spec",
26
+ "unsaid": "the framework never said it",
27
+ "weak-check": "the check was too weak",
28
+ "ignored": "said and ignored",
29
+ }
30
+ SORT_LEGEND = ("**Whose gap** answers the four questions of the miss protocol: "
31
+ "**the app's spec** did not say it, so the checklist line is fixed; "
32
+ "**the framework never said it**, so one requirement line and a check are added; "
33
+ "**the check was too weak** (a review, or a script that did not fire), so the check is fixed; "
34
+ "**said and ignored**, so the rule becomes a hook or is deleted. "
35
+ "**not sorted** means the record predates the sort or nobody has answered yet; "
36
+ "`bash .tfcore/utils/tf-emit.sh --amend <miss> sort <spec|unsaid|weak-check|ignored>` completes it.")
37
+
38
+
39
+ def find_root(explicit):
40
+ if explicit and os.path.isdir(explicit):
41
+ return os.path.abspath(explicit)
42
+ for k in ("TF_METRICS_ROOT", "CLAUDE_PROJECT_DIR"):
43
+ v = os.environ.get(k)
44
+ if v and os.path.isdir(v):
45
+ return os.path.abspath(v)
46
+ d = os.getcwd()
47
+ while d and d != os.path.dirname(d):
48
+ if os.path.isdir(os.path.join(d, ".tfcore")) or os.path.isdir(os.path.join(d, "docs")):
49
+ return d
50
+ d = os.path.dirname(d)
51
+ return os.getcwd()
52
+
53
+
54
+ def app_name(root, explicit):
55
+ if explicit:
56
+ return explicit
57
+ hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
58
+ if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)]
59
+ if len(hits) == 1:
60
+ return os.path.basename(hits[0])[: -len("-Checklist.md")]
61
+ return os.path.basename(os.path.abspath(root))
62
+
63
+
64
+ def load(path):
65
+ out = []
66
+ try:
67
+ with open(path, encoding="utf-8") as fh:
68
+ for line in fh:
69
+ line = line.strip()
70
+ if not line:
71
+ continue
72
+ try:
73
+ out.append(json.loads(line))
74
+ except Exception:
75
+ pass
76
+ except FileNotFoundError:
77
+ pass
78
+ return out
79
+
80
+
81
+ def cell(text):
82
+ text = (text or "").replace("\r", " ").replace("\n", " ").replace("|", "\\|").strip()
83
+ return text or "—"
84
+
85
+
86
+ def day(ts):
87
+ return (ts or "")[:10] or "—"
88
+
89
+
90
+ def build(root, app):
91
+ recs = load(os.path.join(root, "docs", "metrics", "misses.jsonl"))
92
+ misses = [r for r in recs if r.get("kind") == "miss"]
93
+ by_id = {}
94
+ for i, m in enumerate(misses):
95
+ if not m.get("miss_id"): # a record from before the emitter required an id: still a miss, still a row
96
+ m["miss_id"] = f"(no id, record {i + 1})"
97
+ by_id.setdefault(m["miss_id"], m)
98
+ for a in sorted((r for r in recs if r.get("kind") == "miss-amend"), key=lambda r: r.get("ts") or ""):
99
+ p = by_id.get(a.get("miss_id"))
100
+ if p is not None and a.get("field") and p.get(a["field"]) is None:
101
+ p[a["field"]] = a.get("value")
102
+ latest = {}
103
+ for f in recs:
104
+ if f.get("kind") != "miss-fix" or not f.get("miss_id"):
105
+ continue
106
+ prev = latest.get(f["miss_id"])
107
+ if prev is None or (f.get("ts") or "") >= (prev.get("ts") or ""):
108
+ latest[f["miss_id"]] = f
109
+
110
+ def state(m):
111
+ v = (latest.get(m["miss_id"]) or {}).get("verdict_after")
112
+ return "fixed" if v == "Verified" else "wont-fix" if v == "wont-fix" else "open"
113
+
114
+ groups = {"open": [], "fixed": [], "wont-fix": []}
115
+ for m in sorted(by_id.values(), key=lambda r: (r.get("miss_id") or "", r.get("ts") or ""), reverse=True):
116
+ groups[state(m)].append(m)
117
+
118
+ def what(m):
119
+ w = (m.get("what") or "").strip()
120
+ if w:
121
+ return cell(w)
122
+ bits = [m.get("miss_class") or "?", m.get("artifact") or "?"]
123
+ if m.get("why_missed"):
124
+ bits.append("why: " + m["why_missed"])
125
+ return "no sentence recorded (" + ", ".join(bits) + ")"
126
+
127
+ def found(m):
128
+ who = m.get("found_by") or "?"
129
+ return f"{day(m.get('ts'))} by {who}"
130
+
131
+ def gap(m):
132
+ return SORT_WORDS.get(m.get("sort") or "", "not sorted")
133
+
134
+ def ident(m):
135
+ return m["miss_id"] + (f" ({m['req_id']})" if m.get("req_id") else "")
136
+
137
+ today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
138
+ n = len(by_id)
139
+ lines = [f"# {app} — Misses", "", "| | |", "|---|---|", f"| App | {app} |",
140
+ f"| Count | {n} logged: {len(groups['open'])} open, {len(groups['fixed'])} fixed, {len(groups['wont-fix'])} will not fix |",
141
+ "| Source | `docs/metrics/misses.jsonl`, one row per miss record. Rewritten by `tf-misses-md.sh` on every new record. Never edit it: a wrong row is corrected by a new record. |",
142
+ f"| Updated | {today} |", "", SORT_LEGEND, ""]
143
+ if not n:
144
+ lines += ["No miss has been logged yet.", ""]
145
+ for key, title in (("open", "Open"), ("fixed", "Fixed"), ("wont-fix", "Will not fix")):
146
+ rows = groups[key]
147
+ if not rows:
148
+ continue
149
+ lines += [f"## {title} ({len(rows)})", ""]
150
+ if key == "open":
151
+ lines += ["| Miss | Found | Whose gap | What went wrong |", "|---|---|---|---|"]
152
+ for m in rows:
153
+ lines.append(f"| {ident(m)} | {found(m)} | {gap(m)} | {what(m)} |")
154
+ else:
155
+ lines += ["| Miss | Found | Closed | Whose gap | What went wrong |", "|---|---|---|---|---|"]
156
+ for m in rows:
157
+ f = latest.get(m["miss_id"]) or {}
158
+ closed = day(f.get("ts")) + (f" by {f['fix_cmd']}" if f.get("fix_cmd") else "")
159
+ lines.append(f"| {ident(m)} | {found(m)} | {closed} | {gap(m)} | {what(m)} |")
160
+ lines.append("")
161
+ return "\n".join(lines), n
162
+
163
+
164
+ def main(argv):
165
+ if "-h" in argv or "--help" in argv:
166
+ print(__doc__)
167
+ return 0
168
+ quiet = "--quiet" in argv
169
+
170
+ def opt(name):
171
+ return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else None
172
+
173
+ try:
174
+ root = find_root(opt("--root"))
175
+ app = app_name(root, opt("--app"))
176
+ text, n = build(root, app)
177
+ rel = os.path.join("docs", f"{app}-Misses.md")
178
+ out = os.path.join(root, rel)
179
+ os.makedirs(os.path.dirname(out), exist_ok=True)
180
+ old = None
181
+ try:
182
+ with open(out, encoding="utf-8") as fh:
183
+ old = fh.read()
184
+ except FileNotFoundError:
185
+ pass
186
+ same = lambda s: re.sub(r"\| Updated \| [0-9-]+ \|", "", s) # everything but the date must match
187
+ if old is not None and same(old) == same(text) and os.path.isfile(out[:-3] + ".html"):
188
+ if not quiet:
189
+ print(f"tf-misses-md: {rel} unchanged ({n} misses)")
190
+ return 0
191
+ with open(out, "w", encoding="utf-8", newline="\n") as fh:
192
+ fh.write(text)
193
+ html = "skipped"
194
+ if "--no-html" not in argv:
195
+ r = os.path.join(HERE, "tf-render-html.py")
196
+ if os.path.isfile(r):
197
+ try:
198
+ p = subprocess.run([sys.executable, r, out, "--quiet"], capture_output=True, text=True, timeout=120)
199
+ html = "rendered" if p.returncode == 0 else "not rendered (" + (p.stderr or p.stdout).strip().splitlines()[-1:][0][:100] + ")" if (p.stderr or p.stdout).strip() else "not rendered"
200
+ except Exception as e:
201
+ html = f"not rendered ({e})"
202
+ if not quiet:
203
+ print(f"tf-misses-md: wrote {rel} ({n} misses); HTML {html}")
204
+ except Exception as e:
205
+ if not quiet:
206
+ print(f"tf-misses-md: nothing written ({e})")
207
+ return 0
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ # tf-misses-md.sh — rebuild the readable miss list docs/<App>-Misses.md and its HTML from the
3
+ # misses stream (Session 5, 2026-09-07; FR-31). tf-emit.sh runs it after every write to that
4
+ # stream; run it by hand once on a project whose stream is older than this script.
5
+ #
6
+ # bash .tfcore/utils/tf-misses-md.sh [--root <repo>] [--app <App>] [--no-html] [--quiet]
7
+ #
8
+ # Exit 0 always. Details: tf-misses-md.py.
9
+ set -u
10
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ command -v python3 >/dev/null 2>&1 || { echo "tf-misses-md: python3 is required" >&2; exit 0; }
12
+ python3 "$HERE/tf-misses-md.py" "$@"
13
+ exit 0
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """tf-mockups-locate.py — bring every mockup into docs/mockups/ (see tf-mockups-locate.sh).
3
+
4
+ bash .tfcore/utils/tf-mockups-locate.sh [--dry-run]
5
+
6
+ The framework keeps one mockup folder, docs/mockups/. A brownfield repository may hold
7
+ mockups elsewhere (a root mockups/ folder, a designs/ folder, an html file beside the
8
+ docs). Two folders confused a real project, so this script finds every folder whose
9
+ name says mockup (mockup, mockups, wireframes, designs, screens) outside docs/mockups/,
10
+ docs/OldDocs/, the framework folders, node_modules, bin and obj, and moves each .html,
11
+ .png, .jpg or .svg it holds into docs/mockups/. A name already taken there gets the
12
+ source folder name as a suffix. Prints one line per file moved, then the final list of
13
+ docs/mockups/ files. Exit 0 always; 2 if docs/ cannot be created.
14
+ """
15
+ import os
16
+ import shutil
17
+ import sys
18
+
19
+ NAMES = {"mockup", "mockups", "wireframe", "wireframes", "designs", "screens", "ui-mockups", "mock-ups"}
20
+ SKIP_DIRS = {".tfcore", ".claude", ".opencode", ".git", "node_modules", "bin", "obj",
21
+ "OldDocs", "tests", "wwwroot"}
22
+ EXT = (".html", ".htm", ".png", ".jpg", ".jpeg", ".svg")
23
+
24
+
25
+ def main(argv):
26
+ dry = "--dry-run" in argv
27
+ if "-h" in argv or "--help" in argv:
28
+ print(__doc__)
29
+ return 0
30
+ root = os.getcwd()
31
+ target = os.path.join(root, "docs", "mockups")
32
+ try:
33
+ os.makedirs(target, exist_ok=True)
34
+ except Exception as e:
35
+ print(f"tf-mockups-locate: cannot create docs/mockups ({e})", file=sys.stderr)
36
+ return 2
37
+ moved = 0
38
+ for dirpath, dirnames, filenames in os.walk(root):
39
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
40
+ rel = os.path.relpath(dirpath, root).replace(os.sep, "/")
41
+ if rel in (".", "docs/mockups") or rel.startswith("docs/mockups/"):
42
+ continue
43
+ if os.path.basename(dirpath).lower() not in NAMES:
44
+ continue
45
+ for fn in sorted(filenames):
46
+ if not fn.lower().endswith(EXT):
47
+ continue
48
+ src = os.path.join(dirpath, fn)
49
+ dest = os.path.join(target, fn)
50
+ if os.path.exists(dest):
51
+ stem, ext = os.path.splitext(fn)
52
+ dest = os.path.join(target, f"{stem}-{os.path.basename(dirpath)}{ext}")
53
+ print(f"tf-mockups-locate: {'would move' if dry else 'moved'} {rel}/{fn} -> docs/mockups/{os.path.basename(dest)}")
54
+ if not dry:
55
+ shutil.move(src, dest)
56
+ moved += 1
57
+ if not dry and not os.listdir(dirpath):
58
+ os.rmdir(dirpath)
59
+ print(f"tf-mockups-locate: removed the empty folder {rel}/")
60
+ files = sorted(f for f in os.listdir(target) if f.lower().endswith(EXT))
61
+ print(f"tf-mockups-locate: {moved} file(s) {'to move' if dry else 'moved'}; docs/mockups/ now holds {len(files)}: " + (", ".join(files) if files else "(none)"))
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__":
66
+ sys.exit(main(sys.argv))
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ # tf-mockups-locate.sh — bring every mockup in the repository into docs/mockups/.
3
+ #
4
+ # bash .tfcore/utils/tf-mockups-locate.sh # move, then list docs/mockups/
5
+ # bash .tfcore/utils/tf-mockups-locate.sh --dry-run # only report
6
+ #
7
+ # Finds folders named mockup(s), wireframe(s), designs or screens outside
8
+ # docs/mockups/ and moves their .html/.png/.jpg/.svg files there, so the
9
+ # framework's one mockup folder is the only one. Run at brownfield day-1 and by
10
+ # the mockups task before it links or draws anything.
11
+ # Thin wrapper over tf-mockups-locate.py; Python 3 standard library only.
12
+ set -uo pipefail
13
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ if ! command -v python3 >/dev/null 2>&1; then
15
+ echo "tf-mockups-locate: python3 is required (standard library only)." >&2
16
+ exit 2
17
+ fi
18
+ exec python3 "$SELF_DIR/tf-mockups-locate.py" "$@"
@@ -99,18 +99,34 @@ def main():
99
99
  diags.append("DIAG|dead-ref|%s" % ref)
100
100
 
101
101
  # --- project-only content ---------------------------------------------
102
+ # A key counts as project content only if it could actually work. Two kinds
103
+ # cannot, and calling them project content is what kept a repo's root config
104
+ # frozen with every framework agent dead (MISS-TechieFlow-20260907-08):
105
+ # - the framework's own retired names: `techieflow:` and the `bmad`
106
+ # prefixes it carried before the rename;
107
+ # - any key whose every {file:} reference is missing from the app, so the
108
+ # block cannot load whatever it names. That rule needs no list of old
109
+ # prefixes and so survives the next rename.
110
+ dead_refs = set(d.split("|", 2)[2] for d in diags if d.startswith("DIAG|dead-ref|"))
111
+
112
+ def is_litter(key, block):
113
+ low = key.lower()
114
+ if low.startswith("techieflow:") or low.startswith("bmad:") or low.startswith("bmad-"):
115
+ return True
116
+ refs = re.findall(r"\{file:([^}]*)\}", json.dumps(block))
117
+ return bool(refs) and all(r in dead_refs for r in refs)
118
+
102
119
  project = []
103
120
  for k in app:
104
121
  if k not in tmpl:
105
122
  project.append(k)
106
- for k in (app.get("agent") or {}):
107
- if k not in (tmpl.get("agent") or {}):
108
- project.append("agent.%s" % k)
109
- for k in (app.get("command") or {}):
110
- if k in (tmpl.get("command") or {}):
123
+ for k, blk in (app.get("agent") or {}).items():
124
+ if k in (tmpl.get("agent") or {}) or is_litter(k, blk):
125
+ continue
126
+ project.append("agent.%s" % k)
127
+ for k, blk in (app.get("command") or {}).items():
128
+ if k in (tmpl.get("command") or {}) or is_litter(k, blk):
111
129
  continue
112
- if k.startswith("techieflow:"):
113
- continue # retired FRAMEWORK command — litter, not project content
114
130
  project.append("command.%s" % k)
115
131
 
116
132
  if app_raw == tmpl_raw: