@techierathore/techieflow 1.0.4 → 1.1.1

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 (148) hide show
  1. package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
  2. package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
  3. package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
  4. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  6. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  9. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
  10. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  11. package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
  12. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  14. package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
  15. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  16. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  17. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  18. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  19. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  20. package/.opencode/plugin/techieflow.js +194 -14
  21. package/.tfcore/agents/flow-master.md +2 -0
  22. package/.tfcore/agents/verifier.md +2 -2
  23. package/.tfcore/core-config.yaml +2 -1
  24. package/.tfcore/hooks/block-git.sh +4 -2
  25. package/.tfcore/hooks/guard-build.sh +5 -1
  26. package/.tfcore/hooks/guard-db.sh +112 -13
  27. package/.tfcore/hooks/guard-status-html.sh +86 -2
  28. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  29. package/.tfcore/hooks/guard-verify.sh +8 -3
  30. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  31. package/.tfcore/hooks/yolo-allow.sh +27 -0
  32. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  33. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  34. package/.tfcore/standards/owner-words.txt +23 -0
  35. package/.tfcore/tasks/_develop-mode.md +34 -0
  36. package/.tfcore/tasks/_owner-language.md +1 -0
  37. package/.tfcore/tasks/_status-update-gate.md +4 -3
  38. package/.tfcore/tasks/_yolo-mode.md +2 -2
  39. package/.tfcore/tasks/amend-docs.md +2 -2
  40. package/.tfcore/tasks/day1-brownfield.md +3 -3
  41. package/.tfcore/tasks/day1-greenfield.md +5 -5
  42. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  43. package/.tfcore/tasks/fix-issues.md +3 -2
  44. package/.tfcore/tasks/generate-html.md +2 -2
  45. package/.tfcore/tasks/handoff-phase.md +2 -2
  46. package/.tfcore/tasks/mockups.md +2 -2
  47. package/.tfcore/tasks/productguide.md +2 -2
  48. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  49. package/.tfcore/tasks/triage-and-fix.md +2 -2
  50. package/.tfcore/tasks/triage-issues.md +2 -2
  51. package/.tfcore/tasks/verify-phase.md +12 -11
  52. package/.tfcore/telemetry/SCHEMA.md +10 -2
  53. package/.tfcore/telemetry/install-metrics.sh +3 -2
  54. package/.tfcore/telemetry/tf-metrics.sh +38 -2
  55. package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
  56. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  57. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  58. package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
  59. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  60. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  61. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  62. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  63. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  64. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  65. package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
  66. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  67. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  68. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  69. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  70. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  71. package/.tfcore/utils/tf-appium.mjs +179 -0
  72. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  73. package/.tfcore/utils/tf-assets.sh +97 -12
  74. package/.tfcore/utils/tf-brd-status.py +14 -5
  75. package/.tfcore/utils/tf-build-list.py +95 -12
  76. package/.tfcore/utils/tf-build-list.sh +2 -1
  77. package/.tfcore/utils/tf-build.sh +163 -15
  78. package/.tfcore/utils/tf-checklist-edit.py +21 -3
  79. package/.tfcore/utils/tf-ci-repro.py +685 -0
  80. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  81. package/.tfcore/utils/tf-day1-files.py +55 -28
  82. package/.tfcore/utils/tf-develop.sh +264 -0
  83. package/.tfcore/utils/tf-devguide-list.py +53 -24
  84. package/.tfcore/utils/tf-doc-check.py +218 -26
  85. package/.tfcore/utils/tf-doc-tests.sh +116 -0
  86. package/.tfcore/utils/tf-emit.sh +132 -45
  87. package/.tfcore/utils/tf-feedback.sh +13 -0
  88. package/.tfcore/utils/tf-fix-close.sh +87 -31
  89. package/.tfcore/utils/tf-goal.sh +12 -12
  90. package/.tfcore/utils/tf-harness.sh +9 -4
  91. package/.tfcore/utils/tf-lock.sh +29 -0
  92. package/.tfcore/utils/tf-log-miss.py +34 -8
  93. package/.tfcore/utils/tf-login.mjs +167 -0
  94. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  95. package/.tfcore/utils/tf-menus.mjs +401 -0
  96. package/.tfcore/utils/tf-misses-md.py +10 -4
  97. package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
  98. package/.tfcore/utils/tf-mockup-parity.sh +25 -2
  99. package/.tfcore/utils/tf-owner-text.py +233 -0
  100. package/.tfcore/utils/tf-owner-text.sh +14 -0
  101. package/.tfcore/utils/tf-phase.sh +38 -4
  102. package/.tfcore/utils/tf-portable.sh +230 -0
  103. package/.tfcore/utils/tf-render-html.py +79 -5
  104. package/.tfcore/utils/tf-req-ids.py +36 -0
  105. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  106. package/.tfcore/utils/tf-split-brd.py +11 -1
  107. package/.tfcore/utils/tf-stack-check.sh +3 -0
  108. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  109. package/.tfcore/utils/tf-status-facts.py +160 -45
  110. package/.tfcore/utils/tf-theme.mjs +189 -0
  111. package/.tfcore/utils/tf-triage.py +74 -17
  112. package/.tfcore/utils/tf-triage.sh +2 -0
  113. package/.tfcore/utils/tf-verify-boot.sh +529 -54
  114. package/.tfcore/utils/tf-verify-emit.sh +57 -17
  115. package/.tfcore/utils/tf-verify-env.sh +33 -4
  116. package/.tfcore/utils/tf-verify-list.py +290 -24
  117. package/.tfcore/utils/tf-verify-list.sh +3 -1
  118. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  119. package/.tfcore/utils/tf-verify-screens.mjs +341 -44
  120. package/.tfcore/utils/tf-verify-screens.sh +23 -5
  121. package/.tfcore/utils/tf-verify-tests.sh +310 -19
  122. package/.tfcore/utils/tf-verify-verdict.py +163 -21
  123. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  124. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  125. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  126. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  127. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  128. package/.tfcore/utils/tf-yolo.sh +31 -3
  129. package/.tfcore/utils/tf_defect.py +27 -0
  130. package/.tfcore/utils/tf_feedback.py +272 -0
  131. package/.tfcore/utils/tf_roadmap.py +35 -0
  132. package/.tfcore/utils/tf_stack_check.py +93 -0
  133. package/README.md +6 -2
  134. package/docs/TechieFlow-Installation.md +5 -3
  135. package/opencode.jsonc +6 -2
  136. package/package.json +1 -1
  137. package/scripts/install.mjs +13 -2
  138. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  139. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  140. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  141. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  142. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  143. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  144. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  145. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  146. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  147. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  148. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
@@ -10,9 +10,14 @@
10
10
  # Only when PROJECT-STATUS.md was written in this session (newer than the
11
11
  # session pointer .tfcore/.session/<harness>.json):
12
12
  # 2. tf-doc-check.sh FAILs on PROJECT-STATUS.md -> fix the file, re-run the check
13
+ # 2c. a BRD or UI design written this session leaves a screen with no row, UI design
14
+ # entry or mockup -> add it, re-run the check (TF-030)
13
15
  # 3. docs/<App>-BRD.md older than docs/<App>-Checklist.md
14
16
  # -> bash .tfcore/utils/tf-brd-status.sh <App>
15
17
  # 4. no runs.jsonl record after the status write -> the run record (_metrics-emit-gate.md)
18
+ # Only on the turn that wrote PROJECT-STATUS.md (a command's closing turn):
19
+ # 5. tf-owner-text.sh FAILs on the closing message or a free-form document it
20
+ # hands the owner -> rewrite it for the owner
16
21
  #
17
22
  # Wired in .claude/settings.json -> hooks.Stop; OpenCode via
18
23
  # .opencode/plugin/techieflow.js session.idle (a nudge: OpenCode has no blocking
@@ -28,6 +33,7 @@ import datetime
28
33
  import glob
29
34
  import json
30
35
  import os
36
+ import re
31
37
  import subprocess
32
38
  import sys
33
39
 
@@ -58,10 +64,18 @@ elif md_mtime > os.path.getmtime(html):
58
64
  def session_start():
59
65
  """mtime of the harness's session pointer; None when there is none (checks 2-4 skipped)."""
60
66
  d = os.path.join(root, ".tfcore", ".session")
61
- if any(k.startswith("OPENCODE") for k in os.environ):
67
+ # The harness in the order tf-harness.sh and tf-emit.sh take it: TF_HARNESS (the OpenCode
68
+ # plugin sets it on every guard it spawns), then Claude Code's own variables, then OPENCODE*.
69
+ # OpenCode first let any OPENCODE_* in the owner's shell (an API key, a config path) send a
70
+ # Claude Code turn to opencode.json; with no such file, checks 2-5 never ran (2026-09-27).
71
+ tf = os.environ.get("TF_HARNESS")
72
+ if tf == "opencode":
62
73
  cands = [os.path.join(d, "opencode.json")]
63
- elif os.environ.get("CLAUDECODE") or any(k.startswith("CLAUDE_CODE") for k in os.environ):
74
+ elif tf == "claude-code" or any(os.environ.get(k) for k in (
75
+ "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_PROJECT_DIR")):
64
76
  cands = [os.path.join(d, "claude-code.json")]
77
+ elif any(k.startswith("OPENCODE") for k in os.environ):
78
+ cands = [os.path.join(d, "opencode.json")]
65
79
  else:
66
80
  cands = [os.path.join(d, "claude-code.json"), os.path.join(d, "opencode.json")]
67
81
  times = [os.path.getmtime(p) for p in cands if os.path.isfile(p)]
@@ -104,6 +118,31 @@ if written_this_session:
104
118
  except Exception:
105
119
  pass
106
120
 
121
+ # 2c. a screen is not finished until it has its row, its UI design entry and its mockup. An
122
+ # amendment added a Price providers screen inside a requirement with none of the three, the
123
+ # checker's one finding was on the checklist and passed as old, and the page was built with no
124
+ # design (TfLens TF-030, 2026-09-11). On a turn that wrote a BRD or a UI design, those findings
125
+ # block; a BRD's other findings stay with *amend-docs, as before.
126
+ try:
127
+ chk = os.path.join(root, ".tfcore", "utils", "tf-doc-check.sh")
128
+ chain = re.compile(r'names the screen "|has no "### Screen:" entry|is in the UIDesign but not in the BRD'
129
+ r'|has no mockup link \(docs/mockups')
130
+ apps = set()
131
+ for f in glob.glob(os.path.join(root, "docs", "*.md")):
132
+ m = re.match(r"^(.+?)-(?:P\d+-)?(?:BRD|UIDesign)\.md$", os.path.basename(f))
133
+ if m and os.path.getmtime(f) >= start:
134
+ apps.add(m.group(1))
135
+ for app in sorted(apps) if os.path.isfile(chk) else ():
136
+ out = subprocess.run(["bash", chk, "--quiet", "--app", app], cwd=root,
137
+ capture_output=True, text=True, timeout=180).stdout
138
+ fails = [l for l in out.splitlines() if l.startswith("FAIL") and chain.search(l)]
139
+ if fails:
140
+ problems.append(f"a screen in {app}'s documents has no row, UI design entry or mockup ({len(fails)} line(s)). "
141
+ f"Add what is missing, then run: bash .tfcore/utils/tf-doc-check.sh --app {app}")
142
+ problems.extend(" " + l for l in fails[:5])
143
+ except Exception:
144
+ pass
145
+
107
146
  # 3. the BRD's Development status table is not older than the checklist
108
147
  try:
109
148
  for cl in glob.glob(os.path.join(root, "docs", "*-Checklist.md")):
@@ -136,6 +175,51 @@ if written_this_session:
136
175
  except Exception:
137
176
  pass
138
177
 
178
+ # 5. what the owner reads when a command ends: the closing message, and every free-form
179
+ # document it hands over that this turn wrote, through tf-owner-text.sh. TfLens's hand-off of
180
+ # 2026-09-11 reached the owner in jargon, named upstream problems with no word on what they
181
+ # touch, called two fixed ones open, and gave no prompt (MISS-TechieFlow-20260911-03). Only on
182
+ # the turn that closed a command -- PROJECT-STATUS written after this turn's prompt -- so an
183
+ # ordinary conversation is never held to a hand-off's shape. Claude Code hands the hook the
184
+ # message; the OpenCode plugin passes the same two fields.
185
+ try:
186
+ message = data.get("last_assistant_message") or ""
187
+ turn = data.get("turn_started")
188
+ if turn is None and data.get("transcript_path") and os.path.isfile(data["transcript_path"]):
189
+ with open(data["transcript_path"], encoding="utf-8", errors="replace") as f:
190
+ for line in f:
191
+ try:
192
+ r = json.loads(line)
193
+ except Exception:
194
+ continue
195
+ c = (r.get("message") or {}).get("content")
196
+ prompt = r.get("type") == "user" and not r.get("isMeta") and (
197
+ isinstance(c, str) or (isinstance(c, list) and any(isinstance(x, dict) and x.get("type") == "text" for x in c)))
198
+ if prompt and r.get("timestamp"):
199
+ turn = r["timestamp"]
200
+ if isinstance(turn, str):
201
+ turn = datetime.datetime.strptime(turn[:19], "%Y-%m-%dT%H:%M:%S").replace(
202
+ tzinfo=datetime.timezone.utc).timestamp()
203
+ elif isinstance(turn, (int, float)) and turn > 1e12:
204
+ turn = turn / 1000.0
205
+ chk = os.path.join(root, ".tfcore", "utils", "tf-owner-text.sh")
206
+ if message.strip() and turn and md_mtime >= turn - 2 and os.path.isfile(chk):
207
+ handed = []
208
+ for m in set(re.findall(r"(?<![\w/.-])((?:docs/)?[\w.-]+\.md)\b", message)):
209
+ p = os.path.join(root, m if m.startswith("docs/") else os.path.join("docs", m))
210
+ if os.path.isfile(p) and os.path.getmtime(p) >= turn - 2:
211
+ handed.append(os.path.relpath(p, root))
212
+ res = subprocess.run(["bash", chk, "--root", root, "--final", "--stdin"] + sorted(handed), cwd=root,
213
+ input=message, capture_output=True, text=True, timeout=60)
214
+ fails = [l for l in res.stdout.splitlines() if l.startswith("FAIL")]
215
+ if fails:
216
+ problems.append(f"what you are handing the owner is not written for the owner ({len(fails)} line(s)). "
217
+ "Fix each, then write the closing message again — plain words, every upstream problem "
218
+ "with what it affects and its prompt, and the next prompt in a code block:")
219
+ problems.extend(" " + l for l in fails[:8])
220
+ except Exception:
221
+ pass
222
+
139
223
  if not problems:
140
224
  sys.exit(0)
141
225
 
@@ -26,7 +26,8 @@
26
26
  # - any `|| docker compose up` fallback, which widens the command precisely when it failed
27
27
  # - `docker compose run|create`, `docker run`, `docker volume create` — creating, not starting
28
28
  # - an inline or exported connection-string variable in front of a test/run command
29
- # ALLOWS: `docker start <name>`, `docker compose up <service>`, `docker compose start`,
29
+ # ALLOWS: during *develop-end-to-end only, one `docker run|create --name <app>-db …` (_develop-mode.md);
30
+ # always: `docker start <name>`, `docker compose up <service>`, `docker compose start`,
30
31
  # `docker ps|logs|inspect`, and anything that is not one of the above.
31
32
  #
32
33
  # Wired in .claude/settings.json -> hooks.PreToolUse (matcher "Bash"); OpenCode via
@@ -97,6 +98,20 @@ if re.search(r"\|\|\s*" + COMPOSE + r"\s+up\b", cmd, re.I):
97
98
  "Drop the fallback. If the named service does not exist, the name is wrong — fix "
98
99
  "the name or ask; do not start everything instead.")
99
100
 
101
+ # *develop-end-to-end (_develop-mode.md): the owner asked for the database to be created when none is
102
+ # reachable. While that run is on (.tfcore/.session/develop.json), ONE command is allowed through: a single
103
+ # `docker run|create` of the container that rule names, `--name <app>-db`, nothing else in the command.
104
+ # Found by the first proof run, which stopped "blocked" because this hook refused it (2026-10-08).
105
+ root = os.environ.get("CLAUDE_PROJECT_DIR") or os.environ.get("TF_PROJECT_DIR") or os.getcwd()
106
+ dev = os.path.join(root, ".tfcore", ".session", "develop.json")
107
+ if os.path.isfile(dev):
108
+ try: app = (json.load(open(dev)).get("app") or "").lower()
109
+ except Exception: app = ""
110
+ one = not re.search(r"(?:&&|\|\||;|\||\n|\$\(|`)", cmd) # && first: the portability check reads a pipe-ampersand pair as bash 4
111
+ if app and one and re.match(r"\s*docker\s+(?:run|create)\b", cmd, re.I) \
112
+ and re.search(r"--name[ =]%s-db(?:\s|$)" % re.escape(app), cmd):
113
+ sys.exit(0)
114
+
100
115
  if re.search(COMPOSE + r"\s+(?:run|create)\b", cmd, re.I) or \
101
116
  re.search(r"\bdocker\s+(?:run|create)\b", cmd, re.I) or \
102
117
  re.search(r"\bdocker\s+volume\s+create\b", cmd, re.I):
@@ -99,14 +99,19 @@ if ledger is not None:
99
99
  sys.exit(0) # a same-day ledger from a framework version before Sitting 4c: date unlocks
100
100
  else:
101
101
  rows = ledger.get("rows") or {}
102
- not_listed = sorted(i for i in introduced if rows.get(i) != "PASS")
102
+ # a ledger kept row by row holds older runs' rows too (Lekhak TF-007): only a row graded
103
+ # today unlocks
104
+ dates = ledger.get("row_dates")
105
+ not_listed = sorted(i for i in introduced if rows.get(i) != "PASS"
106
+ or (isinstance(dates, dict) and str(dates.get(i, "")) != today))
103
107
  if not introduced:
104
108
  # a Verified cell without a readable row id (a malformed line): refuse, nothing can vouch for it
105
109
  not_listed = ["(row id not readable)"]
106
110
  if not not_listed:
107
111
  sys.exit(0)
108
- problem = ("lists these rows as " + ", ".join(f"{i}: {rows.get(i, 'absent')}" for i in not_listed)
109
- + ", not PASS")
112
+ problem = ("lists these rows as " + ", ".join(
113
+ f"{i}: {rows.get(i, 'absent')}" + (f" (graded {dates.get(i)})" if isinstance(dates, dict) and dates.get(i) and dates.get(i) != today else "")
114
+ for i in not_listed) + ", not PASS today")
110
115
 
111
116
  print(
112
117
  "BLOCKED by TechieFlow policy: this write INTRODUCES a `Verified` status, "
@@ -26,6 +26,8 @@
26
26
  # test-results/ test-results-*/ scripts-*/ playwright-report/
27
27
  # The project's own tracked scripts/ is never touched (needs the hyphen).
28
28
  # tests/verify/ (promoted, tracked specs) is never touched (outside the roots).
29
+ # A file under the roots that a test names by path (tests/.artifacts/<folder>/<name>) is kept at
30
+ # any age, with everything below it (Lekhak TF-004); the summary says how many were kept.
29
31
  #
30
32
  # RETENTION: default 7 days. Override per project with `artifactRetentionDays: N`
31
33
  # in .tfcore/core-config.yaml, or per run with TF_ARTIFACT_RETENTION_DAYS=N.
@@ -98,6 +100,42 @@ DAYS = retention_days()
98
100
  CUTOFF = time.time() - DAYS * 86400
99
101
 
100
102
  SWEEP_ROOTS = [os.path.join("tests", ".artifacts"), ".verify"]
103
+
104
+ # ---- what a test names is not litter (Lekhak TF-004) ---------------------------
105
+ # A spec that reads tests/.artifacts/harness/hindi-src lost it on the eighth day, and two rows failed
106
+ # "the crawl produced no result" on a working product. Any path under the sweep roots that a file of
107
+ # the project's tests (or its Playwright config) names is kept, with everything below it, whatever its
108
+ # age. The right home for a fixture is still tests/verify/fixtures/, outside the sweep.
109
+ # Only a NAMED thing inside a folder counts (tests/.artifacts/<folder>/<name>): a folder a test writes
110
+ # into is named with a variable after it (`shots/${name}.png`), which the pattern stops at, and the
111
+ # framework's own output folders are never kept. The Playwright config is not read: it names outputs.
112
+ KEEP_RE = re.compile(r"(?:tests[/\\]+\.artifacts|\.verify)[/\\]+[^'\"`\s)\]},;*?<>|${}+]+")
113
+ OUTPUTS = ("tests/.artifacts/verify/", "tests/.artifacts/build/", "tests/.artifacts/test-results",
114
+ "tests/.artifacts/playwright", "tests/.artifacts/regression", "tests/.artifacts/doc-check/")
115
+ TEXT_EXT = (".ts", ".js", ".mjs", ".cjs", ".cs", ".py", ".json", ".sh", ".cmd", ".ps1", ".yaml", ".yml", ".txt", ".md")
116
+ keep = set()
117
+ def scan_refs(fp):
118
+ try:
119
+ if os.path.getsize(fp) > 2 * 1024 * 1024:
120
+ return
121
+ with open(fp, encoding="utf-8", errors="replace") as fh:
122
+ for m in KEEP_RE.finditer(fh.read()):
123
+ k = re.sub(r"/+", "/", m.group(0).replace("\\", "/")).rstrip("/.")
124
+ if k.count("/") >= (2 if k.startswith(".verify/") else 3) and not (k + "/").startswith(OUTPUTS):
125
+ keep.add(k)
126
+ except Exception:
127
+ pass
128
+ tests_dir = os.path.join(root, "tests")
129
+ if os.path.isdir(tests_dir) and not os.path.islink(tests_dir):
130
+ for dp, dns, fns in os.walk(tests_dir, followlinks=False):
131
+ dns[:] = [d for d in dns if d not in (".artifacts", "node_modules", "bin", "obj", ".git")]
132
+ for f in fns:
133
+ if f.endswith(TEXT_EXT):
134
+ scan_refs(os.path.join(dp, f))
135
+ kept_n = 0
136
+ def kept(fp):
137
+ rel = os.path.relpath(fp, root).replace(os.sep, "/")
138
+ return any(rel == k or rel.startswith(k + "/") for k in keep)
101
139
  LEGACY_ROOT_GLOBS = ["test-results", "test-results-*", "scripts-*", "playwright-report"]
102
140
 
103
141
  removed_files = 0
@@ -162,6 +200,9 @@ if DAYS > 0:
162
200
  except Exception:
163
201
  continue
164
202
  if st.st_mtime < CUTOFF:
203
+ if keep and kept(fp):
204
+ kept_n += 1
205
+ continue
165
206
  rm_file(fp)
166
207
  # remove dirs emptied by this sweep (never the root itself)
167
208
  if dp != top:
@@ -193,6 +234,11 @@ if removed_files or removed_dirs or legacy:
193
234
  f"tests/.artifacts/ and .verify/"]
194
235
  if legacy:
195
236
  parts.append("banned repo-root legacy dirs removed: " + ", ".join(sorted(legacy)))
237
+ if kept_n:
238
+ parts.append(f"kept {kept_n} old file(s) a test names by path (move them to tests/verify/fixtures/)")
196
239
  print("; ".join(parts))
240
+ elif kept_n:
241
+ print(f"TechieFlow sweep-artifacts: kept {kept_n} file(s) older than {DAYS}d because a test names them by path; "
242
+ "move them to tests/verify/fixtures/")
197
243
  PY
198
244
  exit 0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — while YOLO is on, nothing asks (owner, 2026-10-14: "When I am running
3
+ # something in YOLO mode I am expecting that it doesn't ask me for permission whatever the case is").
4
+ #
5
+ # While the flag is live (TF_YOLO=1, or .tfcore/.session/yolo.json not expired — tf-yolo.sh is-on
6
+ # decides) it answers every tool call with permissionDecision "allow", in the session already
7
+ # running. A refusal still wins: the other PreToolUse hooks (block-git.sh for version-control
8
+ # writes, guard-build.sh, guard-db.sh …) exit 2 and the call is blocked, and the deny rules in
9
+ # settings.json still apply. Claude Code still asks where a hook's allow is not enough (an ask rule,
10
+ # its own safety checks); only a session started with `claude --permission-mode bypassPermissions`
11
+ # drops those, and project settings cannot set that mode (Claude Code refuses it there).
12
+ # While YOLO is off it says nothing, and puts the OpenCode config back if a flag expired or went
13
+ # away without `off` (tf-yolo.sh sync).
14
+ #
15
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "*"). OpenCode's side is the plugin's
16
+ # permission.ask and the OpenCode config tf-yolo.sh rewrites. Fails OPEN: no output, exit 0.
17
+
18
+ cat >/dev/null
19
+ ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
20
+ YOLO="$ROOT/.tfcore/utils/tf-yolo.sh"
21
+ [[ -f "$YOLO" ]] || exit 0
22
+ if CLAUDE_PROJECT_DIR="$ROOT" bash "$YOLO" is-on >/dev/null 2>&1; then
23
+ printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"TechieFlow YOLO is on: no permission prompts (tf-yolo.sh off to end it)"}}'
24
+ elif [[ -f "$ROOT/.tfcore/.session/yolo-perms.json" ]]; then
25
+ CLAUDE_PROJECT_DIR="$ROOT" bash "$YOLO" sync >/dev/null 2>&1
26
+ fi
27
+ exit 0
@@ -0,0 +1,191 @@
1
+ // blazor-zoom.cs.txt — copy to the MAUI project's root as BlazorZoom.cs and set the namespace
2
+ // (coding-standards-dotnet.md §9b). Kept as .txt so a project whose .csproj sits at the repository root
3
+ // does not compile the framework's copy. Then two hooks:
4
+ // MainPage (the page holding the BlazorWebView):
5
+ // blazorWebView.BlazorWebViewInitialized += (sender, args) => BlazorZoom.Attach(args.WebView);
6
+ // Platforms/MacCatalyst/AppDelegate.cs (inside the MauiUIApplicationDelegate class):
7
+ // public override void BuildMenu(UIKit.IUIMenuBuilder builder) { base.BuildMenu(builder); BlazorZoom.BuildMenu(builder); }
8
+ // [Export("tfZoomIn:")] public void ZoomIn(NSObject aSender) => BlazorZoom.ZoomBy(1);
9
+ // [Export("tfZoomOut:")] public void ZoomOut(NSObject aSender) => BlazorZoom.ZoomBy(-1);
10
+ // [Export("tfZoomReset:")] public void ZoomReset(NSObject aSender) => BlazorZoom.Reset();
11
+ // Optional: set BlazorZoom.FirstLevel before the page is built (for example from the screen size) to open
12
+ // at more than 100% before anyone has zoomed; Cmd/Ctrl+0 still means 100%.
13
+ // Proved on Lekhak's BlogAdmin (macOS 27, 2026-10-11): bash .tfcore/utils/tf-verify-zoom.sh checks it.
14
+
15
+ #nullable enable
16
+ #if MACCATALYST
17
+ using Foundation;
18
+ using UIKit;
19
+ using WebKit;
20
+ #endif
21
+
22
+ namespace YourApp;
23
+
24
+ /// <summary>
25
+ /// Zoom in, zoom out and actual size for the BlazorWebView, with the operating system's keys.
26
+ /// </summary>
27
+ /// <remarks>
28
+ /// <para><b>Purpose:</b> [coding-standards-dotnet.md §9b] On a high-resolution Mac the text was too small and
29
+ /// Cmd+= did nothing: a Mac Catalyst WKWebView has no zoom of its own. Every Blazor Hybrid head zooms from 50% to
30
+ /// 300% in 10% steps, and the level is kept in MAUI Preferences across restarts.</para>
31
+ /// <para><b>Code Flow:</b> <c>MainPage</c> hands the platform web view to <see cref="Attach"/> once the BlazorWebView is
32
+ /// initialised, which applies the saved level. Mac Catalyst: the View menu's Zoom In (Cmd+=), Zoom Out (Cmd+-) and
33
+ /// Actual Size (Cmd+0), added by <see cref="BuildMenu"/> from the app delegate, call <see cref="ZoomBy"/> and
34
+ /// <see cref="Reset"/>, which set <c>WKWebView.PageZoom</c> on every attached web view, one per window. Windows: WebView2's own zoom (25% to 500%, uneven steps,
35
+ /// not kept) is turned off, and a script run on every page handles Ctrl+plus, Ctrl+minus, Ctrl+0 and Ctrl+wheel with
36
+ /// the same steps, posting each new level back to be saved (and keeping it for the session, so a page reload keeps
37
+ /// it). The WinUI WebView2 control does not expose WebView2's own ZoomFactor, so that cannot be saved instead.</para>
38
+ /// </remarks>
39
+ public static class BlazorZoom
40
+ {
41
+ /// <summary>The Preferences key the zoom level is kept under.</summary>
42
+ public const string PreferenceKey = "tf.zoom";
43
+
44
+ /// <summary>Smallest zoom, 50%.</summary>
45
+ public const double Min = 0.5;
46
+
47
+ /// <summary>Largest zoom, 300%.</summary>
48
+ public const double Max = 3.0;
49
+
50
+ /// <summary>One step, 10%.</summary>
51
+ public const double Step = 0.1;
52
+
53
+ /// <summary>The level used until one is saved; 1.0 unless the app sets it before the page is built.</summary>
54
+ public static double FirstLevel { get; set; } = 1.0;
55
+
56
+ /// <summary>The zoom level now in use, 1.0 being actual size.</summary>
57
+ public static double Level { get; private set; } = 1.0;
58
+
59
+ /// <summary>Rounds to a whole step and keeps the level inside 50% to 300%.</summary>
60
+ /// <param name="aLevel">The level asked for.</param>
61
+ /// <returns>The level that will be used.</returns>
62
+ public static double Clamp(double aLevel) => Math.Round(Math.Clamp(aLevel, Min, Max) * 10) / 10;
63
+
64
+ #if MACCATALYST
65
+ // every window's web view, weakly, so a Mac app with several BlazorWebView windows zooms them all (Chatur TF-010)
66
+ private static readonly List<WeakReference<WKWebView>> lstWebViews = new();
67
+ private static bool blnMenuSet;
68
+
69
+ /// <summary>Takes the platform web view and applies the saved level.</summary>
70
+ /// <param name="aPlatformWebView">The <c>BlazorWebViewInitializedEventArgs.WebView</c>.</param>
71
+ public static void Attach(object aPlatformWebView)
72
+ {
73
+ if (aPlatformWebView is WKWebView objWebView)
74
+ {
75
+ lstWebViews.Add(new WeakReference<WKWebView>(objWebView));
76
+ }
77
+
78
+ Apply(Preferences.Default.Get(PreferenceKey, FirstLevel));
79
+ if (!blnMenuSet && OperatingSystem.IsMacCatalystVersionAtLeast(26))
80
+ {
81
+ // From macOS 26 the main menu is built here: what an app delegate's BuildMenu changes is ignored.
82
+ // Format is off because its Font › Bigger and Smaller already own Cmd+= and Cmd+-, and UIKit
83
+ // silently drops a menu whose shortcut repeats one; Inspector is off because the app has none.
84
+ blnMenuSet = true;
85
+ var objConfig = new UIMainMenuSystemConfiguration
86
+ {
87
+ TextFormattingPreference = UIMenuSystemElementGroupPreference.Removed,
88
+ InspectorPreference = UIMenuSystemElementGroupPreference.Removed,
89
+ };
90
+ UIMainMenuSystem.SharedSystem.SetBuildConfiguration(objConfig, AddCommands);
91
+ }
92
+ }
93
+
94
+ /// <summary>Adds Zoom In, Zoom Out and Actual Size to the top of the View menu, before macOS 26.</summary>
95
+ /// <param name="aBuilder">The builder the app delegate's <c>BuildMenu</c> receives.</param>
96
+ /// <remarks>The selectors are answered by <c>[Export]</c> methods on the app delegate, which is on the
97
+ /// responder chain whatever has focus inside the web view. From macOS 26 <see cref="Attach"/> does this.</remarks>
98
+ public static void BuildMenu(IUIMenuBuilder aBuilder)
99
+ {
100
+ if (!OperatingSystem.IsMacCatalystVersionAtLeast(26) && aBuilder.System?.Handle == UIMenuSystem.MainSystem.Handle)
101
+ {
102
+ aBuilder.RemoveMenu(UIMenuIdentifier.Format.GetConstant()!);
103
+ AddCommands(aBuilder);
104
+ }
105
+ }
106
+
107
+ /// <summary>Zooms in (a positive step) or out (a negative one).</summary>
108
+ /// <param name="aSteps">How many 10% steps.</param>
109
+ public static void ZoomBy(int aSteps) => Apply(Level + (aSteps * Step));
110
+
111
+ /// <summary>Back to actual size.</summary>
112
+ public static void Reset() => Apply(1.0);
113
+
114
+ private static void AddCommands(IUIMenuBuilder aBuilder)
115
+ {
116
+ var objView = aBuilder.GetMenu(UIMenuIdentifier.View.GetConstant()!);
117
+ if (objView == null || objView.Children.Any(c => c.Title == "Zoom In"))
118
+ {
119
+ return;
120
+ }
121
+
122
+ var objZoom = UIMenu.Create(string.Empty, null, UIMenuIdentifier.None, UIMenuOptions.DisplayInline, new UIMenuElement[]
123
+ {
124
+ UIKeyCommand.Create("Zoom In", null, new ObjCRuntime.Selector("tfZoomIn:"), "=", UIKeyModifierFlags.Command, null),
125
+ UIKeyCommand.Create("Zoom Out", null, new ObjCRuntime.Selector("tfZoomOut:"), "-", UIKeyModifierFlags.Command, null),
126
+ UIKeyCommand.Create("Actual Size", null, new ObjCRuntime.Selector("tfZoomReset:"), "0", UIKeyModifierFlags.Command, null),
127
+ });
128
+ aBuilder.ReplaceMenu(UIMenuIdentifier.View.GetConstant()!, objView.GetMenuByReplacingChildren(new UIMenuElement[] { objZoom }.Concat(objView.Children).ToArray()));
129
+ }
130
+
131
+ private static void Apply(double aLevel)
132
+ {
133
+ Level = Clamp(aLevel);
134
+ Preferences.Default.Set(PreferenceKey, Level);
135
+ lstWebViews.RemoveAll(w => !w.TryGetTarget(out _));
136
+ foreach (var objRef in lstWebViews)
137
+ {
138
+ if (objRef.TryGetTarget(out var objWebView))
139
+ {
140
+ objWebView.PageZoom = (nfloat)Level;
141
+ }
142
+ }
143
+ }
144
+ #elif WINDOWS
145
+ private const string MessagePrefix = "tfzoom:";
146
+
147
+ /// <summary>Takes the platform web view, turns WebView2's own zoom off and installs the key and wheel handler.</summary>
148
+ /// <param name="aPlatformWebView">The <c>BlazorWebViewInitializedEventArgs.WebView</c>.</param>
149
+ public static async void Attach(object aPlatformWebView)
150
+ {
151
+ if (aPlatformWebView is not Microsoft.UI.Xaml.Controls.WebView2 objWebView || objWebView.CoreWebView2 == null)
152
+ {
153
+ return;
154
+ }
155
+
156
+ var objCore = objWebView.CoreWebView2;
157
+ Level = Clamp(Preferences.Default.Get(PreferenceKey, FirstLevel));
158
+ objCore.Settings.IsZoomControlEnabled = false;
159
+ objCore.WebMessageReceived += (sender, args) =>
160
+ {
161
+ var strMessage = args.TryGetWebMessageAsString();
162
+ if (strMessage != null && strMessage.StartsWith(MessagePrefix, StringComparison.Ordinal)
163
+ && double.TryParse(strMessage[MessagePrefix.Length..], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var dblLevel))
164
+ {
165
+ Level = Clamp(dblLevel);
166
+ Preferences.Default.Set(PreferenceKey, Level);
167
+ }
168
+ };
169
+ var strLevel = Level.ToString(System.Globalization.CultureInfo.InvariantCulture);
170
+ await objCore.AddScriptToExecuteOnDocumentCreatedAsync(
171
+ "(function(){var z=parseFloat(sessionStorage.getItem('tfzoom'))||" + strLevel + ",min=0.5,max=3;"
172
+ + "function set(v){z=Math.round(Math.min(max,Math.max(min,v))*10)/10;document.documentElement.style.zoom=z;"
173
+ + "sessionStorage.setItem('tfzoom',z);window.chrome.webview.postMessage('" + MessagePrefix + "'+z);}"
174
+ + "function apply(){document.documentElement.style.zoom=z;}"
175
+ + "if(document.documentElement){apply();}document.addEventListener('DOMContentLoaded',apply);"
176
+ + "window.addEventListener('keydown',function(e){if(!e.ctrlKey||e.altKey||e.metaKey)return;"
177
+ + "if(e.key==='='||e.key==='+'){set(z+0.1);}else if(e.key==='-'){set(z-0.1);}else if(e.key==='0'){set(1);}else return;"
178
+ + "e.preventDefault();},true);"
179
+ + "window.addEventListener('wheel',function(e){if(!e.ctrlKey)return;e.preventDefault();set(z+(e.deltaY<0?0.1:-0.1));},{passive:false});"
180
+ + "})();");
181
+ // the page already loaded before the script was added: give it the level too
182
+ await objCore.ExecuteScriptAsync("document.documentElement.style.zoom=" + strLevel + ";");
183
+ }
184
+ #else
185
+ /// <summary>No zoom on this head.</summary>
186
+ /// <param name="aPlatformWebView">Unused.</param>
187
+ public static void Attach(object aPlatformWebView)
188
+ {
189
+ }
190
+ #endif
191
+ }
@@ -69,13 +69,33 @@ Controller actions: the `a` prefix applies to `[FromRoute]`, `[FromQuery]` and `
69
69
  ## 7. Testability
70
70
 
71
71
  - Blazor: a stable `data-testid` (or element id) on every control the verifier must reach.
72
- - MAUI: a stable, unique `AutomationId` on every interactive or data-bound control, named by intent (`LoginSubmitButton`, `ClientsGrid`), set on the element whose data the gate asserts.
72
+ - MAUI: a stable, unique `AutomationId` on every interactive or data-bound control, named by intent (`LoginSubmitButton`, `ClientsGrid`), set on the element whose data the gate asserts. The tab, menu item or button that opens a screen carries `AutomationId="nav-<screen>"` (the screen's name, lower case, words joined by `-`), or a label equal to the screen's name; the Mac check reaches screens that way.
73
73
 
74
74
  ## 8. Security
75
75
 
76
76
  - No credentials in code. Parameterised queries. Validate inputs. Log security events.
77
77
 
78
- ## 9. Enforcement
78
+ ## 9. Mac Catalyst heads
79
+
80
+ Every MAUI project with a `net*-maccatalyst` target, Blazor Hybrid included:
81
+
82
+ - **Scene manifest.** `Platforms/MacCatalyst/Info.plist` carries `UIApplicationSceneManifest` with a `UIWindowSceneSessionRoleApplication` configuration named `__MAUI_DEFAULT_SCENE_CONFIGURATION__` whose `UISceneDelegateClassName` is `SceneDelegate`, and the project has `[Register("SceneDelegate")] public class SceneDelegate : MauiUISceneDelegate { }` beside the `AppDelegate`. On macOS 27 an app without it quits at its first window; the stock `dotnet new maui` template has none, so add it when the project is created.
83
+ - **Keychain and signing.** An app that asks for `keychain-access-groups` (MAUI `SecureStorage` needs it) must be signed with a development certificate and provisioning profile (`CodesignKey`, `CodesignProvisioningProfile`), or give Debug builds a second entitlements file without it and a Debug credential store that does not use the keychain. macOS refuses to start a locally signed build that asks for it. Decide which before the first Mac build.
84
+ - **Clean after an update.** After a .NET SDK or MAUI workload update, build the Mac head from clean (`--no-incremental`, or delete its `bin/` and `obj/`): an old precompiled build stops at start in `load_aot_module`.
85
+ - **Launching it.** Open the built `.app` with `open`, never its binary: macOS kills a sandboxed app started from its binary.
86
+ - **Blazor Hybrid.** On a Mac, `data-testid` and HTML `id` do not reach the test driver, so the Mac screens are checked without control names (blank window, error bar, overlap, screenshot) and the control-by-control check is the Windows head's. Keep the stock error bar (`#blazor-error-ui`, "An unhandled error has occurred"), or pass its text with `--error-text`.
87
+
88
+ ## 9b. Zoom in Blazor Hybrid heads
89
+
90
+ Every head that draws its screens in a `BlazorWebView` (Mac Catalyst and Windows) zooms with the operating system's keys: Cmd on the Mac, Ctrl on Windows, `=` in, `-` out, `0` actual size, and Ctrl+wheel on Windows. The Mac menu bar shows View › Zoom In, Zoom Out, Actual Size. The level runs from 50% to 300% in 10% steps and is kept in MAUI `Preferences` (key `tf.zoom`) across restarts.
91
+
92
+ - **Copy, do not rewrite.** `.tfcore/standards/blazor-zoom.cs.txt` is the code: copy it to the project as `BlazorZoom.cs`, set its namespace, and add the hooks its header names (one line in the page holding the `BlazorWebView`, four members on the Mac app delegate).
93
+ - **Mac Catalyst.** It sets `WKWebView.PageZoom`. From macOS 26 the main menu ignores what an app delegate's `BuildMenu` changes, so the commands are added through `UIMainMenuSystem.SharedSystem.SetBuildConfiguration`, with the Format menu off: its Font › Bigger and Smaller own Cmd+= and Cmd+-, and UIKit silently drops a menu whose shortcut repeats one.
94
+ - **Windows.** The WinUI `WebView2` control does not expose WebView2's `ZoomFactor`, and WebView2's own zoom steps unevenly from 25% to 500% and keeps nothing. So its zoom control is turned off and a script added to every page handles the keys and the wheel, and posts each level back to be saved.
95
+
96
+ `tf-maccatalyst-check.sh` warns about a Blazor Hybrid head without this; `tf-verify-zoom.sh` presses the keys on a booted head.
97
+
98
+ ## 10. Enforcement
79
99
 
80
100
  `.editorconfig` at the repository root (created at day-1) enforces: file-scoped namespaces (warning), `Async` suffix (warning), `var` for locals (warning), nullable enabled, no `_` prefix on private fields (custom naming rule, warning). `StyleCop.Analyzers` is optional and off by default.
81
101
 
@@ -90,4 +110,12 @@ grep -rE "public\s+(async\s+)?Task\s+\w+_\w+\s*\(" tests/ 2>/dev/null
90
110
  grep -rPE "private(\s+readonly)?\s+\w+\s+(?!obj)[A-Z]\w+\s*[;=]" src/ 2>/dev/null | grep -v "static\|const"
91
111
  ```
92
112
 
93
- Severity: error for file-scoped namespace and underscore field prefix; warning for nullable and the `Async` suffix; the rest informational.
113
+ A MAUI project with a Mac Catalyst target or a Blazor Hybrid head also runs, on any host (it only reads files):
114
+
115
+ ```bash
116
+ bash .tfcore/utils/tf-maccatalyst-check.sh # FAIL: scene manifest or delegate missing; WARN: keychain on a local build, or a Blazor Hybrid head without zoom (§9b)
117
+ ```
118
+
119
+ `tf-verify-boot.sh --head maccatalyst` runs it first and stops on a FAIL on macOS 27 or later.
120
+
121
+ Severity: error for file-scoped namespace and underscore field prefix; error for a Mac Catalyst FAIL; warning for nullable and the `Async` suffix; the rest informational.
@@ -0,0 +1,23 @@
1
+ # Words the owner should never have to look up, and what to say instead.
2
+ # Read by .tfcore/utils/tf-owner-text.py. One per line: word | say instead
3
+ # A line is added when the owner has had to ask what a word meant, or when the framework's own
4
+ # rules name it (_owner-language.md). Adding a word is cheap; a word the owner knows is a false alarm,
5
+ # so do not add ordinary English or the owner's own vocabulary (parity, checklist, verify, phase).
6
+ oracle | the framework's own script
7
+ denominator | the number you divide by
8
+ numerator | the number being divided
9
+ idempotent | safe to run twice
10
+ invariant | a rule that always holds
11
+ orthogonal | unrelated
12
+ heuristic | a rule of thumb
13
+ canonical | the standard one
14
+ provenance | where the number came from
15
+ cardinality | how many
16
+ semantics | meaning
17
+ deterministic | gives the same answer every time
18
+ tuple | list
19
+ enum | fixed list of values
20
+ rollup | the combined report
21
+ taint | marked unreliable
22
+ short-circuit | stops early
23
+ payload | the data sent
@@ -0,0 +1,34 @@
1
+ # _develop-mode (shared rule — *develop-end-to-end: a brief to a UAT-ready app, nobody asked)
2
+
3
+ Develop mode is on while `.tfcore/.session/develop.json` exists. `tf-develop.sh` writes it when `*develop-end-to-end` starts and removes it when the run ends. Everything in `_yolo-mode.md` applies, and on top of it:
4
+
5
+ ## What changes
6
+
7
+ - **No question to the owner, ever.** Every question a task would ask is answered here, or by the brief (`docs/{App}-Brief.md`), or decided by you. A decision the owner would normally make is written down where the owner will see it: in the Architecture's Stack Decisions, or in the document it belongs to, under a line starting `Decided in develop mode:`, with one reason. Never stop to ask, and never write `done blocked` for a question you could decide.
8
+ - **The mockups are yours.** In this command you drew them on Day 1, so a difference between a mockup and what the stack mandates (a library's own control, a required layout) is never an owner decision: redraw the mockup to match the mandated component, write `Decided in develop mode:` with the reason in the UIDesign, re-render it, and verify again. The same for any document you wrote in this run. Only a thing the owner alone can supply is a blocker.
9
+ - **No owner-review stop.** This is the one command that crosses the reviews: `*day1-greenfield` stage 1 ends without "review them, then run --stage2"; the supervisor starts stage 2 itself. Everything else about each stage is unchanged.
10
+ - **Git is the supervisor's.** `tf-develop.sh` commits and pushes after each phase from its own shell. You still never run a git write: the guard is unchanged, and you do not need to.
11
+
12
+ ## Day 1 answers
13
+
14
+ | Question in `day1-greenfield.md` | Answer in develop mode |
15
+ |---|---|
16
+ | 1. The app name | `{App}` from `develop.json` |
17
+ | 2. The concept | `docs/{App}-Brief.md`, all of it |
18
+ | 3. Hints | none, unless the brief names notes or mockups to harvest |
19
+ | 4. The stack | the stack the brief names; when it names none, the owner's answer set under `.tfcore/templates/stack-defaults/` (the one `core-config.yaml` names, or the only one there). Each question that answer set leaves open is decided as below, and every answer is written into the Architecture's Stack Decisions |
20
+ | 5. The size | the size you propose from your count; a Large brief is built as its phase 1 only, and the other phases are listed in the Build Report as not built |
21
+
22
+ Questions an answer set commonly leaves open:
23
+
24
+ - **Authentication.** The mechanism the brief names. When it names none: the app's own sign-in with local accounts, stored in the app's database; a shared identity platform needs credentials an unattended run does not have.
25
+ - **Rendering or hosting model of the UI.** The one the brief implies; otherwise the answer set's first choice.
26
+ - **Hosting and production secrets.** Not asked: they belong after UAT.
27
+
28
+ ## The database
29
+
30
+ When the Architecture names a database server and none is reachable, create it locally and continue: a container named `{app}-db` (lower case) on a free port, with a development password kept in the app's development secrets, never in a committed file. Write the connection details into the UsageGuide's setup section. Create the schema only through the migration path the Stack Decisions name, during `*build-phase` (the database guard allows it there). This overrides the answer set's "never creates its own database container" for this command only.
31
+
32
+ ## What does not change
33
+
34
+ The status gate, the smoke policy, the verifier's checks and the guard hooks. A row is `Verified` only from an executed verify. A thing only the owner can supply (a paid account, a device, a credential for a third-party service the brief requires) is still a blocker: finish everything else first, list it under PROJECT-STATUS "Known blockers", then `done blocked`.
@@ -9,6 +9,7 @@ Everything the owner reads — a terminal reply, a document, a status line, the
9
9
  - **No coined terms, no shorthand.** "the framework's own script", not "the oracle". "the number you divide by", not "the denominator". If the owner would have to look a word up, it is the wrong word.
10
10
  - **Writing a glossary means the words are wrong.** Change the words. `tf-doc-check.sh` refuses a glossary section.
11
11
  - First sentence carries the answer. Name the thing, not its id. Short sentences. Never dramatise.
12
+ - **A command's closing message is checked before the turn can end** (`tf-owner-text.sh`, run by the Stop hook on it and on any free-form document it hands over): no word from `.tfcore/standards/owner-words.txt`; each open upstream problem in a table row saying what it affects and whether it blocks or breaks anything, with the prompt that fixes it; one recorded as fixed upstream called fixed; each command still to run given as the line to paste; and the next prompt in a code block.
12
13
 
13
14
  ## 2. A question for the owner goes in a file, not in the conversation
14
15
 
@@ -7,8 +7,9 @@ Steps, in this order:
7
7
  1. `bash .tfcore/utils/tf-status-facts.sh {App} "{command}"` prints today's date, the phase line, the last verified build, the Open requirements section, the Verification log with its new row, the library feedback lines, and the next command in both harness forms. Copy them as printed.
8
8
  2. Write `PROJECT-STATUS.md` with the Write tool in the shape of `.tfcore/templates/v4custom/app-project-status-tmpl.md`: replace every section in place, add none. You write two parts yourself: "Where I am" (at most 80 words, state not story) and "Known blockers". A refused write means the content is mis-shaped: trim it, do not reword it.
9
9
  3. `bash .tfcore/utils/tf-doc-check.sh PROJECT-STATUS.md <every document this command wrote>`; day-1 and `*amend-docs` run `--app {App}` instead, because they own every document. A `FAIL` line means the phase is not closed: fix the document and re-run. A line printed as `OLD` was already there when the command started (the phase marker records them); it does not block and is repaired through `*amend-docs`. Never edit a schema to make a document pass. Documents this command did not write are reported with `--warn`, not fixed here: an older project is repaired through `*amend-docs` when it is next worked on (Schemas §7.1, decision 8).
10
- 4. `bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md`, and the same for every human document this command wrote. The checklist is never rendered.
11
- 5. `bash .tfcore/utils/tf-brd-status.sh {App}` rewrites the BRD's Development status table from the checklist and re-renders the BRD.
12
- 6. Append the run record: `.tfcore/tasks/_metrics-emit-gate.md`.
10
+ 4. When this command changed a file under `docs/`, `bash .tfcore/utils/tf-doc-tests.sh <every document it changed>`. A unit test may read a document and guard its text; when one does, this runs the unit tests. `NONE` or `PASS` goes on. `FAIL` means the phase is not closed: put the guarded text back, or, when the change is meant, name the test under "Known blockers" for `*triage-and-fix` to update, since a document command never edits code. `NOT-RUN` is reported as it is printed (Lekhak TF-016).
11
+ 5. `bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md`, and the same for every human document this command wrote. The checklist, the miss log and feedback files are never rendered.
12
+ 6. `bash .tfcore/utils/tf-brd-status.sh {App}` rewrites the BRD's Development status table from the checklist and re-renders the BRD.
13
+ 7. Append the run record: `.tfcore/tasks/_metrics-emit-gate.md`.
13
14
 
14
15
  Evidence for every line is the checklist's Requirements Status table and the files on disk, never git. Check a framework file by reading its literal path under `.tfcore/`; search tools skip that folder.