@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.
- package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +7 -5
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +15 -7
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +25 -2
- package/.tfcore/hooks/guard-db.sh +106 -9
- package/.tfcore/hooks/guard-status-html.sh +5 -1
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +4 -4
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +1 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +6 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -0
- package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
- package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-brd-status.py +10 -1
- package/.tfcore/utils/tf-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +28 -3
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +6 -2
- package/.tfcore/utils/tf-doc-check.py +124 -21
- package/.tfcore/utils/tf-doc-tests.sh +40 -3
- package/.tfcore/utils/tf-emit.sh +40 -10
- package/.tfcore/utils/tf-fix-close.sh +13 -7
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- package/.tfcore/utils/tf-owner-text.py +27 -5
- package/.tfcore/utils/tf-phase.sh +24 -4
- package/.tfcore/utils/tf-render-html.py +19 -3
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-facts.py +90 -15
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +30 -4
- package/.tfcore/utils/tf-verify-boot.sh +296 -21
- package/.tfcore/utils/tf-verify-emit.sh +37 -17
- package/.tfcore/utils/tf-verify-list.py +219 -20
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +184 -20
- package/.tfcore/utils/tf-verify-screens.sh +17 -4
- package/.tfcore/utils/tf-verify-tests.sh +107 -17
- package/.tfcore/utils/tf-verify-verdict.py +118 -15
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_feedback.py +86 -18
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +2 -2
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +14 -3
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- 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
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 [[ ${#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|