@techierathore/techieflow 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -16
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
  7. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  8. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  9. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
  11. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  12. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  13. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  14. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  17. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  18. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  19. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  20. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  21. package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
  22. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
  23. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  24. package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
  25. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  26. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  27. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  28. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  29. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  30. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  31. package/.opencode/plugin/techieflow.js +40 -13
  32. package/.tfcore/agents/analyst.md +5 -13
  33. package/.tfcore/agents/architect.md +1 -16
  34. package/.tfcore/agents/flow-master.md +42 -215
  35. package/.tfcore/agents/verifier.md +24 -64
  36. package/.tfcore/core-config.yaml +10 -0
  37. package/.tfcore/hooks/block-git.sh +4 -4
  38. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  39. package/.tfcore/hooks/guard-build.sh +76 -0
  40. package/.tfcore/hooks/guard-db.sh +90 -0
  41. package/.tfcore/hooks/guard-metrics.sh +68 -0
  42. package/.tfcore/hooks/guard-status-html.sh +116 -71
  43. package/.tfcore/hooks/guard-status.sh +43 -16
  44. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  45. package/.tfcore/hooks/guard-verify.sh +70 -48
  46. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  47. package/.tfcore/install-manifest.yaml +0 -18
  48. package/.tfcore/routing.yaml +86 -14
  49. package/.tfcore/standards/coding-standards-core.md +61 -0
  50. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  51. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  52. package/.tfcore/tasks/_owner-language.md +26 -0
  53. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  54. package/.tfcore/tasks/_status-update-gate.md +10 -74
  55. package/.tfcore/tasks/_yolo-mode.md +20 -58
  56. package/.tfcore/tasks/amend-docs.md +21 -143
  57. package/.tfcore/tasks/build-phase.md +14 -224
  58. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  59. package/.tfcore/tasks/create-doc.md +4 -0
  60. package/.tfcore/tasks/create-project-brief.md +22 -0
  61. package/.tfcore/tasks/day1-brownfield.md +31 -505
  62. package/.tfcore/tasks/day1-greenfield.md +28 -213
  63. package/.tfcore/tasks/deploy-checklist.md +19 -0
  64. package/.tfcore/tasks/devguide.md +10 -219
  65. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  66. package/.tfcore/tasks/fix-issues.md +10 -129
  67. package/.tfcore/tasks/generate-html.md +5 -1
  68. package/.tfcore/tasks/handoff-phase.md +18 -16
  69. package/.tfcore/tasks/log-miss.md +19 -158
  70. package/.tfcore/tasks/metrics-report.md +3 -1
  71. package/.tfcore/tasks/mockups.md +12 -117
  72. package/.tfcore/tasks/refresh-status.md +9 -130
  73. package/.tfcore/tasks/split-brd.md +10 -126
  74. package/.tfcore/tasks/triage-and-fix.md +14 -0
  75. package/.tfcore/tasks/triage-issues.md +11 -152
  76. package/.tfcore/tasks/verify-phase.md +36 -572
  77. package/.tfcore/telemetry/SCHEMA.md +100 -8
  78. package/.tfcore/telemetry/tf-metrics.sh +564 -32
  79. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  80. package/.tfcore/templates/stack-questions.md +77 -0
  81. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  82. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  83. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  84. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  85. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  86. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  87. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  88. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  89. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  90. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  91. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  92. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  93. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  94. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  95. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  96. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  97. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  98. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  99. package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
  100. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  101. package/.tfcore/user-guide.md +0 -23
  102. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  107. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  108. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  109. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  110. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  111. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  112. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  113. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  114. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  115. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  116. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  117. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  118. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  119. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  120. package/.tfcore/utils/tf-brd-status.py +176 -0
  121. package/.tfcore/utils/tf-brd-status.sh +19 -0
  122. package/.tfcore/utils/tf-build-list.py +211 -0
  123. package/.tfcore/utils/tf-build-list.sh +21 -0
  124. package/.tfcore/utils/tf-build.sh +143 -0
  125. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  126. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  127. package/.tfcore/utils/tf-day1-files.py +208 -0
  128. package/.tfcore/utils/tf-day1-files.sh +20 -0
  129. package/.tfcore/utils/tf-devguide-list.py +249 -0
  130. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  131. package/.tfcore/utils/tf-doc-check.py +1369 -0
  132. package/.tfcore/utils/tf-doc-check.sh +29 -0
  133. package/.tfcore/utils/tf-emit.sh +545 -18
  134. package/.tfcore/utils/tf-fix-close.sh +115 -0
  135. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  136. package/.tfcore/utils/tf-goal.sh +284 -66
  137. package/.tfcore/utils/tf-harness.sh +4 -9
  138. package/.tfcore/utils/tf-log-miss.py +197 -0
  139. package/.tfcore/utils/tf-log-miss.sh +14 -0
  140. package/.tfcore/utils/tf-misses-md.py +208 -0
  141. package/.tfcore/utils/tf-misses-md.sh +14 -0
  142. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  143. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  144. package/.tfcore/utils/tf-model-pick.sh +475 -0
  145. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  146. package/.tfcore/utils/tf-perf-grade.py +97 -0
  147. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  148. package/.tfcore/utils/tf-phase.sh +92 -0
  149. package/.tfcore/utils/tf-render-html.py +20 -0
  150. package/.tfcore/utils/tf-routing-bind.sh +97 -6
  151. package/.tfcore/utils/tf-routing.sh +13 -7
  152. package/.tfcore/utils/tf-selfcheck.sh +323 -0
  153. package/.tfcore/utils/tf-split-brd.py +309 -0
  154. package/.tfcore/utils/tf-split-brd.sh +22 -0
  155. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  156. package/.tfcore/utils/tf-status-facts.py +358 -0
  157. package/.tfcore/utils/tf-status-facts.sh +20 -0
  158. package/.tfcore/utils/tf-triage.py +243 -0
  159. package/.tfcore/utils/tf-triage.sh +16 -0
  160. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  161. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  162. package/.tfcore/utils/tf-verify-env.sh +88 -0
  163. package/.tfcore/utils/tf-verify-list.py +270 -0
  164. package/.tfcore/utils/tf-verify-list.sh +13 -0
  165. package/.tfcore/utils/tf-verify-screens.mjs +355 -0
  166. package/.tfcore/utils/tf-verify-screens.sh +31 -0
  167. package/.tfcore/utils/tf-verify-tests.sh +166 -0
  168. package/.tfcore/utils/tf-verify-verdict.py +292 -0
  169. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  170. package/.tfcore/utils/tf-yolo.sh +59 -0
  171. package/README.md +120 -1145
  172. package/docs/TechieFlow-Installation.md +1 -5
  173. package/opencode.jsonc +37 -33
  174. package/package.json +1 -5
  175. package/scripts/install.mjs +56 -46
  176. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  177. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  178. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  179. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  180. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  181. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  182. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  183. package/.codex/config.toml +0 -46
  184. package/.codex/hooks.json +0 -63
  185. package/.codex/rules/techieflow.rules +0 -8
  186. package/.tfcore/hooks/codex-adapter.py +0 -248
  187. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  188. package/.tfcore/tasks/author-brd.md +0 -79
  189. package/.tfcore/tasks/document-project.md +0 -343
  190. package/.tfcore/tasks/execute-checklist.md +0 -86
  191. package/.tfcore/tasks/index-docs.md +0 -173
  192. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  193. package/.tfcore/tasks/shard-doc.md +0 -185
  194. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  195. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  196. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  197. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  198. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  199. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  200. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  201. package/.tfcore/utils/techieflow-doc-template.md +0 -325
  202. package/.tfcore/utils/tf-codex-bind.py +0 -196
  203. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  204. package/WORKFLOW.html +0 -1986
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """tf-build-list.py — the mechanical start of a build pass (see tf-build-list.sh).
3
+
4
+ bash .tfcore/utils/tf-build-list.sh <App> [--phase N] [--prompts]
5
+
6
+ Reads the phase's checklist (appPhase in core-config.yaml, or --phase) and prints:
7
+ - the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist, and they
8
+ come FIRST in the list), FRESH (nothing failing), or NOTHING (every row terminal or
9
+ Blocked). The working list is every open row in both FIX and FRESH: the mode says
10
+ what to build first, never what to leave out.
11
+ - the working list, grouped into clusters by the checklist's page section, each
12
+ naming its builder from the row prefix: REQ-UI -> trblazeui, REQ-RAG -> techierag,
13
+ REQ-FN / REQ-NFR -> builder
14
+ - with --prompts, one ready sub-agent prompt per cluster, filled from
15
+ .tfcore/templates/v4custom/build-subagent-prompt.md with the rows, their acceptance
16
+ lines, their mockups and the two standing rules.
17
+ Writes nothing. Python 3 standard library only. Exit 0 printed, 2 could not run.
18
+ """
19
+ import os
20
+ import re
21
+ import sys
22
+
23
+ TERMINAL = {"verified", "done (pre-existing)", "n/a"}
24
+ FIX = {"fail", "partial", "in progress", "needs re-verify"}
25
+ BLOCKED = {"blocked"}
26
+ BUILDER = {"UI": "trblazeui", "RAG": "techierag", "FN": "builder", "NFR": "builder"}
27
+ TPL = os.path.join(".tfcore", "templates", "v4custom", "build-subagent-prompt.md")
28
+
29
+
30
+ def die(msg):
31
+ print(f"tf-build-list: {msg}", file=sys.stderr)
32
+ sys.exit(2)
33
+
34
+
35
+ def read(p):
36
+ with open(p, encoding="utf-8", errors="replace") as f:
37
+ return f.read()
38
+
39
+
40
+ def config_phase():
41
+ p = os.path.join(".tfcore", "core-config.yaml")
42
+ if os.path.isfile(p):
43
+ m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
44
+ if m:
45
+ return int(m.group(1))
46
+ return 1
47
+
48
+
49
+ def norm(s):
50
+ s = s.strip().strip("`* ").lower()
51
+ return re.sub(r"\s*\(.*$", "", s).strip()
52
+
53
+
54
+ def rows_of(text):
55
+ rows = []
56
+ for line in text.splitlines():
57
+ if re.match(r"^\s*\|\s*`?REQ-", line):
58
+ c = [x.strip() for x in line.strip().strip("|").split("|")]
59
+ if len(c) >= 3:
60
+ rows.append({"id": c[0].strip("`* "), "title": c[1].strip("`* "), "status": norm(c[2]),
61
+ "remarks": c[4] if len(c) > 4 else ""})
62
+ return rows
63
+
64
+
65
+ def entries_of(text):
66
+ """id -> (section heading, acceptance line, mockup, brd)"""
67
+ out, current = {}, "Other"
68
+ lines = text.splitlines()
69
+ for i, line in enumerate(lines):
70
+ m = re.match(r"^##\s+(.+)$", line)
71
+ if m:
72
+ current = re.sub(r"^(?:page|screen)\s*:\s*", "", m.group(1).strip(), flags=re.I)
73
+ continue
74
+ a = re.search(r"<a id=['\"]d-(req-[a-z]+-\d+)['\"]", line, re.I)
75
+ if a:
76
+ rid = a.group(1).upper()
77
+ acc = mock = brd = ""
78
+ for j in range(i, min(i + 12, len(lines))):
79
+ l = lines[j]
80
+ if re.search(r"\*Acceptance:?\*|^\s*[-*]\s*Acceptance:", l, re.I) and not acc:
81
+ acc = re.sub(r"^\s*[-*]\s*\*?Acceptance:?\*?:?\s*", "", l).strip()
82
+ mm = re.search(r"((?:docs/)?mockups/[\w./-]+\.html)", l)
83
+ if mm and not mock:
84
+ mock = mm.group(1)
85
+ bm = re.search(r"BRD-\d+", l)
86
+ if bm and not brd:
87
+ brd = bm.group(0)
88
+ if j > i and re.match(r"^\s*(?:[-*]\s*)?<a id=|^## ", l):
89
+ break # the next entry starts (it begins with "- <a id="): nothing below is this row's
90
+ out[rid] = (current, acc, mock, brd)
91
+ return out
92
+
93
+
94
+ def main(argv):
95
+ if len(argv) < 2 or argv[1] in ("-h", "--help"):
96
+ print(__doc__)
97
+ return 0
98
+ app = argv[1]
99
+ prompts = "--prompts" in argv
100
+ phase = config_phase()
101
+ if "--phase" in argv:
102
+ i = argv.index("--phase")
103
+ if i + 1 >= len(argv) or not argv[i + 1].isdigit():
104
+ die("--phase needs a number")
105
+ phase = int(argv[i + 1])
106
+ pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
107
+ cl = os.path.join("docs", f"{pfx}Checklist.md")
108
+ if not os.path.isfile(cl):
109
+ die(f"{cl} does not exist; day-1 stage 2 writes it")
110
+ text = read(cl)
111
+ rows = rows_of(text)
112
+ if not rows:
113
+ die(f"{cl} has no REQ rows")
114
+ ent = entries_of(text)
115
+
116
+ fix_rows = [r for r in rows if r["status"] in FIX]
117
+ open_rows = [r for r in rows if r["status"] not in TERMINAL | BLOCKED]
118
+ # Repair before new work, but never INSTEAD of it. A row can sit at `Needs re-verify`
119
+ # permanently -- TfLens REQ-FN-067 and -070 need a repository that emits events.ndjson
120
+ # and none exists, so every honest verify run re-confirms that gate rather than clearing
121
+ # it. FIX used to win outright, which pinned the mode forever: the twelve rows a later
122
+ # *amend-docs added were never once printed, and a pass that trusted the working list
123
+ # would have met build-phase's ending condition with twelve requirements at 0%.
124
+ # So the working list is every open row, and FIX only says which ones come first.
125
+ # tests/regression/run.sh tf_019.
126
+ if fix_rows:
127
+ rest = [r for r in open_rows if r not in fix_rows]
128
+ mode, work = "FIX", fix_rows + rest
129
+ elif open_rows:
130
+ mode, work = "FRESH", open_rows
131
+ else:
132
+ mode, work = "NOTHING", []
133
+ terminal = sum(1 for r in rows if r["status"] in TERMINAL)
134
+ blocked = [r for r in rows if r["status"] in BLOCKED]
135
+ print(f"# tf-build-list — {app} — {cl} — phase {phase}")
136
+ print(f"Mode: {mode} — {len(work)} row(s) to build; {terminal} terminal, {len(blocked)} Blocked, {len(rows)} total"
137
+ + (f" — {', '.join(r['id'] for r in work[:8])}{' …' if len(work) > 8 else ''}" if work else ""))
138
+ # Every row is in exactly one of the four counts, and the arithmetic is printed so a
139
+ # reader can check it rather than trust it. A row missing from all of them is the
140
+ # failure this line exists to make impossible.
141
+ if len(work) + terminal + len(blocked) != len(rows):
142
+ print(f"⚠ {len(rows) - len(work) - terminal - len(blocked)} row(s) are in no category — "
143
+ "the checklist carries a status this script does not know; fix the row or the script")
144
+ if fix_rows and len(work) > len(fix_rows):
145
+ print(f"Order: {len(fix_rows)} failing row(s) first ({', '.join(r['id'] for r in fix_rows)}), "
146
+ f"then {len(work) - len(fix_rows)} not yet started")
147
+ if blocked:
148
+ print("Blocked (library gaps, pass through unless the feedback entry is resolved): " + ", ".join(r["id"] for r in blocked))
149
+ if not work:
150
+ print("Nothing to build. Run the status gate; the next command is the verifier's or the owner's.")
151
+ return 0
152
+
153
+ clusters = {}
154
+ for r in work:
155
+ sec, acc, mock, brd = ent.get(r["id"].upper(), ("Other", "", "", ""))
156
+ cls = re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1)
157
+ key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, BUILDER[cls])
158
+ clusters.setdefault(key, []).append((r, acc, mock, brd))
159
+ print()
160
+ print("## Clusters (one sub-agent each; spawn them all in one turn)")
161
+ letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
162
+ named = []
163
+ for i, ((sec, builder), items) in enumerate(clusters.items()):
164
+ label = letters[i % 26] + ("" if i < 26 else str(i // 26))
165
+ named.append((label, sec, builder, items))
166
+ mocks = sorted({m for _r, _a, m, _b in items if m})
167
+ print(f"Cluster {label} [{builder}]: " + ", ".join(r["id"] for r, *_ in items)
168
+ + f" ({sec}" + (f" — mockup: {', '.join(mocks)}" if mocks else "") + ")")
169
+ print()
170
+ print("## Working list")
171
+ for label, sec, builder, items in named:
172
+ for r, acc, mock, brd in items:
173
+ todo = "" if acc else " ⚠ no acceptance line: fix the checklist first"
174
+ print(f"- {r['id']} [{label}/{builder}] {r['title']} — {r['status']}{todo}")
175
+ if acc:
176
+ print(f" Acceptance: {acc}")
177
+ if mock:
178
+ print(f" Mockup: {mock}")
179
+ if not prompts:
180
+ print()
181
+ print("Add --prompts for one ready sub-agent prompt per cluster.")
182
+ return 0
183
+ if not os.path.isfile(TPL):
184
+ die(f"template {TPL} does not exist at its literal path")
185
+ tpl = read(TPL)
186
+ tpl = re.sub(r"<!--.*?-->\s*", "", tpl, flags=re.S)
187
+ for label, sec, builder, items in named:
188
+ lines = []
189
+ for r, acc, mock, brd in items:
190
+ lines.append(f"- {r['id']} — {r['title']}" + (f" ({brd})" if brd else ""))
191
+ lines.append(f" Acceptance: {acc or 'MISSING — refuse the row and report it'}")
192
+ if mock:
193
+ lines.append(f" Mockup: {mock}")
194
+ mocks = sorted({m for _r, _a, m, _b in items if m})
195
+ body = (tpl.replace("{App}", app).replace("{Cluster}", label).replace("{Builder}", builder)
196
+ .replace("{Section}", sec).replace("{Rows}", "\n".join(lines))
197
+ .replace("{Mockups}", ", ".join(mocks) if mocks else "none (not a UI cluster)")
198
+ .replace("{Checklist}", cl))
199
+ print()
200
+ print(f"## Prompt for cluster {label} [{builder}]")
201
+ print(body.strip())
202
+ return 0
203
+
204
+
205
+ if __name__ == "__main__":
206
+ try:
207
+ sys.exit(main(sys.argv))
208
+ except SystemExit:
209
+ raise
210
+ except Exception as e:
211
+ die(str(e))
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # tf-build-list.sh — the mechanical start of a build pass.
3
+ #
4
+ # bash .tfcore/utils/tf-build-list.sh MyApp # mode (FIX / FRESH / NOTHING), clusters, working list
5
+ # bash .tfcore/utils/tf-build-list.sh MyApp --prompts # plus one ready sub-agent prompt per cluster
6
+ # bash .tfcore/utils/tf-build-list.sh MyApp --phase 2 # a later phase of a Large project
7
+ #
8
+ # Reads the phase's checklist (appPhase in core-config.yaml unless --phase). Rows FAIL /
9
+ # PARTIAL / In Progress / Needs re-verify make it FIX mode, which puts those rows FIRST and
10
+ # then lists every other open row; FRESH is the same list when nothing is failing; Blocked rows
11
+ # pass through. Clusters follow the checklist's page sections,
12
+ # each with its builder from the prefix (UI -> trblazeui, RAG -> techierag, FN/NFR -> builder).
13
+ # Prompts are filled from .tfcore/templates/v4custom/build-subagent-prompt.md.
14
+ # Writes nothing. Thin wrapper over tf-build-list.py; Python 3 stdlib only.
15
+ set -uo pipefail
16
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ if ! command -v python3 >/dev/null 2>&1; then
18
+ echo "tf-build-list: python3 is required (standard library only)." >&2
19
+ exit 2
20
+ fi
21
+ exec python3 "$SELF_DIR/tf-build-list.py" "$@"
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # tf-build.sh — build, test or run a .NET target on whatever host this is, trying the
3
+ # invocation rungs in order and stopping at the first that works (Sitting 4b, 2026-09-05;
4
+ # replaces the prose ladder build-invocation-ladder.md §0 to §C).
5
+ #
6
+ # bash .tfcore/utils/tf-build.sh [build] [<target>] [-- <extra dotnet args>]
7
+ # bash .tfcore/utils/tf-build.sh test [<target>] [-- <extra dotnet args>]
8
+ # bash .tfcore/utils/tf-build.sh run <project> [-- <extra dotnet args>]
9
+ # bash .tfcore/utils/tf-build.sh probe # print the platform and the rungs, run nothing
10
+ #
11
+ # <target> is a .sln, .slnx or .csproj relative to the current folder (default: the one
12
+ # solution or project found here). Output goes to tests/.artifacts/build/<UTC time>.log.
13
+ #
14
+ # Prints ONE verdict line the agent copies into the status log row:
15
+ # PASS built on wsl via cmd.exe /c dotnet (rung 4) — 0 warnings
16
+ # FAIL real compile errors on wsl via ~/.dotnet/dotnet (rung 2): 3 CS errors, first: …
17
+ # NOT-RUN no rung could run dotnet on wsl; tried: dotnet, ~/.dotnet/dotnet, cmd.exe, winrun, powershell.exe
18
+ # Exit 0 PASS · 1 FAIL (the code is wrong: fix it) · 2 NOT-RUN (the host is wrong: never a
19
+ # project blocker; say which rungs were tried).
20
+ #
21
+ # Rungs by host (docs/TechieFlow-How-It-Works.md; build-invocation-ladder.md §D keeps the
22
+ # runtime-driver table, which this script does not replace):
23
+ # macOS / native Linux / native Windows: dotnet
24
+ # WSL, no MAUI head in the target: dotnet · ~/.dotnet/dotnet · winrun · cmd.exe /c dotnet · powershell.exe dotnet
25
+ # WSL, a MAUI head in the target: cmd.exe /c dotnet · winrun · powershell.exe dotnet (workloads live Windows-side)
26
+ # OpenCode Docker (TF_OPENCODE_DOCKER=1 or winrun + TF_WINDOWS_APP_PATH): MAUI → winrun; else dotnet
27
+ # A workload / SDK-missing error (NETSDK1178, Microsoft.iOS/Android/MacCatalyst.Sdk, Workload ID,
28
+ # WindowsAppSDK, command not found) means the wrong rung: the next one is tried, nothing is logged
29
+ # as a blocker. A CS#### error means the code is wrong: the rung stays and the script exits 1.
30
+ set -u
31
+
32
+ MODE="build"; TARGET=""; EXTRA=()
33
+ case "${1:-}" in
34
+ build|test|run|probe) MODE="$1"; shift ;;
35
+ esac
36
+ while [[ $# -gt 0 ]]; do
37
+ if [[ "$1" == "--" ]]; then shift; EXTRA=("$@"); break; fi
38
+ TARGET="$1"; shift
39
+ done
40
+
41
+ # ---- platform -------------------------------------------------------------------------
42
+ PLATFORM="linux"
43
+ U="$(uname -s 2>/dev/null || echo windows)"
44
+ if [[ "$U" == Darwin* ]]; then PLATFORM="macos"
45
+ elif [[ "$U" == Linux* ]]; then
46
+ if grep -qiE "microsoft|wsl" /proc/version 2>/dev/null; then PLATFORM="wsl"
47
+ elif [[ "${TF_OPENCODE_DOCKER:-}" == "1" ]] || { [[ -x /usr/local/bin/winrun ]] && [[ -n "${TF_WINDOWS_APP_PATH:-}" ]]; }; then PLATFORM="docker"
48
+ fi
49
+ elif [[ "$U" == windows || "$U" == MINGW* || "$U" == MSYS* || "$U" == CYGWIN* ]]; then PLATFORM="windows"
50
+ fi
51
+
52
+ # ---- target ---------------------------------------------------------------------------
53
+ if [[ -z "$TARGET" && "$MODE" != "probe" ]]; then
54
+ mapfile -t CANDS < <(ls -1 *.slnx *.sln 2>/dev/null; ls -1 *.csproj 2>/dev/null)
55
+ if [[ ${#CANDS[@]} -eq 1 ]]; then TARGET="${CANDS[0]}"
56
+ elif [[ ${#CANDS[@]} -gt 1 ]]; then echo "NOT-RUN more than one solution or project here; name one: ${CANDS[*]}"; exit 2
57
+ fi
58
+ fi
59
+
60
+ # ---- does the target include a MAUI head? ----------------------------------------------
61
+ maui=0
62
+ projects=()
63
+ if [[ -n "$TARGET" ]]; then
64
+ case "$TARGET" in
65
+ *.csproj) projects=("$TARGET") ;;
66
+ *.sln) mapfile -t projects < <(grep -oE '"[^"]+\.csproj"' "$TARGET" 2>/dev/null | tr -d '"' | tr '\\' '/') ;;
67
+ *.slnx) mapfile -t projects < <(grep -oE 'Path="[^"]+\.csproj"' "$TARGET" 2>/dev/null | sed 's/Path="//; s/"$//' | tr '\\' '/') ;;
68
+ esac
69
+ base="$(dirname "$TARGET")"
70
+ for p in "${projects[@]}"; do
71
+ f="$p"; [[ -f "$f" ]] || f="$base/$p"
72
+ [[ -f "$f" ]] || continue
73
+ if grep -qiE '<UseMaui>\s*true|Microsoft\.NET\.Sdk\.Maui|net[0-9.]+-(android|ios|maccatalyst|windows10)' "$f"; then maui=1; fi
74
+ done
75
+ fi
76
+
77
+ # ---- rungs -----------------------------------------------------------------------------
78
+ rungs=()
79
+ case "$PLATFORM" in
80
+ macos|linux|windows) rungs=("dotnet") ;;
81
+ docker) if [[ $maui -eq 1 ]]; then rungs=("winrun"); else rungs=("dotnet" "winrun"); fi ;;
82
+ wsl)
83
+ if [[ $maui -eq 1 ]]; then rungs=("cmd.exe" "winrun" "powershell.exe")
84
+ else rungs=("dotnet" "$HOME/.dotnet/dotnet" "winrun" "cmd.exe" "powershell.exe"); fi ;;
85
+ esac
86
+
87
+ if [[ "$MODE" == "probe" ]]; then
88
+ echo "platform: $PLATFORM · target: ${TARGET:-none} · maui head: $([[ $maui -eq 1 ]] && echo yes || echo no)"
89
+ echo "rungs, in order: ${rungs[*]}"
90
+ exit 0
91
+ fi
92
+ [[ -z "$TARGET" && "$MODE" != "run" ]] && { echo "NOT-RUN no .sln, .slnx or .csproj here; name the target"; exit 2; }
93
+
94
+ # ---- the dotnet verb -------------------------------------------------------------------
95
+ verb="$MODE"
96
+ args=("$verb")
97
+ [[ -n "$TARGET" ]] && args+=("$TARGET")
98
+ [[ "$MODE" == "run" ]] && args=("run" "--project" "$TARGET")
99
+ args+=("${EXTRA[@]}")
100
+
101
+ mkdir -p tests/.artifacts/build
102
+ LOG="tests/.artifacts/build/$(date -u +%Y%m%dT%H%M%SZ)-$MODE.log"
103
+ WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload ID|not recognized|WindowsAppSDK|command not found|No such file or directory|is not recognized as an internal or external command|The term .* is not recognized|workload.*not installed|Inadequate permissions'
104
+
105
+ tried=()
106
+ n=0
107
+ for r in "${rungs[@]}"; do
108
+ n=$((n+1))
109
+ case "$r" in
110
+ dotnet) cmd=(dotnet "${args[@]}"); label="dotnet" ;;
111
+ */.dotnet/dotnet) cmd=("$r" "${args[@]}"); label="~/.dotnet/dotnet" ;;
112
+ winrun) cmd=(winrun "dotnet ${args[*]}"); label="winrun dotnet" ;;
113
+ cmd.exe) cmd=(cmd.exe /c "dotnet ${args[*]}"); label="cmd.exe /c dotnet" ;;
114
+ powershell.exe) cmd=(powershell.exe -Command "dotnet ${args[*]}"); label="powershell.exe dotnet" ;;
115
+ esac
116
+ tried+=("$label")
117
+ { echo "### rung $n: ${cmd[*]}"; "${cmd[@]}" 2>&1; echo "### exit $?"; } >> "$LOG" 2>&1
118
+ rc="$(tail -1 "$LOG" | sed 's/### exit //')"
119
+ seg="$(awk -v n="### rung $n:" 'index($0,n)==1{p=1} p' "$LOG")"
120
+ if [[ "$rc" == "0" ]]; then
121
+ warns="$(grep -cE 'warning [A-Z]+[0-9]+' <<<"$seg" || true)"
122
+ echo "PASS $MODE on $PLATFORM via $label (rung $n) — $warns warning line(s); log $LOG"
123
+ exit 0
124
+ fi
125
+ # Code errors: C# (CS), Razor (RZ), Blazor (BL), XAML (XC, XLS), missing reference (MSB/NU).
126
+ # Until 2026-09-06 only CS counted, so a build failing on sixteen Razor RZ9991 errors was
127
+ # taken for a wrong rung and reported NOT-RUN "host issue" (MISS-TechieFlow-20260906-03).
128
+ if grep -qE 'error (CS|RZ|BL|XC|XLS)[0-9]{3,5}|error MSB[0-9]{4}: .*(does not exist|could not be found)|error NU1' <<<"$seg" && ! grep -qE "$WRONG_RUNG" <<<"$seg"; then
129
+ errs="$(grep -cE 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" || true)"
130
+ first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^\s*//' | cut -c1-160)"
131
+ echo "FAIL real errors on $PLATFORM via $label (rung $n): $errs error line(s), first: $first; log $LOG"
132
+ grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | sed 's/^\s*//' | sort -u | head -10
133
+ exit 1
134
+ fi
135
+ if [[ "$MODE" == "test" ]] && grep -qE 'Failed!|Failed:\s+[1-9]|Tests? failed' <<<"$seg"; then
136
+ echo "FAIL tests failed on $PLATFORM via $label (rung $n); log $LOG"
137
+ grep -E 'Failed |Failed!|Error Message|Total tests|Passed!' <<<"$seg" | head -12
138
+ exit 1
139
+ fi
140
+ # anything else (workload, wrong rung, tool absent): try the next rung
141
+ done
142
+ echo "NOT-RUN no rung could $MODE on $PLATFORM; tried: ${tried[*]}. This is a host issue, never a project blocker; log $LOG"
143
+ exit 2
@@ -0,0 +1,64 @@
1
+ # tf-cdp-relay.ps1 — relay a Windows loopback port to every interface (Sitting 4c, 2026-09-06).
2
+ #
3
+ # powershell.exe -File tf-cdp-relay.ps1 -ListenPort 9223 -TargetPort 9222
4
+ #
5
+ # A MAUI Blazor Hybrid app's WebView2 opens its DevTools port on 127.0.0.1 only, so WSL cannot
6
+ # reach it. This relay listens on every interface and forwards to the loopback port, so Playwright
7
+ # in WSL can attach with connectOverCDP('http://<windows host ip>:9223'). Started and stopped by
8
+ # tf-verify-boot.sh; proven on MyDiary's Windows head, 2026-09-06 (the DevGuide run).
9
+ param(
10
+ [int]$ListenPort = 9223,
11
+ [string]$TargetHost = '127.0.0.1',
12
+ [int]$TargetPort = 9222
13
+ )
14
+
15
+ Add-Type -TypeDefinition @"
16
+ using System;
17
+ using System.Net;
18
+ using System.Net.Sockets;
19
+ using System.Threading;
20
+
21
+ public static class TcpRelay {
22
+ public static void Start(int listenPort, string targetHost, int targetPort) {
23
+ var listener = new TcpListener(IPAddress.Any, listenPort);
24
+ listener.Start();
25
+ while (true) {
26
+ var client = listener.AcceptTcpClient();
27
+ var t = new Thread(() => Handle(client, targetHost, targetPort));
28
+ t.IsBackground = true;
29
+ t.Start();
30
+ }
31
+ }
32
+
33
+ private static void Handle(TcpClient client, string targetHost, int targetPort) {
34
+ try {
35
+ using (client)
36
+ using (var target = new TcpClient(targetHost, targetPort)) {
37
+ using (var cs = client.GetStream())
38
+ using (var ts = target.GetStream()) {
39
+ var t1 = new Thread(() => Pump(cs, ts));
40
+ t1.IsBackground = true;
41
+ t1.Start();
42
+ Pump(ts, cs);
43
+ t1.Join();
44
+ }
45
+ }
46
+ } catch {}
47
+ }
48
+
49
+ private static void Pump(System.IO.Stream src, System.IO.Stream dst) {
50
+ var buf = new byte[65536];
51
+ try {
52
+ int n;
53
+ while ((n = src.Read(buf, 0, buf.Length)) > 0) {
54
+ dst.Write(buf, 0, n);
55
+ dst.Flush();
56
+ }
57
+ } catch {}
58
+ try { dst.Close(); } catch {}
59
+ }
60
+ }
61
+ "@
62
+
63
+ Write-Host "relay listening on 0.0.0.0:$ListenPort -> $TargetHost`:$TargetPort"
64
+ [TcpRelay]::Start($ListenPort, $TargetHost, $TargetPort)
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """tf-checklist-edit.py — the checklist edits the bug scripts share (Sitting 4c, 2026-09-06).
3
+
4
+ Imported by tf-triage.py and tf-log-miss.py; not run on its own. Every function takes the
5
+ checklist path, edits the file in place and returns what it did. The shapes follow
6
+ docs/TechieFlow-Document-Schemas.md §3.4: the six-cell row, the `<a id="d-req-…">` entry, one
7
+ acceptance line, a Remarks cell of at most 60 words.
8
+ """
9
+ import datetime
10
+ import os
11
+ import re
12
+
13
+ ROW = re.compile(r"^(\s*\|\s*`?)(REQ-[A-Z]+-\d+)(`?\s*\|)")
14
+ TODAY = datetime.date.today().isoformat()
15
+
16
+
17
+ def read(p):
18
+ with open(p, encoding="utf-8") as f:
19
+ return f.read()
20
+
21
+
22
+ def write(p, s):
23
+ with open(p, "w", encoding="utf-8") as f:
24
+ f.write(s)
25
+
26
+
27
+ def words(s, n=60):
28
+ w = s.split()
29
+ return s if len(w) <= n else " ".join(w[:n]) + "…" # the ellipsis rides on the last word: the checker counts words, and a bare "…" was a 61st (miss 29)
30
+
31
+
32
+ def rows(text):
33
+ """id -> {status, pct, remarks, title, line_no}"""
34
+ out = {}
35
+ for i, line in enumerate(text.splitlines()):
36
+ m = ROW.match(line)
37
+ if m:
38
+ c = [x.strip() for x in line.strip().strip("|").split("|")]
39
+ if len(c) >= 6:
40
+ pct = re.sub(r"[^0-9]", "", c[3])
41
+ out[m.group(2).upper()] = {"status": c[2].strip("`* "), "pct": int(pct) if pct.isdigit() else None,
42
+ "remarks": c[4], "title": c[1].strip("`* "), "line_no": i}
43
+ return out
44
+
45
+
46
+ def set_cells(path, rid, status=None, pct=None, remark=None, append_remark=None):
47
+ """rewrite the Status, % and Remarks cells of one row; returns the prior cells or None"""
48
+ text = read(path)
49
+ lines = text.splitlines(keepends=True)
50
+ prior = None
51
+ for i, line in enumerate(lines):
52
+ m = ROW.match(line)
53
+ if not m or m.group(2).upper() != rid.upper():
54
+ continue
55
+ nl = "\r\n" if line.endswith("\r\n") else "\n"
56
+ c = line.rstrip("\r\n").strip().strip("|").split("|")
57
+ if len(c) < 6:
58
+ break
59
+ prior = {"status": c[2].strip().strip("`* "), "pct": c[3].strip(), "remarks": c[4].strip()}
60
+ if status is not None:
61
+ c[2] = f" {status} "
62
+ if pct is not None:
63
+ c[3] = f" {pct}% " if "%" in c[3] or c[3].strip() == "" else f" {pct} "
64
+ if remark is not None:
65
+ c[4] = f" {words(remark.replace('|', '/'))} "
66
+ elif append_remark:
67
+ c[4] = f" {words((prior['remarks'] + ' ' + append_remark.replace('|', '/')).strip())} "
68
+ lines[i] = "|" + "|".join(c) + "|" + nl
69
+ break
70
+ if prior is not None:
71
+ write(path, "".join(lines))
72
+ return prior
73
+
74
+
75
+ def next_id(text, prefix):
76
+ ids = [int(m.group(1)) for m in re.finditer(rf"REQ-{prefix}-(\d{{3}})", text)]
77
+ return f"REQ-{prefix}-{(max(ids) + 1 if ids else 1):03d}"
78
+
79
+
80
+ def add_row(path, prefix, title, acceptance, section=None, remark=None, status="Not Started", mockup=None, brd="BRD-pending"):
81
+ """append a row to the Requirements Status table and a detail entry under `section`
82
+ (a new section at the end when it does not exist); returns the new id"""
83
+ text = read(path)
84
+ rid = next_id(text, prefix)
85
+ nl = "\r\n" if "\r\n" in text else "\n"
86
+ lines = text.split(nl)
87
+ # the table: after the last REQ row
88
+ last = max((i for i, l in enumerate(lines) if ROW.match(l)), default=None)
89
+ if last is None:
90
+ raise SystemExit("no Requirements Status table with REQ rows in " + path)
91
+ remark = words((remark or f"logged from UAT {TODAY}").replace("|", "/"))
92
+ anchor = f"d-{rid.lower()}"
93
+ lines.insert(last + 1, f"| {rid} | {title.replace('|', '/')} | {status} | 0% | {remark} | [d](#{anchor}) |")
94
+ # the entry
95
+ entry = [f"- <a id=\"{anchor}\"></a> **{rid}** ({brd}) {title}" + (f" Mockup: {mockup}" if mockup else ""),
96
+ f" - *Acceptance:* {acceptance}", ""]
97
+ sec_idx = None
98
+ if section:
99
+ for i, l in enumerate(lines):
100
+ if re.match(rf"^##\s+(?:page|screen)?\s*:?\s*{re.escape(section)}\s*$", l, re.I):
101
+ sec_idx = i
102
+ break
103
+ if sec_idx is not None:
104
+ # after the last entry of that section: the line before the next "## " heading
105
+ j = sec_idx + 1
106
+ while j < len(lines) and not lines[j].startswith("## "):
107
+ j += 1
108
+ while j > sec_idx + 1 and lines[j - 1].strip() == "":
109
+ j -= 1
110
+ lines[j:j] = [""] + entry[:-1]
111
+ else:
112
+ heading = f"## {section}" if section else "## Logged from UAT"
113
+ if not any(l.strip() == heading for l in lines):
114
+ lines += ["", heading, ""]
115
+ lines += entry[:-1]
116
+ write(path, nl.join(lines).rstrip(nl) + nl)
117
+ return rid
118
+
119
+
120
+ def demote(path, rid, symptom, kind=None, evidence=None, source="owner", prefix="⚠ UAT bug"):
121
+ """Needs re-verify, % capped at 75, a dated Remark; returns the prior cells"""
122
+ r = rows(read(path)).get(rid.upper())
123
+ if not r:
124
+ return None
125
+ pct = min(r["pct"] if r["pct"] is not None else 75, 75)
126
+ ev = f"evidence: {evidence}" if evidence else f"evidence: {source} report"
127
+ remark = f"{prefix} {TODAY}: {symptom} ({ev}" + (f"; kind: {kind}" if kind else "") + ")"
128
+ return set_cells(path, rid, status="Needs re-verify", pct=pct, remark=remark)
129
+
130
+
131
+ def note(path, rid, text):
132
+ """a dated remark, no status change"""
133
+ return set_cells(path, rid, append_remark=f"{TODAY} triage: {text}")