@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
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # tf-doc-check.sh — check TechieFlow human documents against their template schemas.
3
+ #
4
+ # bash .tfcore/utils/tf-doc-check.sh docs/MyApp-BRD.md [more.md ...]
5
+ # bash .tfcore/utils/tf-doc-check.sh --app MyApp # every document of the app + PROJECT-STATUS.md
6
+ # bash .tfcore/utils/tf-doc-check.sh --app MyApp --warn # report mode: findings as WARN, exit 0
7
+ # bash .tfcore/utils/tf-doc-check.sh --size Medium docs/MyApp-BRD.md
8
+ #
9
+ # Each template under .tfcore/templates/v4custom/ opens with a `<!-- tf-schema -->`
10
+ # block (required sections in order, budgets per size, row rules). This script
11
+ # prints one line per problem — FAIL blocks the phase, WARN does not — and exits
12
+ # 0 when nothing FAILs, 1 when something does, 2 when it could not run.
13
+ #
14
+ # Run by the status gate (_status-update-gate.md) on every human document a
15
+ # command wrote, before the HTML render. Readable version of the rules:
16
+ # docs/TechieFlow-Document-Schemas.md.
17
+ #
18
+ # Thin wrapper over tf-doc-check.py, matching the tf-render-html.sh idiom.
19
+ # Python 3 standard library only.
20
+ set -uo pipefail
21
+
22
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+
24
+ if ! command -v python3 >/dev/null 2>&1; then
25
+ echo "tf-doc-check: python3 is required (standard library only)." >&2
26
+ exit 2
27
+ fi
28
+
29
+ exec python3 "$SELF_DIR/tf-doc-check.py" "$@"
@@ -19,6 +19,8 @@
19
19
  # tf-emit.sh --amend MISS-App-20260828-01 why_missed missing-checklist-item
20
20
  # # fills a field that is still null on a miss already on
21
21
  # # the stream (SCHEMA.md §5.5.7). Never overwrites.
22
+ # tf-emit.sh --origin-of REQ-UI-014 # prints "<started> <cmd> <agent>" of the last build or fix
23
+ # # run that touched the row, nothing when there is none
22
24
  # tf-emit.sh --where # prints the resolved docs/metrics dir
23
25
  #
24
26
  # STREAMS: runs | gates | sessions | commits | misses (anything else is dropped)
@@ -131,6 +133,39 @@ PY
131
133
  exit 0
132
134
  fi
133
135
 
136
+ # --- read helper: which run caused a row's defect ------------------------
137
+ # Prints "<started> <cmd> <agent>" for the most recent non-backfilled build-phase
138
+ # or fix-issues run whose reqs_touched holds the REQ, nothing when there is none
139
+ # (the caller then leaves origin_run_id out and the record is marked inferred).
140
+ # Used by tf-log-miss.sh and tf-triage.sh (Sitting 4c, 2026-09-06).
141
+ if [[ "$1" == "--origin-of" ]]; then
142
+ REQ="$2"
143
+ [[ -n "$REQ" ]] || exit 0
144
+ python3 - "$MET_DIR/runs.jsonl" "$REQ" 2>"$TF_ERR" <<'PY2' || true
145
+ import json, sys
146
+ path, req = sys.argv[1], sys.argv[2].upper()
147
+ best = None
148
+ try:
149
+ with open(path, "r", encoding="utf-8") as fh:
150
+ for line in fh:
151
+ try:
152
+ r = json.loads(line)
153
+ except Exception:
154
+ continue
155
+ if r.get("kind") != "run" or r.get("backfilled") or r.get("cmd") not in ("build-phase", "fix-issues"):
156
+ continue
157
+ if req in [str(x).upper() for x in (r.get("reqs_touched") or [])]:
158
+ if best is None or r.get("started", "") >= best.get("started", ""):
159
+ best = r
160
+ except FileNotFoundError:
161
+ pass
162
+ if best:
163
+ subs = best.get("subagents") or []
164
+ print(best.get("started", ""), best.get("cmd"), subs[0] if subs else "flow-master")
165
+ PY2
166
+ exit 0
167
+ fi
168
+
134
169
  # --- read helper: next RUN attempt number for a cmd + REQ set ------------
135
170
  # run attempt = 1 + count of prior NON-BACKFILLED `run` records with the same
136
171
  # cmd whose reqs_touched intersects the given REQ IDs. Same counting rule that
@@ -170,9 +205,10 @@ fi
170
205
  # `miss-fix`, and a collision would silently merge two defects into one lifecycle.
171
206
  if [[ "$1" == "--next-miss-id" ]]; then
172
207
  python3 - "$MET_DIR/misses.jsonl" "$ROOT" 2>"$TF_ERR" <<'PY' || true
173
- import datetime, glob, json, os, sys
208
+ import datetime, glob, json, os, re, sys
174
209
  path, root = sys.argv[1], sys.argv[2]
175
- hits = glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
210
+ hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
211
+ if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)] # only the phase-1 work list names the app
176
212
  app = (os.path.basename(hits[0])[: -len("-Checklist.md")] if len(hits) == 1
177
213
  else os.path.basename(os.path.abspath(root)))
178
214
  day = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
@@ -304,6 +340,9 @@ AMENDABLE = {
304
340
  "why_missed": ("missing-checklist-item", "insufficient-verify-method",
305
341
  "code-audit-limitation", "ambiguous-acceptance",
306
342
  "dependency-not-declared", "instruction-ignored", "other"),
343
+ # whose gap it was, the four-question sort (FR-32, Session 5): a judgement a reader can
344
+ # still make later, so a miss logged before the field existed can be sorted now.
345
+ "sort": ("spec", "unsaid", "weak-check", "ignored"),
307
346
  }
308
347
  if fld not in AMENDABLE:
309
348
  print("REFUSED %s is not an amendable field (SCHEMA.md §5.5.7)" % fld); raise SystemExit(0)
@@ -360,7 +399,7 @@ esac
360
399
  # The program is captured into a variable rather than fed on stdin — a heredoc
361
400
  # on `python3 -` would REPLACE the caller's piped JSON and silently drop every event.
362
401
  TF_PROG="$(cat <<'PY'
363
- import json, os, sys, datetime, glob, re
402
+ import json, os, sys, datetime, glob, re, time
364
403
 
365
404
  met_dir, stream, root = sys.argv[1], sys.argv[2], sys.argv[3]
366
405
  dbg = os.environ.get("TF_METRICS_DEBUG")
@@ -456,11 +495,8 @@ def _detect_harness():
456
495
  # the OTHER harness inherits that harness's markers (e.g. OpenCode started
457
496
  # from a Claude Code bash still carries CLAUDE_PROJECT_DIR).
458
497
  tf = os.environ.get("TF_HARNESS")
459
- if tf in ("claude-code", "opencode", "codex"):
498
+ if tf in ("claude-code", "opencode"):
460
499
  return tf
461
- for k in ("CODEX_THREAD_ID", "CODEX_SESSION_ID"):
462
- if os.environ.get(k):
463
- return "codex"
464
500
  for k in ("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID",
465
501
  "CLAUDE_PROJECT_DIR"):
466
502
  if os.environ.get(k):
@@ -488,8 +524,6 @@ def _detect_harness():
488
524
  name, ppid = os.path.basename(parts[0]), int(parts[1])
489
525
  if name and "opencode" in name.lower():
490
526
  return "opencode"
491
- if name and "codex" in name.lower():
492
- return "codex"
493
527
  if name and "claude" in name.lower():
494
528
  return "claude-code"
495
529
  pid = ppid
@@ -505,7 +539,11 @@ except Exception:
505
539
  # app name — inferred from the one checklist, else the repo directory name
506
540
  def _app_name():
507
541
  try:
508
- hits = glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
542
+ # The Deployment Checklist and a Large project's phase-n checklists share the suffix;
543
+ # only <App>-Checklist.md names the app, else a copy such as TfLens-oc was recorded
544
+ # under its folder name (MISS-TechieFlow-20260906-04).
545
+ hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
546
+ if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)]
509
547
  if len(hits) == 1:
510
548
  return os.path.basename(hits[0])[: -len("-Checklist.md")]
511
549
  except Exception:
@@ -524,6 +562,54 @@ def enrich(rec):
524
562
  DECISIONS.md 2026-08-20)."""
525
563
  rec.setdefault("v", 1)
526
564
  rec.setdefault("ts", NOW)
565
+ # A run record says whether YOLO was on (D-12, FR-35; Sitting 4c, 2026-09-06): the task
566
+ # never has to remember it. TF_YOLO=1 (the supervisor) or a live .tfcore/.session/yolo.json.
567
+ if rec.get("kind") == "run" and "yolo" not in rec:
568
+ on = os.environ.get("TF_YOLO") == "1"
569
+ if not on:
570
+ try:
571
+ fl = os.path.join(root, ".tfcore", ".session", "yolo.json")
572
+ on = os.path.isfile(fl) and (time.time() - os.path.getmtime(fl)) < 24 * 3600
573
+ except Exception:
574
+ on = False
575
+ rec["yolo"] = bool(on)
576
+ # A run record without `started` takes it from the command marker written by
577
+ # tf-phase.sh start (step 0 of every task), when the marker names the same command.
578
+ if rec.get("kind") == "run" and not rec.get("started"):
579
+ try:
580
+ with open(os.path.join(root, ".tfcore", ".session", "phase.json"), encoding="utf-8") as fh:
581
+ mk = json.load(fh) or {}
582
+ if mk.get("started") and (not rec.get("cmd") or mk.get("cmd") == rec.get("cmd")):
583
+ rec["started"] = mk["started"]
584
+ rec.setdefault("ended", NOW)
585
+ sys.stdout.write("tf-emit: started taken from the command marker (%s)\n" % mk["started"])
586
+ except Exception:
587
+ pass
588
+ # A run's `ended` is when the record is written, never a guess: an ended in the
589
+ # future (more than a minute past now) or before started is replaced with now and
590
+ # duration_s recomputed (MISS-TechieFlow-20260905-11: a day-1 run wrote 17:05 at 16:40).
591
+ # The same rule the other way round: a run record with NO `ended` was silently
592
+ # accepted and could never be costed — the effort-per-phase figure just lost it
593
+ # (MISS-TechieFlow-20260907-09, the session-6 record itself). `ended` is when the
594
+ # record is written, so an absent one is filled in, never left empty.
595
+ if rec.get("kind") == "run" and not isinstance(rec.get("ended"), str):
596
+ rec["ended"] = NOW
597
+ if rec.get("kind") == "run" and isinstance(rec.get("ended"), str):
598
+ t_end, t_now = _iso_ms(rec["ended"]), _iso_ms(NOW)
599
+ t_start = _iso_ms(rec["started"]) if isinstance(rec.get("started"), str) else None
600
+ if t_end is not None and t_now is not None and (t_end > t_now + 60_000 or (t_start is not None and t_end < t_start)):
601
+ sys.stdout.write("tf-emit: ended %s is not a measurement (it lies %s); set to now, %s\n"
602
+ % (rec["ended"], "in the future" if t_end > t_now else "before started", NOW))
603
+ rec["ended"] = NOW
604
+ if t_start is not None and "duration_s" in rec:
605
+ rec["duration_s"] = max(0, (t_now - t_start) // 1000)
606
+ # A run record that omits duration_s gets it from started and ended (the devguide and
607
+ # refresh-status records of 2026-09-06 had none; MISS-TechieFlow-20260906-10).
608
+ if rec.get("kind") == "run" and "duration_s" not in rec and rec.get("started") and rec.get("ended"):
609
+ try:
610
+ rec["duration_s"] = max(0, (_iso_ms(rec["ended"]) - _iso_ms(rec["started"])) // 1000)
611
+ except Exception:
612
+ pass
527
613
  if "project_type" not in rec:
528
614
  if PTYPE:
529
615
  rec["project_type"] = PTYPE
@@ -531,7 +617,10 @@ def enrich(rec):
531
617
  # Default, but NEVER silently — the report labels these unclassified.
532
618
  rec["project_type"] = "app"
533
619
  rec["project_type_inferred"] = True
534
- rec.setdefault("harness", HARNESS)
620
+ if HARNESS:
621
+ rec["harness"] = HARNESS # detected, never declared (SCHEMA.md §1)
622
+ else:
623
+ rec.setdefault("harness", None)
535
624
  rec.setdefault("app", APP)
536
625
  return rec
537
626
 
@@ -771,7 +860,7 @@ def enrich_run(rec):
771
860
  tier = ROUTING["phases"].get(cmd)
772
861
  if tier and tier != "inherit":
773
862
  rec["tier"] = tier
774
- key = {"claude-code": "claude", "opencode": "opencode", "codex": "codex"}.get(HARNESS)
863
+ key = {"claude-code": "claude", "opencode": "opencode"}.get(HARNESS)
775
864
  tm = ROUTING["tiers"].get(tier, {}).get(key) if key else None
776
865
  if tm:
777
866
  rec["tier_model"] = tm
@@ -891,6 +980,7 @@ _AMENDABLE = {
891
980
  "why_missed": ("missing-checklist-item", "insufficient-verify-method",
892
981
  "code-audit-limitation", "ambiguous-acceptance",
893
982
  "dependency-not-declared", "instruction-ignored", "other"),
983
+ "sort": ("spec", "unsaid", "weak-check", "ignored"),
894
984
  }
895
985
 
896
986
  def _amend_ok(rec):
@@ -952,6 +1042,14 @@ _MISS_ENUMS = {
952
1042
  "why_missed": ("missing-checklist-item", "insufficient-verify-method",
953
1043
  "code-audit-limitation", "ambiguous-acceptance",
954
1044
  "dependency-not-declared", "instruction-ignored", "other"),
1045
+ # whose gap (FR-32, Session 5 2026-09-07): spec = the app's spec did not say it (fix the
1046
+ # checklist line) · unsaid = the framework never said it (a requirement line plus a check) ·
1047
+ # weak-check = a check existed and did not catch it (fix the check) · ignored = it was written
1048
+ # and not followed (a hook, or delete the rule)
1049
+ "sort": ("spec", "unsaid", "weak-check", "ignored"),
1050
+ }
1051
+ _REVIEW_ENUMS = {
1052
+ "phase": ("day1-review", "build-review", "verify-review", "handoff-review"),
955
1053
  }
956
1054
  _FIX_ENUMS = {
957
1055
  "verdict_after": ("Verified", "Needs re-verify", "FAIL", "deferred", "wont-fix"),
@@ -975,6 +1073,72 @@ def _enums_ok(rec, table):
975
1073
  return True
976
1074
 
977
1075
 
1076
+ # --- shape check (added 2026-09-05, reset Sitting 4a) ------------------------
1077
+ # A record may carry only the fields SCHEMA.md defines for its kind (plus the
1078
+ # ones this script injects). A `miss` must carry a miss_id from --next-miss-id.
1079
+ # Anything else is REFUSED on stdout and never appended, the same choice as
1080
+ # _enums_ok(): a field nobody defined is a field no report can read.
1081
+ _COMMON = {"v", "ts", "kind", "app", "project_type", "project_type_inferred", "harness", "backfilled"}
1082
+ _ALLOWED = {
1083
+ ("runs", "run"): {"cmd", "mode", "yolo", "started", "ended", "duration_s", "reqs_touched", "reqs_count",
1084
+ "subagents", "files_written", "build_result", "tier", "tier_model", "model", "models",
1085
+ "routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
1086
+ "cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
1087
+ "model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts"},
1088
+ ("gates", "gate"): {"run_id", "req_id", "req_class", "attempt", "verdict", "gate", "gates_run",
1089
+ "failure_class", "prior_verdict", "proof", "escaped", "phase", "result", "detail", "at",
1090
+ "tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
1091
+ "tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
1092
+ "subagent_runs", "tokens_out_subagents", "session_id"},
1093
+ ("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
1094
+ "cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
1095
+ ("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
1096
+ ("misses", "miss"): {"miss_id", "req_id", "req_class", "miss_class", "artifact", "severity", "origin_phase",
1097
+ "origin_agent", "origin_run_id", "origin_model", "origin_harness", "origin_confidence",
1098
+ "why_missed", "found_by", "found_phase", "found_gate", "found_run_id", "failure_class",
1099
+ "what", "sort"},
1100
+ ("misses", "miss-fix"): {"miss_id", "req_id", "fix_run_id", "fix_cmd", "fix_attempt", "verdict_after",
1101
+ "reopened", "cost_attribution", "tokens_in", "tokens_out", "tokens_cache_read",
1102
+ "tokens_cache_write", "cost_usd", "tokens_scope", "model"},
1103
+ ("misses", "miss-amend"): {"miss_id", "field", "value"},
1104
+ # an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
1105
+ # given, what producing the reviewed output cost, what applying the corrections cost. The two
1106
+ # costs are copied from the runs named by reviewed_run_id and correction_run_id (their `started`).
1107
+ ("misses", "review"): {"phase", "reviewed_run_id", "correction_run_id", "corrections", "what",
1108
+ "tokens_produce", "tokens_correct", "cost_produce_usd", "cost_correct_usd",
1109
+ "model_produce", "model_correct"},
1110
+ }
1111
+ # Fields the emitter derives for runs/gates: a caller's value is discarded.
1112
+ _DERIVED = ("harness", "tier", "tier_model", "model", "models", "model_tokens_out", "routed",
1113
+ "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write", "cost_usd",
1114
+ "tokens_scope", "subagent_runs", "tokens_out_subagents")
1115
+
1116
+
1117
+ def _shape_ok(rec):
1118
+ kind = rec.get("kind")
1119
+ if not isinstance(rec, dict) or not kind:
1120
+ sys.stdout.write("tf-emit: REFUSED — record has no \"kind\" (SCHEMA.md §1). Nothing was appended.\n")
1121
+ return False
1122
+ allowed = _ALLOWED.get((stream, kind))
1123
+ if allowed is None:
1124
+ sys.stdout.write("tf-emit: REFUSED — kind %r is not a record kind of the %s stream. Nothing was appended.\n" % (kind, stream))
1125
+ return False
1126
+ unknown = sorted(set(rec) - allowed - _COMMON)
1127
+ if unknown:
1128
+ sys.stdout.write("tf-emit: REFUSED — field(s) not in SCHEMA.md for a %s record: %s. Add the field to the schema first, or drop it. Nothing was appended.\n" % (kind, ", ".join(unknown)))
1129
+ return False
1130
+ if kind == "run" and rec.get("build_result") not in (None, "pass", "fail", "not-run"):
1131
+ sys.stdout.write("tf-emit: REFUSED — build_result must be pass, fail or not-run (SCHEMA.md §2); got %r. Put the detail in the checklist Remarks or Known blockers, not here. Nothing was appended.\n" % (rec.get("build_result"),))
1132
+ return False
1133
+ if kind == "miss" and not rec.get("miss_id"):
1134
+ sys.stdout.write("tf-emit: REFUSED — a miss record needs a miss_id; get one with: bash .tfcore/utils/tf-emit.sh --next-miss-id. Nothing was appended.\n")
1135
+ return False
1136
+ if stream in ("runs", "gates") and not rec.get("backfilled"):
1137
+ for f in _DERIVED:
1138
+ rec.pop(f, None)
1139
+ return True
1140
+
1141
+
978
1142
  def enrich_miss(rec):
979
1143
  if stream != "misses":
980
1144
  return rec
@@ -984,6 +1148,21 @@ def enrich_miss(rec):
984
1148
  return None
985
1149
  if rec.get("kind") == "miss-fix" and not _enums_ok(rec, _FIX_ENUMS):
986
1150
  return None
1151
+ if rec.get("kind") == "review":
1152
+ if not _enums_ok(rec, _REVIEW_ENUMS):
1153
+ return None
1154
+ if not isinstance(rec.get("corrections"), int) or rec["corrections"] < 0:
1155
+ sys.stdout.write("tf-emit: REFUSED — a review record needs \"corrections\", a whole number of corrections the owner gave. Nothing was appended.\n")
1156
+ return None
1157
+ for f in ("tokens_produce", "tokens_correct", "cost_produce_usd", "cost_correct_usd", "model_produce", "model_correct"):
1158
+ rec.pop(f, None) # derived, never a caller's figure
1159
+ runs = _runs_by_start()
1160
+ for side, key in (("produce", "reviewed_run_id"), ("correct", "correction_run_id")):
1161
+ src = runs.get(rec.get(key)) if rec.get(key) else None
1162
+ rec["tokens_" + side] = src.get("tokens_out") if src else None
1163
+ rec["cost_" + side + "_usd"] = src.get("cost_usd") if src else None
1164
+ rec["model_" + side] = src.get("model") if src else None
1165
+ return rec
987
1166
  try:
988
1167
  kind = rec.get("kind")
989
1168
  runs = _runs_by_start()
@@ -1003,9 +1182,11 @@ def enrich_miss(rec):
1003
1182
  rec["origin_harness"] = None
1004
1183
  elif kind == "miss-fix":
1005
1184
  src = runs.get(rec.get("fix_run_id")) if rec.get("fix_run_id") else None
1185
+ for f in _COST_FIELDS:
1186
+ rec.pop(f, None) # a caller's cost figure is never kept
1006
1187
  if src:
1007
1188
  for f in _COST_FIELDS:
1008
- if f in src and f not in rec:
1189
+ if f in src:
1009
1190
  rec[f] = src[f]
1010
1191
  if "cost_attribution" not in rec:
1011
1192
  touched = src.get("reqs_touched") or []
@@ -1048,6 +1229,8 @@ def enrich_miss(rec):
1048
1229
  # .gitattributes block pins to LF. These files are LF on every platform.
1049
1230
  lines = []
1050
1231
  for rec in records:
1232
+ if not _shape_ok(rec):
1233
+ continue
1051
1234
  out = enrich_miss(enrich_run(stamp_attempt(enrich(rec))))
1052
1235
  if out is None: # a refused miss-amend (§5.5.7) — dropped, never appended
1053
1236
  continue
@@ -1072,4 +1255,11 @@ PY
1072
1255
  )"
1073
1256
  python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
1074
1257
 
1258
+ # The readable miss list beside the record (FR-31, Session 5 2026-09-07): after any write to the
1259
+ # misses stream, docs/<App>-Misses.md and its HTML are rebuilt from the stream, so the file is
1260
+ # never older than the record. Best effort, like everything else here.
1261
+ if [[ "$STREAM" == "misses" ]]; then
1262
+ python3 "$(dirname "${BASH_SOURCE[0]}")/tf-misses-md.py" --root "$ROOT" --quiet 2>"$TF_ERR" || true
1263
+ fi
1264
+
1075
1265
  exit 0
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # tf-fix-close.sh — the telemetry a fix owes when it lands (Sitting 4c, 2026-09-06; D-15).
3
+ #
4
+ # bash .tfcore/utils/tf-fix-close.sh <App> [--started <ISO>] [--reqs REQ-UI-009,REQ-FN-014]
5
+ # [--subagents trblazeui,builder] [--build pass|fail|not-run] [--files N]
6
+ #
7
+ # Emits, in this order, through tf-emit.sh only:
8
+ # 1. the fix-issues run record (cmd fix-issues, mode fix, the rows touched) — first, because the
9
+ # miss-fix records below carry no numbers of their own: the emitter copies this run's token window
10
+ # 2. one miss-fix per row that has an open miss, with verdict_after taken from the verify ledger
11
+ # docs/.last-verify.json written by the verify the fix chained, mapped to a checklist status (PASS →
12
+ # Verified; FAIL → FAIL; any other failing verdict or an absent row → Needs re-verify). A row with no open miss is listed,
13
+ # not invented: a fix on a defect nobody logged is a triage gap, so open it first with tf-triage.sh
14
+ # demote or tf-log-miss.sh, then re-run this.
15
+ # --reqs defaults to every row in the ledger. Telemetry never blocks: refusals are printed, exit 0.
16
+ set -u
17
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ [[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
19
+ APP="$1"; shift
20
+ STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""
21
+ while [[ $# -gt 0 ]]; do
22
+ case "$1" in
23
+ --started) STARTED="${2:-}"; shift 2 ;;
24
+ --reqs) REQS="${2:-}"; shift 2 ;;
25
+ --subagents) SUBS="${2:-}"; shift 2 ;;
26
+ --build) BUILD="${2:-}"; shift 2 ;;
27
+ --files) FILES="${2:-}"; shift 2 ;;
28
+ *) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
29
+ esac
30
+ done
31
+ [[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | sed -n 's/.*"started":"\([^"]*\)".*/\1/p')"
32
+ TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
33
+ import datetime, json, os, subprocess
34
+ EMIT = os.environ["TF_EMIT"]; app = os.environ["TF_APP"]; started = os.environ["TF_STARTED"]
35
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
36
+ def q(*a): return subprocess.run(["bash", EMIT, *a], capture_output=True, text=True).stdout.strip()
37
+ def emit(stream, rec):
38
+ p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
39
+ msg = (p.stdout + p.stderr).strip(); ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
40
+ if not ok: print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
41
+ return ok
42
+ ledger = {}
43
+ try:
44
+ ledger = json.load(open(os.path.join("docs", ".last-verify.json"), encoding="utf-8"))
45
+ except Exception:
46
+ pass
47
+ rows = ledger.get("rows") or {}
48
+ reqs = [r.strip().upper() for r in os.environ["TF_REQS"].split(",") if r.strip()] or sorted(rows)
49
+ subs = [s.strip() for s in os.environ["TF_SUBS"].split(",") if s.strip()]
50
+ files = os.environ["TF_FILES"]
51
+ run = {"kind": "run", "app": app, "cmd": "fix-issues", "mode": "fix", "ended": now, "reqs_touched": reqs, "reqs_count": len(reqs),
52
+ "subagents": subs, "files_written": int(files) if files.isdigit() else len(reqs), "build_result": os.environ["TF_BUILD"]}
53
+ if started: run["started"] = started
54
+ # one run record per start: a close called twice must not count the fix twice
55
+ runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
56
+ already = False
57
+ if started and os.path.isfile(runs_path):
58
+ for line in open(runs_path, encoding="utf-8", errors="replace"):
59
+ if '"cmd":"fix-issues"' in line and f'"started":"{started}"' in line:
60
+ already = True
61
+ break
62
+ ok = True if already else emit("runs", run)
63
+ if already:
64
+ print(f"fix-issues: run record for start {started} already exists; not written again")
65
+ closed, none = 0, []
66
+ for rid in reqs:
67
+ opened = q("--open-miss", rid)
68
+ if not opened:
69
+ none.append(rid); continue
70
+ mid = opened.split()[0]
71
+ fa = q("--next-fix-attempt", mid) or "1"
72
+ v = rows.get(rid)
73
+ # verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
74
+ # (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
75
+ after = ("Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
76
+ rec = {"kind": "miss-fix", "miss_id": mid, "req_id": rid, "fix_cmd": "fix-issues", "fix_attempt": int(fa) if fa.isdigit() else 1,
77
+ "verdict_after": after, "reopened": False}
78
+ if started: rec["fix_run_id"] = started
79
+ if emit("misses", rec): closed += 1
80
+ print(f"fix-issues: run record {'already there' if already else 'written' if ok else 'not written'}; {closed} miss-fix record(s) with the verifier's verdict"
81
+ + (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else ""))
82
+ PY
83
+ exit 0