@techierathore/techieflow 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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/_smoke-test-policy.md +7 -5
  5. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  6. package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
  9. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  10. package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
  11. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  12. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  14. package/.opencode/plugin/techieflow.js +15 -7
  15. package/.tfcore/agents/flow-master.md +2 -0
  16. package/.tfcore/agents/verifier.md +2 -2
  17. package/.tfcore/core-config.yaml +2 -1
  18. package/.tfcore/hooks/block-git.sh +25 -2
  19. package/.tfcore/hooks/guard-db.sh +106 -9
  20. package/.tfcore/hooks/guard-status-html.sh +5 -1
  21. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  22. package/.tfcore/hooks/yolo-allow.sh +27 -0
  23. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  24. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  25. package/.tfcore/tasks/_develop-mode.md +34 -0
  26. package/.tfcore/tasks/_smoke-test-policy.md +7 -5
  27. package/.tfcore/tasks/_yolo-mode.md +2 -2
  28. package/.tfcore/tasks/amend-docs.md +1 -1
  29. package/.tfcore/tasks/day1-brownfield.md +2 -2
  30. package/.tfcore/tasks/day1-greenfield.md +4 -4
  31. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  32. package/.tfcore/tasks/fix-issues.md +1 -0
  33. package/.tfcore/tasks/mockups.md +1 -1
  34. package/.tfcore/tasks/triage-and-fix.md +2 -2
  35. package/.tfcore/tasks/verify-phase.md +12 -11
  36. package/.tfcore/telemetry/SCHEMA.md +6 -2
  37. package/.tfcore/telemetry/tf-metrics.sh +19 -0
  38. package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
  39. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  40. package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
  41. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  42. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  43. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
  44. package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
  45. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  46. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  47. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  48. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  49. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  50. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  51. package/.tfcore/utils/tf-appium.mjs +179 -0
  52. package/.tfcore/utils/tf-brd-status.py +10 -1
  53. package/.tfcore/utils/tf-build-list.py +16 -1
  54. package/.tfcore/utils/tf-build.sh +75 -0
  55. package/.tfcore/utils/tf-checklist-edit.py +28 -3
  56. package/.tfcore/utils/tf-day1-files.py +55 -28
  57. package/.tfcore/utils/tf-develop.sh +264 -0
  58. package/.tfcore/utils/tf-devguide-list.py +6 -2
  59. package/.tfcore/utils/tf-doc-check.py +124 -21
  60. package/.tfcore/utils/tf-doc-tests.sh +40 -3
  61. package/.tfcore/utils/tf-emit.sh +40 -10
  62. package/.tfcore/utils/tf-fix-close.sh +13 -7
  63. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  64. package/.tfcore/utils/tf-menus.mjs +401 -0
  65. package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
  66. package/.tfcore/utils/tf-mockup-parity.sh +14 -3
  67. package/.tfcore/utils/tf-owner-text.py +27 -5
  68. package/.tfcore/utils/tf-phase.sh +24 -4
  69. package/.tfcore/utils/tf-render-html.py +19 -3
  70. package/.tfcore/utils/tf-req-ids.py +36 -0
  71. package/.tfcore/utils/tf-stack-check.sh +3 -0
  72. package/.tfcore/utils/tf-status-facts.py +90 -15
  73. package/.tfcore/utils/tf-theme.mjs +189 -0
  74. package/.tfcore/utils/tf-triage.py +30 -4
  75. package/.tfcore/utils/tf-verify-boot.sh +296 -21
  76. package/.tfcore/utils/tf-verify-emit.sh +37 -17
  77. package/.tfcore/utils/tf-verify-list.py +219 -20
  78. package/.tfcore/utils/tf-verify-list.sh +3 -1
  79. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  80. package/.tfcore/utils/tf-verify-screens.mjs +184 -20
  81. package/.tfcore/utils/tf-verify-screens.sh +17 -4
  82. package/.tfcore/utils/tf-verify-tests.sh +107 -17
  83. package/.tfcore/utils/tf-verify-verdict.py +118 -15
  84. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  85. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  86. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  87. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  88. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  89. package/.tfcore/utils/tf-yolo.sh +31 -3
  90. package/.tfcore/utils/tf_feedback.py +86 -18
  91. package/.tfcore/utils/tf_stack_check.py +93 -0
  92. package/README.md +2 -2
  93. package/opencode.jsonc +6 -2
  94. package/package.json +1 -1
  95. package/scripts/install.mjs +14 -3
  96. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  98. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  99. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  100. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  101. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  102. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
@@ -15,6 +15,10 @@
15
15
  # - a migration runner: dotnet ef database update, dotnet run --project <…Db|…Migration(s)…>,
16
16
  # dbup, flyway migrate, liquibase update, alembic upgrade, prisma migrate, knex migrate,
17
17
  # rails db:migrate, sequelize db:migrate
18
+ # - code a python / node / deno / bun / ruby / perl / pwsh run executes, inline or from a script
19
+ # file, that uses a database library and holds a SQL write, unless every database path it names
20
+ # is a throw-away (tests/.artifacts/, /tmp, :memory:) (Sevak TF-002)
21
+ # A client or a SQL verb that is only mentioned (a grep pattern, a note written to a file) passes.
18
22
  # Reads (SELECT, \d, .tables, dumps to stdout) pass. YOLO does not relax this.
19
23
  #
20
24
  # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Bash"); OpenCode via
@@ -55,17 +59,110 @@ def phase():
55
59
  except Exception:
56
60
  return None
57
61
 
58
- # Words inside an echo/printf string or a comment line are not commands: an echo naming the
59
- # migration tool refused a verify run's read on 2026-09-06 (MISS-TechieFlow-20260906-20).
60
- # Strip them before matching, so only a command that runs a migrator or writes through a
61
- # client is refused.
62
+ # What counts is what RUNS, not what a command mentions (Sevak TF-002, 2026-10-06).
63
+ # - A client is a write only when it is the command word of a segment (after a start, ; & | ( or
64
+ # a backtick, past sudo / env / `exec <container>`), so a grep or a note naming a client and a
65
+ # SQL verb passes, while `<client> app.db "<write>"` does not.
66
+ # - A heredoc's body is SQL when it feeds a client, code when it feeds an interpreter, and data
67
+ # (dropped) when it feeds anything else (`cat > notes.md <<EOF`).
68
+ # - An interpreter (python, node, deno, bun, ruby, perl, pwsh) running inline code or a script
69
+ # file has that code read: a database library together with a SQL write is refused unless every
70
+ # database path it names is a throw-away one (tests/.artifacts/, /tmp, :memory:). A sub-agent's
71
+ # script deleted four owner settings from Sevak's app database because the guard saw only
72
+ # `python3 x.py`.
73
+ # Words inside an echo/printf string or a comment line are not commands (MISS-TechieFlow-20260906-20),
74
+ # unless the string is redirected into a file, which may then be run.
75
+ SEG = (r"(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?(?:env\s+(?:\S+=\S*\s+)*)?"
76
+ r"(?:[\w./\\-]*\bexec\s+(?:-\S+\s+)*\S+\s+)?")
77
+ CLIENT_AT = SEG + r"(?:\S*/)?" + CLIENTS
78
+ INTERP = r"(?:python3?|py|node|deno|bun|ruby|perl|pwsh|powershell(?:\.exe)?|tsx|ts-node)"
79
+ INTERP_AT = SEG + r"(?:\S*/)?" + INTERP + r"\b"
80
+ DB_LIBS = (r"\bsqlite3\b|better-sqlite3|\bsql\.js\b|node:sqlite|\bpsycopg|\bpymysql\b|mysql\.connector|\bmysql2\b"
81
+ r"|require\(\s*['\"]pg['\"]\s*\)|from\s+['\"]pg['\"]|\bsqlalchemy\b|microsoft\.data\.sqlite|system\.data\.sqlite"
82
+ r"|\bpymongo\b|\bmongodb\b|\baiosqlite\b|\bduckdb\b|invoke-sqlcmd")
83
+ SQL_WRITE = (r"\binsert\s+(?:or\s+\w+\s+)?into\b|\bupdate\s+[\w\"`\[\].]+\s+set\b|\bdelete\s+from\b"
84
+ r"|\bdrop\s+(?:table|index|view|database)\b|\bcreate\s+(?:table|index|view|trigger)\b|\balter\s+table\b"
85
+ r"|\btruncate\b|\breplace\s+into\b|\bexecutescript\b"
86
+ r"|\.(?:delete_many|delete_one|insert_one|insert_many|update_one|update_many|replace_one|drop)\s*\(")
87
+ DB_PATH = r"['\"]([^'\"\n]*\.(?:db|sqlite3?|mdf)|:memory:)['\"]"
88
+ SCRIPT_EXT = r"\.(?:py|js|mjs|cjs|ts|mts|rb|pl|ps1)\b"
89
+
90
+
91
+ def throwaway(path):
92
+ q = path.replace("\\", "/").lower()
93
+ return q == ":memory:" or "tests/.artifacts/" in q or q.startswith("/tmp/") or "/tmp/" in q or q.startswith("tmp/")
94
+
95
+
96
+ def code_writes(code):
97
+ """A database library and a SQL write in the same code, against a database that is not a throw-away."""
98
+ c = code.lower()
99
+ if not (re.search(DB_LIBS, c) and re.search(SQL_WRITE, c)):
100
+ return False
101
+ paths = re.findall(DB_PATH, c)
102
+ return not paths or not all(throwaway(x) for x in paths)
103
+
104
+
62
105
  low = cmd.lower()
63
106
  low = re.sub(r"(?m)^\s*#.*$", "", low)
64
- low = re.sub(r"\b(echo|printf)\s+(\"[^\"]*\"|'[^']*'|[^\n;|&]*)", r"\1", low)
65
- direct_write = (re.search(CLIENTS, low)
66
- and (re.search(WRITE_VERBS, low)
67
- or re.search(r"\s-(f|i)\s+\S+\.sql\b", low)
68
- or re.search(r"<\s*\S+\.sql\b", low)))
107
+
108
+ # heredocs first: keep the body only where it is SQL (fed to a client) or code (fed to an interpreter)
109
+ code_bits, body_sql = [], []
110
+
111
+
112
+ runs_code = bool(re.search(INTERP_AT, low))
113
+
114
+
115
+ def _heredoc(m):
116
+ head, body = m.group(1), m.group(4)
117
+ if re.search(CLIENT_AT, head):
118
+ body_sql.append(body)
119
+ elif re.search(INTERP_AT, head) or runs_code:
120
+ # fed to an interpreter, or written to a file in a command that also runs one
121
+ # (`cat > s.py <<EOF … EOF; python3 s.py`): the file does not exist when this hook runs
122
+ code_bits.append(body)
123
+ return head + "\n"
124
+
125
+
126
+ low = re.sub(r"(?ms)^([^\n]*?)<<-?\s*(['\"]?)(\w+)\2[^\n]*\n(.*?)^\s*\3\s*$", _heredoc, low)
127
+ if runs_code:
128
+ code_bits.append(low) # printf '…' > s.py && python3 s.py: the code is in the command itself
129
+ # an echo/printf string is dropped unless it is redirected into a file
130
+ low = re.sub(r"\b(echo|printf)\s+(?:\"[^\"]*\"|'[^']*'|[^\n;|&>]*)(?!\s*>)", r"\1", low)
131
+
132
+ direct_write = bool(re.search(CLIENT_AT, low)
133
+ and (re.search(WRITE_VERBS, low)
134
+ or re.search(r"\s-(f|i)\s+\S+\.sql\b", low)
135
+ or re.search(r"<\s*\S+\.sql\b", low))) or any(re.search(WRITE_VERBS, b) for b in body_sql)
136
+
137
+ # inline code (-c / -e / --eval / -Command) and script files run by an interpreter
138
+ script_write, script_name = False, ""
139
+ if re.search(INTERP_AT, low) or code_bits:
140
+ for m in re.finditer(INTERP_AT + r"[^\n;|&]*?\s(?:-c|-e|--eval|-p|-command)\s+(\"(?:[^\"\\]|\\.)*\"|'[^']*')", low):
141
+ code_bits.append(m.group(1))
142
+ roots = [data.get("cwd") or "", os.environ.get("TF_ROOT", ".")]
143
+ # `cd tools && python3 seed.py`: the script is found from the folder the command moved to
144
+ for cdir in re.findall(r"(?:^|[;&|(]\s*)cd\s+(['\"]?)([^\s'\";&|]+)\1", cmd):
145
+ roots += [os.path.join(r, cdir[1]) for r in list(roots) if r] + [cdir[1]]
146
+ for seg in re.finditer(INTERP_AT + r"([^\n;|&]*)", cmd, flags=re.I):
147
+ for tok in re.findall(r"[^\s'\"]+" + SCRIPT_EXT, seg.group(1), flags=re.I):
148
+ for cand in [tok] + [os.path.join(r, tok) for r in roots if r]:
149
+ try:
150
+ if os.path.isfile(cand) and os.path.getsize(cand) < 2_000_000:
151
+ with open(cand, encoding="utf-8", errors="replace") as fh:
152
+ if code_writes(fh.read()):
153
+ script_write, script_name = True, tok
154
+ break
155
+ except Exception:
156
+ pass
157
+ script_write = script_write or any(code_writes(b) for b in code_bits)
158
+
159
+ if script_write:
160
+ print("BLOCKED by TechieFlow policy: this runs code that writes to a database directly "
161
+ f"({script_name or 'inline code'}: a database library and an INSERT / UPDATE / DELETE / DROP / CREATE). "
162
+ "The app's database changes only through its migration path or through the app itself. For a "
163
+ "smoke or a seed, point the script at a throw-away copy whose path names tests/.artifacts/, /tmp "
164
+ "or :memory:, written literally in the script. Owner rule 2026-09-05; Sevak TF-002.", file=sys.stderr)
165
+ sys.exit(2)
69
166
  if direct_write or re.search(DROP_DB, low):
70
167
  print("BLOCKED by TechieFlow policy: a direct database write (SQL through a client, or dropping "
71
168
  "a database) is not allowed from any command. Schema and data changes go through the "
@@ -149,7 +149,11 @@ if written_this_session:
149
149
  app = os.path.basename(cl)[: -len("-Checklist.md")]
150
150
  brd = os.path.join(root, "docs", f"{app}-BRD.md")
151
151
  if os.path.isfile(brd) and os.path.getmtime(cl) > os.path.getmtime(brd):
152
- problems.append(f"docs/{app}-BRD.md is older than docs/{app}-Checklist.md. Run: bash .tfcore/utils/tf-brd-status.sh {app}")
152
+ # the stem of a phase file is "<App>-P<n>": the command takes the app and --phase n
153
+ # (Lekhak TF-034, TechieRag TF-007)
154
+ m = re.match(r"^(.+)-P(\d+)$", app)
155
+ cmd = f"{m.group(1)} --phase {m.group(2)}" if m else app
156
+ problems.append(f"docs/{app}-BRD.md is older than docs/{app}-Checklist.md. Run: bash .tfcore/utils/tf-brd-status.sh {cmd}")
153
157
  except Exception:
154
158
  pass
155
159
 
@@ -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):
@@ -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,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`.
@@ -12,23 +12,25 @@ A screen passes only if all three hold, at a desktop width and a mobile width:
12
12
 
13
13
  A failure on any of the three is a defect: write it into the row's Remarks (prefix `⚠ visual:` for 2 and 3) and do not report the row done. A smoke earns a row at most `Implemented`; only an executed verify run writes `Verified`.
14
14
 
15
- The evidence of a smoke is the file `bash .tfcore/utils/tf-verify-screens.sh --screen <name>=<route> … --base <url>` writes (`tests/.artifacts/verify/screens.json`, with a screenshot per screen and width); a row is written `Implemented` only after that run, and its Remark names the file. The application is booted with `bash .tfcore/utils/tf-verify-boot.sh start`, which also drives an embedded-browser desktop head Windows head; a head the script cannot boot leaves the row below `Implemented` with the reason.
15
+ The evidence of a smoke is the file `bash .tfcore/utils/tf-verify-screens.sh --screen <name>=<route> … --base <url>` writes (`tests/.artifacts/verify/screens.json`, with a screenshot per screen and width); a row is written `Implemented` only after that run, and its Remark names the file. The application is booted with `bash .tfcore/utils/tf-verify-boot.sh start`, which also drives a Windows head; a head the script cannot boot leaves the row below `Implemented` with the reason.
16
16
 
17
17
  ## Run it yourself
18
18
 
19
- The build ladder (`.tfcore/templates/v4custom/build-invocation-ladder.md`) says how to build, run and reach the running UI on every supported host, including native and mobile heads. Follow it. A dependent service that is down is something you start, from its own configuration, in dependency order. Ask the owner to run something only after the ladder's last rung has failed, and then still run the smoke yourself once they answer.
19
+ The build ladder (`.tfcore/templates/v4custom/build-invocation-ladder.md`) says how to build, run and reach the running UI on every supported host, including native and mobile heads. Follow it. Start a dependent service that is down, from its own configuration, in dependency order. Ask the owner to run something only after the ladder's last rung has failed, and then still run the smoke yourself once they answer.
20
20
 
21
21
  ## Test users
22
22
 
23
- Resolve credentials in this order and stop at the first that works:
23
+ Take credentials from the first that works:
24
24
 
25
25
  1. The Test users table in `docs/<App>-UsageGuide.md`.
26
- 2. An existing account in the database, read with the connection string from the configuration.
26
+ 2. An existing account in the database, read with the configured connection string.
27
27
  3. Ask the owner, showing what you would create.
28
- 4. Only after a yes, create it and add it to the Test users table.
28
+ 4. Only after a yes, create it and add it to the table.
29
29
 
30
30
  Never invent a throwaway account.
31
31
 
32
+ An AppManager app skips steps 2 to 4 (stack answer Q4a): register through its register call, passwords in git-ignored secrets, never the owner's account.
33
+
32
34
  ## Say what ran
33
35
 
34
36
  Name the exact command and where it ran: which host, which rung, focused tests or the full build, your own smoke or an executed verify. A green intermediate step is not a pass of the whole.
@@ -1,6 +1,6 @@
1
1
  # _yolo-mode (shared rule — YOLO / goal mode: run unattended to completion)
2
2
 
3
- YOLO means all permissions and all access are granted: run one command to completion and never stop to check in. It never crosses an owner review into the next phase; `*day1-greenfield` runs one stage, and stage 2 needs `--stage2`. `*build-phase` and `*verify` are in YOLO by default; every other command honours it when it is on.
3
+ YOLO means all permissions and all access are granted: run one command to completion and never stop to check in. It never crosses an owner review into the next phase; `*day1-greenfield` runs one stage, and stage 2 needs `--stage2`. The one command that does is `*develop-end-to-end`, under its own rule (`_develop-mode.md`). `*build-phase` and `*verify` are in YOLO by default; every other command honours it when it is on.
4
4
 
5
5
  ## When it is on
6
6
 
@@ -14,7 +14,7 @@ YOLO means all permissions and all access are granted: run one command to comple
14
14
 
15
15
  ## What changes
16
16
 
17
- - Deletes and `sudo` run without a prompt. Read-only git is allowed. Git writes stay refused in every mode; the owner commits.
17
+ - Nothing asks. Deletes and `sudo` run without a prompt; `tf-yolo.sh on` also makes OpenCode stop asking (from its next session) and the `yolo-allow.sh` hook approves every Claude Code call. Claude Code's own safety checks still ask unless the session was started with `tfyolo` (`tfyolo -c` keeps the conversation); say that in one line when one asks. Read-only git is allowed. Git writes stay refused in every mode; the owner commits.
18
18
  - No question, confirmation or pause: take the documented default and record it in the checklist Remarks. The one question that survives is a new test user (`_smoke-test-policy.md`): use a documented account; if none exists, create one named `{App}-yolo-tester`, record it in the UsageGuide, and continue.
19
19
  - Never end a turn with a question, a plan or "shall I". End only when the command is complete or everything left is owner-only.
20
20
  - A failed build, a dead port, a stopped service: fix it and continue. A `429` from a sub-agent or an API: wait 60 seconds, retry once, then route around it.
@@ -18,7 +18,7 @@ First: `bash .tfcore/utils/tf-phase.sh start amend-docs {App}` prints the start
18
18
  4. BRD. ADD: append a ledger item with the next id, its screen, its mockup link and its acceptance line "When <actor> <does what> on <screen>, then <observable result>"; a new screen gets a row in the Screens and flow table and a Planned row in the Development status table. MODIFY: edit the item in place, same id. REMOVE: `~~BRD-N~~ (removed {date}: reason)`, never deleted. If the additions would take a Small project past 50 requirements, raise it to Medium: `bash .tfcore/utils/tf-day1-files.sh {App} --size M` and the Size in every document header. If they would take a Medium project, or one phase, past 100 requirements or 20 screens, stop and propose a phase split: a new row in `docs/{App}-Phases.md` (written with `--size L` when absent) and a new `docs/{App}-Pn-BRD.md`, instead of growing this one.
19
19
  5. Architecture, when in scope and ARCH items exist: edit the affected sections in place, and add a Decisions log row (date, decision, why, status `planned` naming the BRD item) for every new decision or package. A reversed decision gets a new row; the old row stays.
20
20
  6. Mockups: when a screen was added or changed, run `.tfcore/tasks/mockups.md` with `--update`, so every screen in the BRD has a current mockup before the checklist changes.
21
- 7. Checklist, only if it exists. ADD: `bash .tfcore/utils/tf-split-brd.sh {App} --add-missing` appends rows for the new items; correct their class. MODIFY: set the rows derived from that item to `Needs re-verify` with a Remark saying what changed. REMOVE: set them to `N/A (removed {date})` and keep the rows. Where the mapping from an item to its rows is unclear, ask, do not guess. If the checklist does not exist yet, the next day-1 stage 2 picks everything up.
21
+ 7. Checklist, only if it exists. ADD: `bash .tfcore/utils/tf-split-brd.sh {App} --add-missing` appends rows for the new items; correct their class. MODIFY: set the rows derived from that item to `Needs re-verify` with a Remark saying what changed. REMOVE: set Status to `N/A`, `%` to `0` and the Remark to `removed {date}: <reason>`, and keep the rows; the check does not ask a removed row for an acceptance line, BRD item, mockup or detail entry. Where the mapping from an item to its rows is unclear, ask, do not guess. If the checklist does not exist yet, the next day-1 stage 2 picks everything up.
22
22
  8. If the amendment changed code-level flows, say that `*devguide {App} --update` is due after the next build.
23
23
  9. Close what this amendment fixed: `bash .tfcore/utils/tf-emit.sh --open-misses {App} --artifact-class doc` lists the open misses whose deficient artifact is a document. Close the ones this amendment resolves in one call: `bash .tfcore/utils/tf-fix-close.sh {App} --misses <id>,<id> --fix-cmd amend-docs --verdict Verified`. Where the mapping from a miss to this amendment is unclear, ask, do not guess. Without this a document miss can never close: `*fix-issues` and `*build-phase` close off checklist rows the verifier touched, which a BRD edit never is.
24
24
  10. Decide whether this amendment is a miss: should the original documents plainly have said this (a behaviour that was always required, or two sections that contradicted each other)? Then run `*log-miss {App} "<one sentence>"`. A change of mind or a feature decided today is scope arriving late, not a miss.
@@ -13,7 +13,7 @@ First, before anything else: `bash .tfcore/utils/tf-phase.sh start day1-brownfie
13
13
 
14
14
  ## Steps, in this order
15
15
 
16
- 1. `bash .tfcore/utils/tf-day1-files.sh {App} --size {S|M|L} --kind {app|library}` writes the size, the kind and the document paths into `core-config.yaml`; for Large it also writes `docs/{App}-Phases.md`, whose table you fill first: one row per phase, each within Medium, every screen in exactly one phase. Before rewriting any document that already exists, `bash .tfcore/utils/tf-day1-files.sh --archive <file> ...` moves it to `docs/OldDocs/`. Never write a `-v2` variant.
16
+ 1. `bash .tfcore/utils/tf-day1-files.sh {App} --size {S|M|L} --kind {app|library}` writes the size and the kind into `core-config.yaml` and nothing else; for Large it also writes `docs/{App}-Phases.md`, whose table you fill first: one row per phase, each within Medium, every screen in exactly one phase. Before rewriting any document that already exists, `bash .tfcore/utils/tf-day1-files.sh --archive <file> ...` moves it to `docs/OldDocs/`. Never write a `-v2` variant.
17
17
  2. `bash .tfcore/utils/tf-mockups-locate.sh` moves every mockup found elsewhere in the repository into `docs/mockups/`, the only mockup folder, and lists what is there.
18
18
  3. Architecture: `docs/{App}-Architecture.md` from `.tfcore/templates/v4custom/app-architecture-tmpl.md`, in the template's order, Status "Current". Harvest architecture material from the hint documents first, attributed; scan the code only for what they do not cover. Stack decisions come from the project files, citing the source per row; the Solution structure from the projects; the Component map from project references; the Data model from the migrations or entity classes; the Decisions log opens with "current stack as-is" and adds a `planned` row for every structural change the BRD calls for. Scan the instance fields for the dominant naming style; if none reaches about 80 percent, record the drift under Open questions. Quote every mermaid label; never use `end` as a node id.
19
19
  4. BRD, first half: `docs/{App}-BRD.md` from `app-brd-tmpl.md`: the header (App, Kind, Size, Stack answer set, Status, Date), Summary, Scope, Users and roles, and the Screens and flow table, one row per routed page found in the code. A dialog is a row under its parent screen with `on /route` in the Route column.
@@ -22,7 +22,7 @@ First, before anything else: `bash .tfcore/utils/tf-phase.sh start day1-brownfie
22
22
  Large: steps 4 to 6 run once per phase of the Phases table, phase 1 in `docs/{App}-BRD.md` and `docs/{App}-UIDesign.md`, phase n in `docs/{App}-Pn-BRD.md` (from `app-phase-brd-tmpl.md`: this phase's screens and requirements only, linking back to phase 1) and `docs/{App}-Pn-UIDesign.md` (from `app-phase-uidesign-tmpl.md`, the same way), each header carrying `Phase | n of m` and its own Size, ids running on across phases.
23
23
  7. Checklist: run `.tfcore/tasks/split-brd.md` for `{App}` (Large: with `--all-phases`, one checklist per phase). If a phased plan is among the harvested documents, split-brd migrates its statuses; the plan then moves to `docs/OldDocs/`.
24
24
  8. Coding Standards: `docs/{App}-Coding-Standards.md` from `app-coding-standards-tmpl.md`. "Standards applied" names `.tfcore/standards/coding-standards-core.md` and the stack file, plus the choices the stack file leaves open. Where the stack file leaves a naming convention open, it follows the drift scan of step 3: the dominant style if one reaches 80 percent, else the stack file's default; a drafting instruction overrides. "Project rules" holds only rules true of this project alone.
25
- 9. `bash .tfcore/utils/tf-day1-files.sh {App} --prefix {obj|none}` writes `.editorconfig`, `AGENTS.md` and `CLAUDE.md`.
25
+ 9. `bash .tfcore/utils/tf-day1-files.sh {App} --prefix {obj|none}` writes `.editorconfig`, `AGENTS.md` and `CLAUDE.md` and adds the document paths to `core-config.yaml`.
26
26
  10. `bash .tfcore/utils/tf-gitignore-audit.sh . --fix`. Read its output. Any untracking commands it prints go into your summary for the owner to run; agents never run git.
27
27
  11. Build once through `.tfcore/templates/v4custom/build-invocation-ladder.md`. A real compile error becomes one line under Known blockers; a wrong-rung error never does.
28
28
  12. UsageGuide: `docs/{App}-UsageGuide.md` from `app-usageguide-tmpl.md`. Test users: read the users table through the connection string in the configuration and list the existing accounts, one per role, marked as existing, with "ask owner" where a password is unknown; if the database is unreachable, list the intended accounts as planned. Create no user. The Execution guide from the code; one "How to test" section per screen in navigation order, naming the user, the steps, the expected result and the REQ ids.
@@ -1,12 +1,12 @@
1
1
  # day1-greenfield
2
2
 
3
- `*day1-greenfield {App}` starts a product that has no code. It runs in two stages with one owner review between them. Stage 1 produces the Architecture, the mockups and the BRD, then stops for review. `*day1-greenfield {App} --stage2` runs only after the owner has approved them and produces the checklist and the remaining documents. No mode skips the review: YOLO runs one stage to completion, never both.
3
+ `*day1-greenfield {App}` starts a product that has no code. It runs in two stages with one owner review between them. Stage 1 produces the Architecture, the mockups and the BRD, then stops for review. `*day1-greenfield {App} --stage2` runs only after the owner has approved them and produces the checklist and the remaining documents. No mode skips the review: YOLO runs one stage to completion, never both. The one exception is `*develop-end-to-end`, which runs both stages itself (`.tfcore/tasks/_develop-mode.md`).
4
4
 
5
5
  First, before anything else: `bash .tfcore/utils/tf-phase.sh start day1-greenfield {App}` prints the start time and marks the command running.
6
6
 
7
7
  ## Stage 1 — Architecture, mockups, BRD
8
8
 
9
- Ask once each, and nothing per section:
9
+ Ask once each, and nothing per section (in develop mode ask nothing: `_develop-mode.md` gives every answer):
10
10
 
11
11
  1. `{App}` if missing. PascalCase, no spaces.
12
12
  2. The concept: "Describe the app, any length: a sentence, paragraphs, bullets, half-baked notes, comparisons to other apps. You will edit the document afterwards." Read all of it. Never cap or summarise the owner's input.
@@ -16,7 +16,7 @@ Ask once each, and nothing per section:
16
16
 
17
17
  Then, in this order:
18
18
 
19
- 6. `bash .tfcore/utils/tf-day1-files.sh {App} --size {S|M|L} --kind {app|library}` writes the size, the kind and the document paths into `core-config.yaml`; for Large it also writes `docs/{App}-Phases.md`, whose table you fill first: one row per phase, each within Medium, every screen in exactly one phase. On a re-run of stage 1, first `bash .tfcore/utils/tf-day1-files.sh --archive docs/{App}-BRD.md docs/{App}-Architecture.md docs/{App}-UIDesign.md` moves the old copies to `docs/OldDocs/`. Never write a `-v2` variant; never ask merge-or-new.
19
+ 6. `bash .tfcore/utils/tf-day1-files.sh {App} --size {S|M|L} --kind {app|library}` writes the size and the kind into `core-config.yaml` and nothing else; for Large it also writes `docs/{App}-Phases.md`, whose table you fill first: one row per phase, each within Medium, every screen in exactly one phase. On a re-run of stage 1, first `bash .tfcore/utils/tf-day1-files.sh --archive docs/{App}-BRD.md docs/{App}-Architecture.md docs/{App}-UIDesign.md` moves the old copies to `docs/OldDocs/`. Never write a `-v2` variant; never ask merge-or-new.
20
20
  7. Architecture: `docs/{App}-Architecture.md` from `.tfcore/templates/v4custom/app-architecture-tmpl.md`, in the template's order. Stack decisions come from the answer set and the owner's answers, each row citing its source. Harvested notes carry into the sections attributed, never summarised away. Quote every mermaid label; never use `end` as a node id.
21
21
  8. BRD, first half: `docs/{App}-BRD.md` from `app-brd-tmpl.md`: the header (App, Kind, Size, Stack answer set, Status, Date), Summary, Scope, Users and roles, and the Screens and flow table. A dialog is a row under its parent screen with `on /route` in the Route column.
22
22
  9. Mockups: run `.tfcore/tasks/mockups.md` for `{App}`. It reads the Screens and flow table and produces `docs/{App}-UIDesign.md` and one `docs/mockups/<screen>.html` per screen. An app with no screens records "skipped — no UI".
@@ -33,7 +33,7 @@ Only with `--stage2`, and only when `docs/{App}-BRD.md` exists. Ask nothing.
33
33
  0. If the owner gave corrections at the review, apply them to the stage-1 documents first (ids never renumbered; `bash .tfcore/utils/tf-doc-check.sh --app {App}` must pass), count them, and after this run's record append a `review` record: `{"kind":"review","phase":"day1-review","reviewed_run_id":"<stage 1 started>","correction_run_id":"<this run's started>","corrections":N,"what":"<one sentence>"}` through `tf-emit.sh misses`.
34
34
  1. Checklist: run `.tfcore/tasks/split-brd.md` for `{App}` (Large: with `--all-phases`, one checklist per phase). The owner never types `*split-brd`.
35
35
  2. Coding Standards: `docs/{App}-Coding-Standards.md` from `app-coding-standards-tmpl.md`. "Standards applied" names `.tfcore/standards/coding-standards-core.md` and the stack file, plus the choices the stack file leaves open, taking that file's stated default for each. "Project rules" stays empty unless the concept demands a rule true of this project alone.
36
- 3. `bash .tfcore/utils/tf-day1-files.sh {App} --prefix {obj|none}` writes `.editorconfig`, `AGENTS.md` and `CLAUDE.md` from their templates.
36
+ 3. `bash .tfcore/utils/tf-day1-files.sh {App} --prefix {obj|none}` writes `.editorconfig`, `AGENTS.md` and `CLAUDE.md` from their templates and adds the document paths to `core-config.yaml`.
37
37
  4. UsageGuide: `docs/{App}-UsageGuide.md` from `app-usageguide-tmpl.md`. The Test users table lists the intended accounts, one per role, none created. The Execution guide is at roadmap level, marking commands that depend on projects not yet built. One "How to test" section per screen in navigation order, naming the user, the steps, the expected result and the REQ ids.
38
38
  5. `bash .tfcore/utils/tf-gitignore-audit.sh . --fix`. Read its output. Any untracking commands it prints go into your summary for the owner to run; agents never run git.
39
39
  6. Run the status gate; the run record carries `"mode":"stage2"`. The next command is `*build-phase {App}`.