@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
@@ -13,15 +13,30 @@
13
13
  # side that built it, reading its settings from the project folder as `dotnet run` does.
14
14
  # Builders side by side never serve each other's half-written build (TF-043). A standalone
15
15
  # WebAssembly project is still started with `dotnet run`. Prints BOOTED mode=base url=….
16
- # windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target): started Windows-side with
16
+ # windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target + a BlazorWebView; a plain MAUI
17
+ # app is refused at once, NONE kind=no-webview, TechieRag TF-006): started Windows-side with
17
18
  # WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222, the DevTools port
18
19
  # relayed to every interface by tf-cdp-relay.ps1, and reached from WSL over CDP. Prints
19
20
  # BOOTED mode=cdp url=http://<host>:9223. Proven on MyDiary, 2026-09-06.
20
21
  # static a folder of HTML served by python3 (the framework self-test, a mockup set).
21
- # android, ios, maccatalyst: no driver ships in this framework version. NONE with that reason;
22
- # their rows are recorded as not verified, never as static-only passes.
23
- # Without --head the script picks web when a web project exists, else windows when a MAUI project
24
- # with a windows target exists, else NONE. --project names the project when there are several.
22
+ # maccatalyst a MAUI project with a net*-maccatalyst target, on a Mac only: built by tf-build.sh,
23
+ # its .app started here, and reached over Appium's mac2 driver at the address in
24
+ # core-config.yaml runtimeVerification.appium.maccatalyst.url (default
25
+ # http://localhost:4723; a local Appium that is down is started and later stopped).
26
+ # Prints BOOTED mode=appium url=<appium> bundle=<id>. An SDK that names an older Xcode
27
+ # is built again with -p:ValidateXcodeVersion=false, and the state says so. A Blazor Hybrid
28
+ # head is driven without control names (its data-testid is not readable on a Mac): the
29
+ # state says webview, and those rows say the name check was not measured.
30
+ # BOOTED only once a mac2 session has opened and the app shows a window of its own, not a
31
+ # system dialog; that window is saved as boot-<port>-window.png (Lekhak TF-026). Before the
32
+ # build it checks the two things a fresh macOS 27 / Xcode 27 machine lacks, and names the fix:
33
+ # Automation Mode allowed without a password, and a mac2 driver new enough for this Xcode.
34
+ # `stop` quits the app as Cmd-Q does, and kills it only when it has not quit in 15 s.
35
+ # android, ios: no driver ships in this framework version. NONE with that reason; their rows
36
+ # are recorded as not verified, never as static-only passes.
37
+ # Without --head the script picks web when a web project exists, else (on a Mac) maccatalyst when a
38
+ # MAUI project with that target exists, else windows when a MAUI project with a windows target
39
+ # exists, else NONE. --project names the project when there are several.
25
40
  # The state goes to tests/.artifacts/verify/boot-<port>.json and the app log to app-<port>.log, one pair
26
41
  # per app, so builders booting side by side never empty or stop each other's (TF-034); boot.json is a
27
42
  # copy of the latest start's, which the verdict reads. `stop --port N` stops that app only; a bare
@@ -71,6 +86,14 @@ find_projects() { # prints csproj paths under src/, source/, the root and one le
71
86
  }
72
87
  is_web() { grep -qE 'Microsoft\.NET\.Sdk\.Web|Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$1"; }
73
88
  is_maui_win() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-windows' "$1"; }
89
+ # A Windows head is driven over its web view's DevTools port, so only a Blazor Hybrid app has one. A
90
+ # plain MAUI app (TechieRag's samples/TechieRag.Probe) was picked as the windows head and waited the
91
+ # full 300 s for a port it can never open (TechieRag TF-006).
92
+ is_hybrid() {
93
+ grep -qE 'Microsoft\.AspNetCore\.Components\.WebView\.Maui' "$1" && return 0
94
+ grep -rqlE 'AddMauiBlazorWebView|BlazorWebView' --include='*.cs' --include='*.xaml' --exclude-dir=bin --exclude-dir=obj "$(dirname "$1")" 2>/dev/null
95
+ }
96
+ is_maui_mac() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-maccatalyst' "$1"; }
74
97
 
75
98
  poll_http() { # url seconds pid-to-watch(optional) -> 0 when it answers
76
99
  local url="$1" secs="$2" pid="${3:-}" i=0 code
@@ -129,6 +152,24 @@ d, port = sys.argv[1], sys.argv[2]
129
152
  main = os.path.join(d, "boot.json")
130
153
  path = os.path.join(d, f"boot-{port}.json") if port else main
131
154
  s = json.load(open(path))
155
+ # A Mac Catalyst head is quit the way Cmd-Q quits it. Killed, macOS offered at the next start to reopen
156
+ # the windows of a run that "quit unexpectedly", and that dialog stood in front of the first screen
157
+ # (Lekhak TF-026). It is killed below only when it has not quit within 15 s.
158
+ if s.get("bundle_id") and s.get("app_path") and not s.get("stopped"):
159
+ exe_dir = os.path.join(s["app_path"], "Contents", "MacOS") + os.sep
160
+ def mac_running():
161
+ out = subprocess.run(["ps", "-A", "-ww", "-o", "command="], capture_output=True).stdout.decode(errors="replace")
162
+ return any(exe_dir in line for line in out.splitlines())
163
+ if mac_running():
164
+ bid = s["bundle_id"].replace('"', "")
165
+ try:
166
+ subprocess.run(["osascript", "-e", f'if application id "{bid}" is running then tell application id "{bid}" to quit'], capture_output=True, timeout=30)
167
+ except Exception:
168
+ pass
169
+ for _ in range(15):
170
+ if not mac_running():
171
+ break
172
+ time.sleep(1)
132
173
  for pid in s.get("pids", []):
133
174
  for sig in (signal.SIGTERM, signal.SIGKILL):
134
175
  try:
@@ -140,8 +181,23 @@ for pid in s.get("pids", []):
140
181
  pass
141
182
  time.sleep(1)
142
183
  image = s.get("win_image") or ""
143
- if image:
144
- subprocess.run(["taskkill.exe", "/F", "/T", "/IM", image], capture_output=True)
184
+ if s.get("win_pids"):
185
+ # this boot's own processes only (Sevak TF-003: /IM stopped every copy, another agent's included)
186
+ for wp in s["win_pids"]:
187
+ subprocess.run(["taskkill.exe", "/F", "/T", "/PID", str(wp)], capture_output=True)
188
+ elif image:
189
+ # a state written before win_pids: by name, as before, unless another start of it is still up
190
+ others = []
191
+ for f in os.listdir(d):
192
+ if f.startswith("boot-") and f.endswith(".json") and os.path.join(d, f) != path:
193
+ try:
194
+ o = json.load(open(os.path.join(d, f)))
195
+ if o.get("win_image") == image and not o.get("stopped") and o.get("pids") != s.get("pids"):
196
+ others.append(f)
197
+ except Exception:
198
+ pass
199
+ if not others:
200
+ subprocess.run(["taskkill.exe", "/F", "/T", "/IM", image], capture_output=True)
145
201
  if s.get("win_port"):
146
202
  ps = f"Get-NetTCPConnection -LocalPort {s['win_port']} -State Listen -ErrorAction SilentlyContinue | ForEach-Object {{ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }}"
147
203
  subprocess.run(["powershell.exe", "-NoProfile", "-Command", ps], capture_output=True)
@@ -149,8 +205,12 @@ if s.get("win_port"):
149
205
  # port and its files. The published copy names it: stop whatever runs from run-<port>/ (TF-043).
150
206
  m = re.search(r":(\d+)$", s.get("url") or "")
151
207
  run_port = port or (m.group(1) if m else "")
152
- if run_port:
153
- mark = os.path.join(os.getcwd(), d, f"run-{run_port}") + os.sep
208
+ marks = [os.path.join(os.getcwd(), d, f"run-{run_port}") + os.sep] if run_port else []
209
+ # A Mac Catalyst head: opening an Appium session starts the app again under a new process, so the
210
+ # pid written at boot is gone and the app is not; stop whatever runs from this boot's .app.
211
+ if s.get("app_path"):
212
+ marks.append(os.path.join(s["app_path"], "Contents", "MacOS") + os.sep)
213
+ if marks:
154
214
  try:
155
215
  procs = [p for p in os.listdir("/proc") if p.isdigit()]
156
216
  except Exception:
@@ -173,7 +233,7 @@ if run_port:
173
233
  if pid.isdigit():
174
234
  cmds.append((int(pid), cmd))
175
235
  for pid, cmd in cmds:
176
- if mark in cmd and pid != os.getpid():
236
+ if any(mk in cmd for mk in marks) and pid != os.getpid():
177
237
  try:
178
238
  os.kill(pid, signal.SIGKILL)
179
239
  except Exception:
@@ -194,7 +254,7 @@ print(f"STOPPED head={s.get('head')} mode={s.get('mode')} pids={s.get('pids')}")
194
254
  PY
195
255
  exit 0 ;;
196
256
  start) ;;
197
- *) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 3 ;;
257
+ *) sed -n '2,39p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 3 ;;
198
258
  esac
199
259
 
200
260
  HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""; PROBE="/"; DRYRUN=0; ENVNAME=""
@@ -232,8 +292,14 @@ fi
232
292
 
233
293
  # ---- pick the project and the head ------------------------------------------------------
234
294
  tf_read_lines ALL < <(find_projects)
235
- WEB=(); WIN=()
236
- for p in ${ALL[@]+"${ALL[@]}"}; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); done
295
+ WEB=(); WIN=(); MAC=()
296
+ WIN_NATIVE=()
297
+ for p in ${ALL[@]+"${ALL[@]}"}; do
298
+ is_web "$p" && WEB+=("$p"); is_maui_mac "$p" && MAC+=("$p")
299
+ if is_maui_win "$p"; then if is_hybrid "$p"; then WIN+=("$p"); else WIN_NATIVE+=("$p"); fi; fi
300
+ done
301
+ # a Mac Catalyst head is picked unasked only on a Mac, the one host that runs it
302
+ [[ "$PLATFORM" == "macos" ]] || MAC=()
237
303
  # Several web projects: the one that serves screens. The first in sorted order booted AppManager's
238
304
  # external API, which has no screens, in front of its admin site (AppManager TF-010). A project
239
305
  # serves screens when its own folder holds .razor or .cshtml pages, or it references a project
@@ -263,18 +329,31 @@ pick_web() { # sets PROJECT from WEB, or prints NONE and exits
263
329
  }
264
330
  if [[ -n "$PROJECT" ]]; then
265
331
  [[ -f "$PROJECT" ]] || { echo "NONE reason=--project $PROJECT does not exist"; exit 3; }
266
- if [[ -z "$HEAD" ]]; then is_web "$PROJECT" && HEAD=web; [[ -z "$HEAD" ]] && is_maui_win "$PROJECT" && HEAD=windows; fi
332
+ if [[ -z "$HEAD" ]]; then
333
+ is_web "$PROJECT" && HEAD=web
334
+ [[ -z "$HEAD" && "$PLATFORM" == "macos" ]] && is_maui_mac "$PROJECT" && HEAD=maccatalyst
335
+ [[ -z "$HEAD" ]] && is_maui_win "$PROJECT" && HEAD=windows
336
+ fi
267
337
  elif [[ -z "$HEAD" ]]; then
268
338
  if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web; pick_web
269
- elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"; fi
339
+ elif [[ ${#MAC[@]} -gt 0 ]]; then HEAD=maccatalyst; PROJECT="${MAC[0]}"
340
+ elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"
341
+ elif [[ ${#WIN_NATIVE[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN_NATIVE[0]}"; fi
270
342
  else
271
- case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-}" ;; esac
343
+ case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-${WIN_NATIVE[0]:-}}" ;; maccatalyst) PROJECT="${MAC[0]:-}" ;; esac
344
+ fi
345
+ if [[ "$HEAD" == windows && -n "$PROJECT" ]] && ! is_hybrid "$PROJECT"; then
346
+ [[ $DRYRUN -eq 1 ]] || write_state windows none "" "" "" "$PROJECT" "a MAUI app with no BlazorWebView: no web view, not drivable over CDP" no-webview "$PLATFORM"
347
+ echo "NONE head=windows kind=no-webview reason=$PROJECT is a MAUI app with no BlazorWebView (no AddMauiBlazorWebView, no <BlazorWebView>): it has no web view, so it is not drivable over CDP; its rows are graded by their tests"; exit 2
272
348
  fi
273
349
  if [[ $DRYRUN -eq 1 ]]; then echo "PICK head=${HEAD:-none} project=${PROJECT:-none}"; exit 0; fi
274
350
  case "$HEAD" in
275
- android|ios|maccatalyst)
351
+ android|ios)
276
352
  write_state "$HEAD" none "" "" "" "${PROJECT:-}" "no driver for the $HEAD head ships in this framework version" no-driver "$PLATFORM"
277
353
  echo "NONE head=$HEAD reason=no driver for the $HEAD head ships in this framework version; its rows are recorded as not verified"; exit 2 ;;
354
+ maccatalyst)
355
+ [[ "$PLATFORM" == "macos" ]] || { write_state maccatalyst none "" "" "" "${PROJECT:-}" "a Mac Catalyst head runs only on a Mac" host "$PLATFORM"
356
+ echo "NONE head=maccatalyst kind=host reason=a Mac Catalyst head runs only on a Mac, this is $PLATFORM"; exit 2; } ;;
278
357
  web|windows) ;;
279
358
  "") write_state none none "" "" "" "" "no web project and no MAUI project with a windows target found under src/, source/ or the root" host "$PLATFORM"
280
359
  echo "NONE head=none reason=no web project (Microsoft.NET.Sdk.Web) and no MAUI project with a windows target found; name one with --project"; exit 2 ;;
@@ -285,6 +364,165 @@ PDIR="$(dirname "$PROJECT")"; PNAME="$(basename "$PROJECT" .csproj)"
285
364
  ASM="$(grep -oE '<AssemblyName>[^<]+' "$PROJECT" 2>/dev/null | head -1 | sed 's/<AssemblyName>//')"; ASM="${ASM:-$PNAME}"
286
365
  CFGARGS=(); [[ -n "$CONFIG" ]] && CFGARGS=(-c "$CONFIG")
287
366
 
367
+ # ---- maccatalyst (MAUI on this Mac, reached over Appium's mac2 driver) -----------------------
368
+ if [[ "$HEAD" == "maccatalyst" ]]; then
369
+ # A Blazor Hybrid head draws its screens in a web view, and on a Mac neither data-testid nor an
370
+ # HTML id reaches mac2 or the macOS accessibility tree (probed 2026-10-07). It is driven all the
371
+ # same, without control names (owner decision A): the state says webview, the screen check skips
372
+ # the name check, and the verdict writes it as not measured.
373
+ WEBVIEW=0; grep -q 'Microsoft\.AspNetCore\.Components\.WebView\.Maui' "$PROJECT" && WEBVIEW=1
374
+ TFM="$(grep -oE 'net[0-9.]+-maccatalyst[0-9.]*' "$PROJECT" | head -1)"
375
+ # the endpoint the app registers (an uncommented maccatalyst: entry with a url), else this Mac
376
+ AURL="$(python3 - <<'PY'
377
+ import re
378
+ try:
379
+ t = open(".tfcore/core-config.yaml", encoding="utf-8").read()
380
+ except Exception:
381
+ t = ""
382
+ m = re.search(r"(?m)^[ \t]+maccatalyst:[ \t]*(?:\{[^}\n]*url:[ \t]*([^,}\s]+)|\n[ \t]+url:[ \t]*(\S+))", t)
383
+ print((m.group(1) or m.group(2)) if m else "")
384
+ PY
385
+ )"
386
+ AURL="${AURL:-http://localhost:4723}"; AURL="${AURL%/}"
387
+ APORT="${AURL##*:}"; [[ "$APORT" =~ ^[0-9]+$ ]] || APORT=4723
388
+ keyed "$APORT"
389
+ mac_none() { # reason: a host fault found before anything was started
390
+ write_state maccatalyst none "" "" "" "$PROJECT" "$1" host "$PLATFORM"
391
+ echo "NONE head=maccatalyst kind=host reason=$1"; exit 2; }
392
+ # Lekhak TF-026: two set-up faults of a fresh macOS 27 / Xcode 27 machine, each of which stopped every
393
+ # mac2 session with an error that does not name its fix. Checked here, before a build of minutes.
394
+ # 1. mac2 turns Automation Mode on for each session; when that needs a password it never comes, and
395
+ # the session fails with "Timed out while enabling automation mode".
396
+ if command -v automationmodetool >/dev/null 2>&1 && automationmodetool 2>&1 | grep -q '^This device REQUIRES user authentication'; then
397
+ mac_none "macOS asks for a password before Automation Mode comes on, so no mac2 session can open. Run once, with an administrator password: sudo automationmodetool enable-automationmode-without-authentication"
398
+ fi
399
+ # 2. mac2 builds its WebDriverAgentMac with the Xcode on this Mac. Before 4.1.1 that build fails
400
+ # under Xcode 27 (deployment target 10.15 below its 12.0 minimum, xcodebuild exit 65). An
401
+ # `appium driver update mac2` left the driver unloadable ("Cannot find package 'appium'");
402
+ # uninstalling and installing again is what worked.
403
+ if command -v appium >/dev/null 2>&1; then
404
+ M2="$(appium driver list --installed --json 2>/dev/null | python3 -c 'import json,sys
405
+ try: print(json.load(sys.stdin).get("mac2", {}).get("version", ""))
406
+ except Exception: print("?")' 2>/dev/null)"
407
+ XMAJ="$(xcodebuild -version 2>/dev/null | sed -n 's/^Xcode \([0-9]*\).*/\1/p' | head -1)"
408
+ REINSTALL="appium driver uninstall mac2; appium driver install mac2"
409
+ if [[ -z "$M2" ]]; then
410
+ mac_none "Appium's mac2 driver is not installed. Run: appium driver install mac2"
411
+ elif [[ "$M2" != "?" && "${XMAJ:-0}" -ge 27 ]] && python3 -c 'import sys
412
+ v = tuple(int(x) for x in sys.argv[1].split("-")[0].split(".")[:3])
413
+ sys.exit(0 if v < (4, 1, 1) else 1)' "$M2"; then
414
+ mac_none "the mac2 driver $M2 cannot build its helper with Xcode $XMAJ (4.1.1 is the first that can). Run: $REINSTALL"
415
+ fi
416
+ fi
417
+ ready() { curl -s -m 3 "$AURL/status" 2>/dev/null | grep -q '"ready":true'; }
418
+ APPIUM_PID=""
419
+ if ! ready; then
420
+ # boot it yourself: a local Appium that is down is started here and stopped by `stop`
421
+ if [[ "$AURL" =~ ^http://(localhost|127\.0\.0\.1): ]] && command -v appium >/dev/null 2>&1; then
422
+ nohup appium --address 127.0.0.1 --port "$APORT" > "$DIR/appium-$APORT.log" 2>&1 < /dev/null &
423
+ APPIUM_PID=$!; i=0
424
+ while [[ $i -lt 60 ]] && ! ready; do sleep 2; i=$((i+2)); done
425
+ fi
426
+ if ! ready; then
427
+ [[ -n "$APPIUM_PID" ]] && kill "$APPIUM_PID" 2>/dev/null
428
+ write_state maccatalyst none "" "" "" "$PROJECT" "Appium did not answer at $AURL/status" host "$PLATFORM"
429
+ echo "NONE head=maccatalyst kind=host reason=Appium did not answer at $AURL/status$([[ -z "$APPIUM_PID" ]] && ! command -v appium >/dev/null 2>&1 && echo '; appium is not installed (docs/TechieFlow-Setup.md §0b step 3)')"; exit 2
430
+ fi
431
+ fi
432
+ stop_appium() { [[ -n "$APPIUM_PID" ]] && kill "$APPIUM_PID" 2>/dev/null; }
433
+ # the Mac rules first (coding-standards-dotnet.md §9): a missing scene manifest is stopped here,
434
+ # before a build that macOS 27 would end at the first window; a WARN is printed and the boot goes on
435
+ chk="$(bash "$HERE/tf-maccatalyst-check.sh" "$PROJECT" 2>&1)"; crc=$?
436
+ printf '%s\n' "$chk" >> "$LOG"; grep -E '^WARN' <<<"$chk" >&2
437
+ OSMAJOR="$(sw_vers -productVersion 2>/dev/null | cut -d. -f1)"
438
+ if [[ $crc -eq 1 && "${OSMAJOR:-0}" -ge 27 ]]; then
439
+ stop_appium; first="$(grep -m1 -E '^FAIL' <<<"$chk" | sed -E 's/^FAIL [^:]+: //')"
440
+ write_state maccatalyst none "" "" "tf-maccatalyst-check.sh" "$PROJECT" "$first" build-error "$PLATFORM"
441
+ echo "NONE head=maccatalyst kind=build-error reason=$first (bash .tfcore/utils/tf-maccatalyst-check.sh)"; exit 2
442
+ fi
443
+ [[ $crc -eq 1 ]] && grep -E '^FAIL' <<<"$chk" | sed 's/^FAIL/WARN (fatal from macOS 27)/' >&2
444
+ BARGS=(-f "$TFM" -c "${CONFIG:-Debug}")
445
+ echo "### build $PROJECT -f $TFM" >> "$LOG"
446
+ out="$(bash "$HERE/tf-build.sh" build "$PROJECT" -- "${BARGS[@]}" 2>&1)"; brc=$?
447
+ # A Mac Catalyst SDK names the one Xcode it was made for and refuses a newer one; tf-build.sh builds
448
+ # again with the check off and says so in a note line (Lekhak TF-032), which the state carries.
449
+ XNOTE="$(grep -oE 'requires Xcode [0-9.]+[.] The current version of Xcode is [0-9.]+' <<<"$(grep '^note' <<<"$out")" | head -1)"
450
+ printf '%s\n' "$out" >> "$LOG"
451
+ verdict="$(grep -E '^(PASS|FAIL|NOT-RUN)' <<<"$out" | tail -1)"
452
+ if [[ $brc -ne 0 ]]; then
453
+ stop_appium
454
+ kind=host; [[ $brc -eq 1 ]] && kind=build-error
455
+ write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "${verdict:-the build failed}" "$kind" "$PLATFORM"
456
+ echo "NONE head=maccatalyst kind=$kind reason=${verdict:-the build failed} (log $LOG)"; exit 2
457
+ fi
458
+ APP="$(ls -td "$PDIR/bin/${CONFIG:-Debug}/$TFM"/*.app "$PDIR/bin/${CONFIG:-Debug}/$TFM"/*/*.app 2>/dev/null | head -1)"
459
+ if [[ -z "$APP" ]]; then
460
+ stop_appium; write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "the build wrote no .app under $PDIR/bin/${CONFIG:-Debug}/$TFM" host "$PLATFORM"
461
+ echo "NONE head=maccatalyst kind=host reason=the build wrote no .app under $PDIR/bin/${CONFIG:-Debug}/$TFM (log $LOG)"; exit 2
462
+ fi
463
+ APP="$(cd "$APP" && pwd)"
464
+ BUNDLE="$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP/Contents/Info.plist" 2>/dev/null)"
465
+ EXE="$(/usr/libexec/PlistBuddy -c 'Print CFBundleExecutable' "$APP/Contents/Info.plist" 2>/dev/null)"
466
+ if OLD="$(pgrep -f "$APP/Contents/MacOS/$EXE" | head -1)" && [[ -n "$OLD" ]]; then
467
+ stop_appium; write_state maccatalyst none "" "" "" "$PROJECT" "another copy of $EXE is already running (pid $OLD)" host "$PLATFORM"
468
+ echo "NONE head=maccatalyst kind=host reason=another copy of $EXE is already running (pid $OLD), and mac2 would attach to either; stop it first"; exit 2
469
+ fi
470
+ MARK="$DIR/.launch-$APORT"; : > "$MARK"; ROOT_LOG="$PWD/$LOG"
471
+ # Opened the way Finder opens it: macOS kills a sandboxed app started from its binary directly
472
+ # ("Killed: 9", Lekhak BlogAdmin, 2026-10-07). Its output goes to the log through open's own options.
473
+ # -ApplePersistenceIgnoreState: never offer to reopen the windows of an earlier run (Lekhak TF-026)
474
+ OPENERR="$(open -n -o "$ROOT_LOG" --stderr "$ROOT_LOG" "$APP" --args -ApplePersistenceIgnoreState YES 2>&1)"; printf '%s\n' "$OPENERR" >> "$LOG"
475
+ APP_PID=""; i=0
476
+ while [[ $i -lt 10 && -z "$APP_PID" ]]; do sleep 1; i=$((i+1)); APP_PID="$(pgrep -f "$APP/Contents/MacOS/$EXE" | head -1)"; done
477
+ i=0; while [[ -n "$APP_PID" && $i -lt 8 ]] && kill -0 "$APP_PID" 2>/dev/null; do sleep 1; i=$((i+1)); done
478
+ if [[ -z "$APP_PID" ]] || ! kill -0 "$APP_PID" 2>/dev/null; then
479
+ stop_appium
480
+ crash="$(find "$HOME/Library/Logs/DiagnosticReports" -name "$EXE-*.ips" -newer "$MARK" 2>/dev/null | head -1)"
481
+ hint=""; grep -qs NoSceneLifecycleAdoption "$crash" && hint="; macOS stops a UIKit app that has not adopted scenes: add UIApplicationSceneManifest to Platforms/MacCatalyst/Info.plist and a SceneDelegate : MauiUISceneDelegate"
482
+ # macOS refused to start it at all (Lekhak BlogAdmin, 2026-10-07: "Launch failed", POSIX 163): a
483
+ # locally signed build that asks for keychain-access-groups, which needs a provisioning profile
484
+ if grep -q 'Launch failed' <<<"$OPENERR"; then
485
+ hint="; macOS refused to start it ($(grep -oE 'Code=[0-9]+ "[^"]*"' <<<"$OPENERR" | tail -1))"
486
+ codesign -d --entitlements :- "$APP" 2>/dev/null | grep -q keychain-access-groups && hint="$hint: the build is signed locally (ad hoc) and asks for keychain-access-groups, which needs a provisioning profile; leave that entitlement out of Debug builds or set CodesignProvisioningProfile"
487
+ fi
488
+ write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "the app quit on start${crash:+ (crash report $crash)}$hint" build-error "$PLATFORM"
489
+ echo "NONE head=maccatalyst kind=build-error reason=the app quit on start${crash:+ (crash report $crash)}$hint (log $LOG)"; exit 2
490
+ fi
491
+ write_state maccatalyst appium "$AURL" "$APP_PID $APPIUM_PID" "tf-build.sh build -f $TFM" "$PROJECT" "" "" "$PLATFORM"
492
+ set_state bundle_id "$(json_escape "$BUNDLE")"
493
+ set_state app_path "$(json_escape "$APP")"
494
+ # Lekhak TF-026: a running process and an answering Appium are not a boot. A mac2 session has to open
495
+ # and find the app's own window on view, not a system dialog, before this says BOOTED.
496
+ SHOT="$PWD/$DIR/boot-$APORT-window.png"
497
+ FIRST="$(TF_A="$AURL" TF_B="$BUNDLE" TF_P="$APP" TF_S="$SHOT" node --input-type=module -e "
498
+ import { firstScreen } from '$HERE/tf-appium.mjs';
499
+ const r = await firstScreen(process.env.TF_A, { bundleId: process.env.TF_B, appPath: process.env.TF_P, shot: process.env.TF_S });
500
+ console.log(JSON.stringify(r));" 2>&1 | tail -1)"
501
+ printf '### first screen: %s\n' "$FIRST" >> "$LOG"
502
+ fs() { python3 -c 'import json,sys
503
+ try: print(json.loads(sys.argv[1]).get(sys.argv[2]) or "")
504
+ except Exception: print("")' "$FIRST" "$1"; }
505
+ if [[ "$(fs ok)" != "True" ]]; then
506
+ why="$(fs reason)"; why="${why:-$(cut -c1-200 <<<"$FIRST")}"
507
+ fix=""
508
+ case "$why" in
509
+ *[Aa]utomation\ mode*) fix="; run once, with an administrator password: sudo automationmodetool enable-automationmode-without-authentication" ;;
510
+ *"Cannot find package"*|*"Could not find a driver"*|*xcodebuild*|*"code 65"*) fix="; the mac2 driver does not load or cannot build its helper: appium driver uninstall mac2; appium driver install mac2" ;;
511
+ esac
512
+ [[ "$(fs stage)" == session && -z "$fix" ]] && fix="; a mac2 session that cannot start is usually a permission: System Settings > Privacy & Security > Accessibility (the terminal, and WebDriverAgentRunner-Runner)"
513
+ bash "${BASH_SOURCE[0]}" stop --port "$APORT" >/dev/null 2>&1
514
+ write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "the app started but its first screen was not reached: $why$fix" host "$PLATFORM"
515
+ echo "NONE head=maccatalyst kind=host reason=the app started but its first screen was not reached: $why$fix (log $LOG)"; exit 2
516
+ fi
517
+ set_state first_screen "$(json_escape "$SHOT")"
518
+ DISMISSED="$(fs dismissed)"
519
+ [[ -n "$DISMISSED" ]] && { set_state dismissed "$(json_escape "$DISMISSED")"; echo "tf-verify-boot: $DISMISSED" >&2; }
520
+ [[ -n "$XNOTE" ]] && set_state xcode_check "$(json_escape "off: $XNOTE")"
521
+ [[ $WEBVIEW -eq 1 ]] && set_state webview true
522
+ echo "BOOTED head=maccatalyst mode=appium url=$AURL bundle=$BUNDLE project=$PROJECT tfm=$TFM$([[ $WEBVIEW -eq 1 ]] && echo ' webview=yes (control names not measured)') pid=$APP_PID${APPIUM_PID:+ appium-pid=$APPIUM_PID}${XNOTE:+ xcode-check=off ($XNOTE)} window=$DIR/boot-$APORT-window.png log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $APORT\""
523
+ exit 0
524
+ fi
525
+
288
526
  # ---- web ------------------------------------------------------------------------------
289
527
  if [[ "$HEAD" == "web" ]]; then
290
528
  if [[ -z "$PORT" ]]; then
@@ -432,10 +670,41 @@ fi
432
670
  [[ "$PLATFORM" == "wsl" || "$PLATFORM" == "windows" ]] || { write_state windows none "" "" "" "$PROJECT" "a Windows head runs only from WSL or Windows" host "$PLATFORM"; echo "NONE head=windows reason=a Windows head runs only from WSL or Windows, this is $PLATFORM"; exit 2; }
433
671
  command -v cmd.exe >/dev/null 2>&1 || { write_state windows none "" "" "" "$PROJECT" "cmd.exe not reachable" host "$PLATFORM"; echo "NONE head=windows reason=cmd.exe is not reachable from this shell"; exit 2; }
434
672
  TFM="$(grep -oE 'net[0-9.]+-windows[0-9.]*' "$PROJECT" | head -1)"
435
- CDP_PORT=9222; RELAY_PORT="${PORT:-9223}"; keyed "$RELAY_PORT"
673
+ RELAY_PORT="${PORT:-9223}"; keyed "$RELAY_PORT"
674
+ # Each boot drives its own embedded browser (Sevak TF-003). The DevTools port was 9222 for every
675
+ # head, so with two up the relay on 9223 listed pages of both apps and a smoke drove the other
676
+ # agent's window. The port now follows the relay port (9223 keeps 9222). A second head while one is
677
+ # up also gets its own WebView2 data folder: two copies of one app would otherwise share one browser
678
+ # process, whose single DevTools port lists both.
679
+ if [[ "$RELAY_PORT" == "9223" ]]; then CDP_PORT=9222; else CDP_PORT=$((RELAY_PORT + 20000)); fi
680
+ OTHERS="$(python3 - "$DIR" "$RELAY_PORT" <<'PY'
681
+ import glob, json, os, sys
682
+ n = 0
683
+ for f in glob.glob(os.path.join(sys.argv[1], "boot-*.json")):
684
+ if f.endswith(f"boot-{sys.argv[2]}.json"):
685
+ continue
686
+ try:
687
+ s = json.load(open(f))
688
+ except Exception:
689
+ continue
690
+ if s.get("head") == "windows" and s.get("mode") == "cdp" and not s.get("stopped"):
691
+ n += 1
692
+ print(n)
693
+ PY
694
+ )"
695
+ UDF_SET=""
696
+ if [[ "${OTHERS:-0}" -gt 0 ]]; then
697
+ UDF="$DIR/webview-$RELAY_PORT"; mkdir -p "$UDF"
698
+ UDF_SET="set WEBVIEW2_USER_DATA_FOLDER=$(wslpath -w "$UDF")&& "
699
+ echo "tf-verify-boot: $OTHERS other Windows head(s) up — this one gets DevTools port $CDP_PORT and its own WebView2 data folder ($UDF), so neither sees the other's pages" >&2
700
+ fi
436
701
  WP="$(winarg "$(wslpath -w "$PROJECT")")"; RELAY="$(wslpath -w "$HERE/tf-cdp-relay.ps1")"
437
- echo "### windows head: $PROJECT -f $TFM" >> "$LOG"
438
- nohup cmd.exe /c "set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=$CDP_PORT&& dotnet run --project $WP -f $TFM ${CFGARGS[*]:-}" >> "$LOG" 2>&1 < /dev/null &
702
+ echo "### windows head: $PROJECT -f $TFM (DevTools $CDP_PORT, relay $RELAY_PORT)" >> "$LOG"
703
+ # this start's own Windows processes: the app's ids before it, so the new ones are this boot's and
704
+ # stop kills them alone, never every copy of the program (taskkill /IM stopped another agent's app)
705
+ win_ids() { powershell.exe -NoProfile -Command "Get-Process -Name '$ASM' -ErrorAction SilentlyContinue | ForEach-Object { \$_.Id }" 2>/dev/null | tr -d '\r' | grep -E '^[0-9]+$' | sort; }
706
+ BEFORE_IDS="$(win_ids)"
707
+ nohup cmd.exe /c "${UDF_SET}set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=$CDP_PORT&& dotnet run --project $WP -f $TFM ${CFGARGS[*]:-}" >> "$LOG" 2>&1 < /dev/null &
439
708
  APP_PID=$!
440
709
  nohup powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$RELAY" -ListenPort "$RELAY_PORT" -TargetPort "$CDP_PORT" >> "$DIR/relay.log" 2>&1 < /dev/null &
441
710
  RELAY_PID=$!
@@ -458,13 +727,19 @@ while [[ $i -lt 300 && -z "$URL" ]]; do
458
727
  fi
459
728
  sleep 3; i=$((i+3))
460
729
  done
730
+ MINE="$(comm -13 <(printf '%s\n' "$BEFORE_IDS") <(win_ids) | tr '\n' ' ')"
461
731
  if [[ -z "$URL" ]]; then
462
732
  pkill -P "$APP_PID" 2>/dev/null; kill "$APP_PID" "$RELAY_PID" 2>/dev/null
463
- taskkill.exe /F /T /IM "$ASM.exe" >/dev/null 2>&1
733
+ for w in $MINE; do taskkill.exe /F /T /PID "$w" >/dev/null 2>&1; done
734
+ # the relay runs Windows-side and outlives its WSL parent: a second try then failed "Only one usage
735
+ # of each socket address" (TechieRag TF-006)
736
+ powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $RELAY_PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
464
737
  write_state windows none "" "" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "the DevTools port $RELAY_PORT never answered within 300 s" host "$PLATFORM"
465
738
  echo "NONE head=windows kind=host reason=the app's DevTools port never answered on ${HOSTS[*]}:$RELAY_PORT within 300 s (log $LOG, relay $DIR/relay.log)"; exit 2
466
739
  fi
467
740
  write_state windows cdp "$URL" "$APP_PID $RELAY_PID" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "" "" "$PLATFORM"
468
741
  set_state win_image "\"$ASM.exe\""
742
+ set_state win_pids "[$(tr ' ' '\n' <<<"$MINE" | grep -E '^[0-9]+$' | paste -sd, -)]"
743
+ set_state cdp_port "$CDP_PORT"
469
744
  echo "BOOTED head=windows mode=cdp url=$URL project=$PROJECT tfm=$TFM pids=$APP_PID,$RELAY_PID image=$ASM.exe log=$LOG"
470
745
  exit 0
@@ -11,6 +11,8 @@
11
11
  # the row, looked up in runs.jsonl, or left out when there is none
12
12
  # runs.jsonl one `run` record for the verify pass: cmd verify-phase, the rows touched, the build
13
13
  # result, whether YOLO was on
14
+ # Run and gate records carry app_phase, the phase whose checklist was graded (list.json). A second pass
15
+ # of one *verify --phase all starts where the first one's record ended (Sevak TF-005).
14
16
  # Rows the run could not grade (not tested, not observable, not driven) get no record. Telemetry
15
17
  # never blocks: a refused record is printed and the script still exits 0.
16
18
  set -u
@@ -59,6 +61,30 @@ if own and run_id and own > run_id:
59
61
  print(f"tf-verify-emit: {run_id} is when {caller} started; this verify started {own} and is recorded from then")
60
62
  run_id = own
61
63
 
64
+ def q(*args):
65
+ p = subprocess.run(["bash", EMIT, *args], capture_output=True, text=True)
66
+ return p.stdout.strip()
67
+
68
+ runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
69
+ runs = []
70
+ if os.path.isfile(runs_path):
71
+ for line in open(runs_path, encoding="utf-8", errors="replace"):
72
+ try:
73
+ runs.append(json.loads(line))
74
+ except Exception:
75
+ pass
76
+
77
+ # Sevak TF-005: *verify --phase all is one pass per phase inside one command, all handed the same
78
+ # step-0 time. A pass that finds a run of this app already recorded since that time starts where it
79
+ # ended, so the second pass is not refused as an overlap (SCHEMA §2.7b) and no window is counted twice.
80
+ if run_id:
81
+ after = [r.get("ended", "") for r in runs if r.get("kind") == "run" and not r.get("backfilled")
82
+ and r.get("app") in (None, app) and r.get("started", "") >= run_id and r.get("ended", "") > run_id]
83
+ if after:
84
+ print(f"tf-verify-emit: a run of {app} was recorded since {run_id}; this pass is recorded from its end, {max(after)}")
85
+ run_id = max(after)
86
+ phase = v.get("phase")
87
+
62
88
  def emit(stream, rec):
63
89
  p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
64
90
  msg = (p.stdout + p.stderr).strip()
@@ -67,24 +93,14 @@ def emit(stream, rec):
67
93
  return False
68
94
  return True
69
95
 
70
- def q(*args):
71
- p = subprocess.run(["bash", EMIT, *args], capture_output=True, text=True)
72
- return p.stdout.strip()
73
-
74
96
  # origin lookup: the last build-phase or fix-issues run that touched the row
75
- runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
76
97
  origin = {}
77
- if os.path.isfile(runs_path):
78
- for line in open(runs_path, encoding="utf-8", errors="replace"):
79
- try:
80
- r = json.loads(line)
81
- except Exception:
82
- continue
83
- if r.get("kind") != "run" or r.get("backfilled") or r.get("cmd") not in ("build-phase", "fix-issues"):
84
- continue
85
- for rid in r.get("reqs_touched") or []:
86
- if r.get("started", "") >= origin.get(rid, ("", ""))[0]:
87
- origin[rid] = (r.get("started", ""), r.get("cmd"), (r.get("subagents") or ["flow-master"])[0] if r.get("subagents") else "flow-master")
98
+ for r in runs:
99
+ if r.get("kind") != "run" or r.get("backfilled") or r.get("cmd") not in ("build-phase", "fix-issues"):
100
+ continue
101
+ for rid in r.get("reqs_touched") or []:
102
+ if r.get("started", "") >= origin.get(rid, ("", ""))[0]:
103
+ origin[rid] = (r.get("started", ""), r.get("cmd"), (r.get("subagents") or ["flow-master"])[0] if r.get("subagents") else "flow-master")
88
104
 
89
105
  gates = misses = 0
90
106
  for row in v["rows"]:
@@ -95,6 +111,8 @@ for row in v["rows"]:
95
111
  rec = {"kind": "gate", "app": app, "run_id": run_id, "req_id": rid, "req_class": row["class"],
96
112
  "attempt": int(att) if att.isdigit() else 1, "verdict": row["status"], "gate": row["gate"],
97
113
  "gates_run": row["gates_run"], "failure_class": row["failure_class"], "prior_verdict": row["prior_verdict"]}
114
+ if phase:
115
+ rec["app_phase"] = phase
98
116
  if emit("gates", rec):
99
117
  gates += 1
100
118
  if not row["gate"]:
@@ -124,7 +142,9 @@ rec = {"kind": "run", "app": app, "cmd": "verify-phase", "mode": mode, "yolo": y
124
142
  "build_result": v.get("build_result", "not-run")}
125
143
  if run_id:
126
144
  rec["started"] = run_id
145
+ if phase:
146
+ rec["app_phase"] = phase
127
147
  ok = emit("runs", rec)
128
- print(f"telemetry: {gates} gate record(s), {misses} new miss(es), run record {'written' if ok else 'not written'} (cmd verify-phase, yolo {str(yolo).lower()})")
148
+ print(f"telemetry: {gates} gate record(s), {misses} new miss(es), run record {'written' if ok else 'not written'} (cmd verify-phase, phase {phase or '?'}, yolo {str(yolo).lower()})")
129
149
  PY
130
150
  exit 0