@techierathore/techieflow 1.0.3 → 1.1.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 (150) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -10
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  8. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  9. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  12. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  13. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  14. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
  15. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  16. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  17. package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
  18. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
  19. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  20. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  21. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  22. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  23. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  24. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  25. package/.opencode/plugin/techieflow.js +181 -9
  26. package/.tfcore/agents/analyst.md +5 -8
  27. package/.tfcore/agents/architect.md +1 -10
  28. package/.tfcore/agents/flow-master.md +1 -0
  29. package/.tfcore/agents/verifier.md +1 -0
  30. package/.tfcore/hooks/block-git.sh +4 -2
  31. package/.tfcore/hooks/guard-build.sh +5 -1
  32. package/.tfcore/hooks/guard-db.sh +6 -4
  33. package/.tfcore/hooks/guard-metrics.sh +13 -1
  34. package/.tfcore/hooks/guard-status-html.sh +86 -2
  35. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  36. package/.tfcore/hooks/guard-verify.sh +8 -3
  37. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  38. package/.tfcore/routing.yaml +61 -0
  39. package/.tfcore/standards/owner-words.txt +23 -0
  40. package/.tfcore/tasks/_owner-language.md +27 -0
  41. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  42. package/.tfcore/tasks/_status-update-gate.md +4 -3
  43. package/.tfcore/tasks/amend-docs.md +6 -5
  44. package/.tfcore/tasks/build-phase.md +1 -1
  45. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  46. package/.tfcore/tasks/create-doc.md +4 -0
  47. package/.tfcore/tasks/create-project-brief.md +22 -0
  48. package/.tfcore/tasks/day1-brownfield.md +3 -3
  49. package/.tfcore/tasks/day1-greenfield.md +3 -3
  50. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  51. package/.tfcore/tasks/fix-issues.md +2 -2
  52. package/.tfcore/tasks/generate-html.md +7 -3
  53. package/.tfcore/tasks/handoff-phase.md +20 -18
  54. package/.tfcore/tasks/metrics-report.md +2 -0
  55. package/.tfcore/tasks/mockups.md +1 -1
  56. package/.tfcore/tasks/productguide.md +2 -2
  57. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  58. package/.tfcore/tasks/triage-issues.md +2 -2
  59. package/.tfcore/tasks/verify-phase.md +1 -1
  60. package/.tfcore/telemetry/SCHEMA.md +61 -1
  61. package/.tfcore/telemetry/install-metrics.sh +3 -2
  62. package/.tfcore/telemetry/tf-metrics.sh +435 -21
  63. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  64. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  65. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  66. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  67. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  68. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  69. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  70. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  71. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  72. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  73. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  74. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  75. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  76. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  77. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  78. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  79. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  80. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  81. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  82. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  83. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  84. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  90. package/.tfcore/utils/tf-assets.sh +97 -12
  91. package/.tfcore/utils/tf-brd-status.py +14 -5
  92. package/.tfcore/utils/tf-build-list.py +99 -11
  93. package/.tfcore/utils/tf-build-list.sh +5 -3
  94. package/.tfcore/utils/tf-build.sh +88 -15
  95. package/.tfcore/utils/tf-ci-repro.py +685 -0
  96. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  97. package/.tfcore/utils/tf-devguide-list.py +47 -22
  98. package/.tfcore/utils/tf-doc-check.py +242 -10
  99. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  100. package/.tfcore/utils/tf-emit.sh +413 -19
  101. package/.tfcore/utils/tf-feedback.sh +13 -0
  102. package/.tfcore/utils/tf-fix-close.sh +113 -31
  103. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  104. package/.tfcore/utils/tf-goal.sh +105 -18
  105. package/.tfcore/utils/tf-harness.sh +9 -4
  106. package/.tfcore/utils/tf-lock.sh +29 -0
  107. package/.tfcore/utils/tf-log-miss.py +34 -8
  108. package/.tfcore/utils/tf-login.mjs +167 -0
  109. package/.tfcore/utils/tf-misses-md.py +24 -21
  110. package/.tfcore/utils/tf-misses-md.sh +5 -4
  111. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  112. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  113. package/.tfcore/utils/tf-model-pick.sh +475 -0
  114. package/.tfcore/utils/tf-owner-text.py +233 -0
  115. package/.tfcore/utils/tf-owner-text.sh +14 -0
  116. package/.tfcore/utils/tf-phase.sh +33 -1
  117. package/.tfcore/utils/tf-portable.sh +230 -0
  118. package/.tfcore/utils/tf-render-html.py +80 -2
  119. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  120. package/.tfcore/utils/tf-routing.sh +7 -0
  121. package/.tfcore/utils/tf-selfcheck.sh +356 -0
  122. package/.tfcore/utils/tf-split-brd.py +17 -2
  123. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  124. package/.tfcore/utils/tf-status-facts.py +133 -44
  125. package/.tfcore/utils/tf-triage.py +62 -16
  126. package/.tfcore/utils/tf-triage.sh +2 -0
  127. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  128. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  129. package/.tfcore/utils/tf-verify-env.sh +33 -4
  130. package/.tfcore/utils/tf-verify-list.py +75 -8
  131. package/.tfcore/utils/tf-verify-screens.mjs +254 -43
  132. package/.tfcore/utils/tf-verify-screens.sh +18 -4
  133. package/.tfcore/utils/tf-verify-tests.sh +272 -28
  134. package/.tfcore/utils/tf-verify-verdict.py +59 -11
  135. package/.tfcore/utils/tf_defect.py +27 -0
  136. package/.tfcore/utils/tf_feedback.py +209 -0
  137. package/.tfcore/utils/tf_roadmap.py +35 -0
  138. package/README.md +19 -5
  139. package/docs/TechieFlow-Installation.md +5 -3
  140. package/opencode.jsonc +24 -0
  141. package/package.json +1 -1
  142. package/scripts/install.mjs +4 -0
  143. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  144. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  145. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  146. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  147. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  148. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  149. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  150. package/.tfcore/utils/techieflow-doc-template.md +0 -325
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env bash
2
2
  # tf-verify-boot.sh — start the application for a verify, reach it, and stop it (Sitting 4c, 2026-09-06).
3
3
  #
4
- # bash .tfcore/utils/tf-verify-boot.sh start [--head web|windows|static] [--project <csproj>]
5
- # [--port N] [--config Release] [--static <dir>]
6
- # bash .tfcore/utils/tf-verify-boot.sh stop
7
- # bash .tfcore/utils/tf-verify-boot.sh status
4
+ # bash .tfcore/utils/tf-verify-boot.sh start [--head web|windows|static] [--project <csproj>] [--dry-run]
5
+ # [--port N] [--config Release] [--static <dir>] [--probe-path /healthz]
6
+ # [--environment Development] (web: ASPNETCORE_ENVIRONMENT)
7
+ # bash .tfcore/utils/tf-verify-boot.sh stop [--port N]
8
+ # bash .tfcore/utils/tf-verify-boot.sh status [--port N]
8
9
  #
9
10
  # Heads:
10
- # web a project on Microsoft.NET.Sdk.Web: started with `dotnet run --urls`, first through
11
- # the rungs tf-build.sh knows (dotnet, ~/.dotnet/dotnet, then Windows-side cmd.exe on
12
- # WSL), polled until it answers. Prints BOOTED mode=base url=http://localhost:PORT.
11
+ # web a project on Microsoft.NET.Sdk.Web: published by `tf-build.sh publish` (its rungs, one
12
+ # build at a time) into tests/.artifacts/verify/run-<port>/, and that copy is run on the
13
+ # side that built it, reading its settings from the project folder as `dotnet run` does.
14
+ # Builders side by side never serve each other's half-written build (TF-043). A standalone
15
+ # WebAssembly project is still started with `dotnet run`. Prints BOOTED mode=base url=….
13
16
  # windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target): started Windows-side with
14
17
  # WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222, the DevTools port
15
18
  # relayed to every interface by tf-cdp-relay.ps1, and reached from WSL over CDP. Prints
@@ -19,13 +22,19 @@
19
22
  # their rows are recorded as not verified, never as static-only passes.
20
23
  # Without --head the script picks web when a web project exists, else windows when a MAUI project
21
24
  # with a windows target exists, else NONE. --project names the project when there are several.
22
- # The state goes to tests/.artifacts/verify/boot.json; the app log to tests/.artifacts/verify/app.log.
23
- # `stop` kills what `start` started and nothing else. Never asks anyone to start anything.
25
+ # The state goes to tests/.artifacts/verify/boot-<port>.json and the app log to app-<port>.log, one pair
26
+ # per app, so builders booting side by side never empty or stop each other's (TF-034); boot.json is a
27
+ # copy of the latest start's, which the verdict reads. `stop --port N` stops that app only; a bare
28
+ # `stop` stops the one boot.json names, and refuses while two or more are running. `stop` kills what
29
+ # `start` started and nothing else. Never asks anyone to start anything.
24
30
  # Exit 0 booted / stopped · 2 NONE (reason printed; kind=build-error, host or no-driver) · 3 usage.
25
31
  set -u
26
32
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
- DIR="tests/.artifacts/verify"; STATE="$DIR/boot.json"; LOG="$DIR/app.log"
33
+ source "$HERE/tf-portable.sh" # bash-4 and GNU-only spellings go through the shim (stock Mac)
34
+ DIR="tests/.artifacts/verify"; STATE="$DIR/boot.json"; LOG="$DIR/app.log"; KEYED=""
28
35
  mkdir -p "$DIR"
36
+ # once the port is known: this app's own state file and log (TF-034)
37
+ keyed() { KEYED="$DIR/boot-$1.json"; LOG="$DIR/app-$1.log"; : > "$LOG"; }
29
38
 
30
39
  PLATFORM="linux"
31
40
  U="$(uname -s 2>/dev/null || echo windows)"
@@ -39,29 +48,38 @@ winarg() { # a Windows path as a cmd.exe argument: bare when it has no space (a
39
48
  json_escape() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }
40
49
 
41
50
  write_state() { # head mode url pids rung project reason kind
42
- python3 - "$@" <<'PY'
43
- import json, sys, datetime
51
+ TF_BOOT_FILES="$STATE $KEYED" TF_BOOT_LOG="$LOG" python3 - "$@" <<'PY'
52
+ import json, os, sys, datetime
44
53
  head, mode, url, pids, rung, project, reason, kind = sys.argv[1:9]
45
- json.dump({"head": head, "mode": mode, "url": url, "pids": [int(p) for p in pids.split() if p.isdigit()],
46
- "rung": rung, "project": project, "reason": reason, "reason_kind": kind, "stopped": False,
47
- "platform": sys.argv[9] if len(sys.argv) > 9 else "", "log": "tests/.artifacts/verify/app.log",
48
- "started": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")},
49
- open("tests/.artifacts/verify/boot.json", "w"), indent=1)
54
+ s = {"head": head, "mode": mode, "url": url, "pids": [int(p) for p in pids.split() if p.isdigit()],
55
+ "rung": rung, "project": project, "reason": reason, "reason_kind": kind, "stopped": False,
56
+ "platform": sys.argv[9] if len(sys.argv) > 9 else "", "log": os.environ["TF_BOOT_LOG"],
57
+ "started": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
58
+ for f in os.environ["TF_BOOT_FILES"].split():
59
+ json.dump(s, open(f, "w"), indent=1)
50
60
  PY
51
61
  }
62
+ set_state() { # key value-as-JSON: one more field on this start's state files
63
+ TF_BOOT_FILES="$STATE $KEYED" python3 -c 'import json,os,sys
64
+ for p in os.environ["TF_BOOT_FILES"].split():
65
+ s=json.load(open(p)); s[sys.argv[1]]=json.loads(sys.argv[2]); json.dump(s,open(p,"w"),indent=1)' "$1" "$2"
66
+ }
52
67
 
53
68
  find_projects() { # prints csproj paths under src/, source/, the root and one level down; never tests
54
69
  find . -maxdepth 4 -name '*.csproj' -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' \
55
70
  -not -path './tests/*' -not -path '*/test/*' -not -path '*Tests/*' -not -path '*.Tests/*' 2>/dev/null | sed 's#^\./##' | sort
56
71
  }
57
72
  is_web() { grep -qE 'Microsoft\.NET\.Sdk\.Web|Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$1"; }
58
- is_maui_win() { grep -qiE '<UseMaui>\s*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-windows' "$1"; }
73
+ is_maui_win() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-windows' "$1"; }
59
74
 
60
75
  poll_http() { # url seconds pid-to-watch(optional) -> 0 when it answers
61
76
  local url="$1" secs="$2" pid="${3:-}" i=0 code
62
77
  while [[ $i -lt $secs ]]; do
63
78
  code="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$url" 2>/dev/null || true)"
64
- [[ "$code" =~ ^[23] ]] && return 0
79
+ # Any HTTP answer means the app is serving; this asks whether it is up, not whether the page is
80
+ # right. A Web API with nothing at / answers 404, and was polled for 120 s, called "not brought up"
81
+ # and stopped while its log said "Application started" (AppManager TF-004).
82
+ [[ "$code" =~ ^[1-5][0-9][0-9]$ ]] && return 0
65
83
  if [[ -n "$pid" ]] && ! kill -0 "$pid" 2>/dev/null; then return 1; fi
66
84
  sleep 2; i=$((i+2))
67
85
  done
@@ -71,13 +89,46 @@ WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload I
71
89
  CODE_ERR='error (CS|RZ|BL|XC|XLS)[0-9]{3,5}|error MSB[0-9]{4}: .*(does not exist|could not be found)|error NU1'
72
90
 
73
91
  cmd="${1:-}"; shift || true
92
+ SPORT=""; [[ "${1:-}" == "--port" && "$cmd" != "start" ]] && SPORT="${2:-}"
74
93
  case "$cmd" in
75
- status) if [[ -f "$STATE" ]]; then cat "$STATE"; else echo "NONE no boot state"; fi; exit 0 ;;
94
+ status) f="$STATE"; [[ -n "$SPORT" ]] && f="$DIR/boot-$SPORT.json"
95
+ if [[ -f "$f" ]]; then cat "$f"; else echo "NONE no boot state${SPORT:+ for port $SPORT}"; fi; exit 0 ;;
76
96
  stop)
77
- [[ -f "$STATE" ]] || { echo "STOPPED nothing was started"; exit 0; }
78
- python3 - <<'PY'
79
- import json, os, signal, subprocess, time
80
- s = json.load(open("tests/.artifacts/verify/boot.json"))
97
+ if [[ -n "$SPORT" ]]; then
98
+ [[ -f "$DIR/boot-$SPORT.json" ]] || { echo "STOPPED nothing was started on port $SPORT"; exit 0; }
99
+ else
100
+ [[ -f "$STATE" ]] || { echo "STOPPED nothing was started"; exit 0; }
101
+ # a bare stop while several apps run could stop another builder's: name the port instead
102
+ live="$(python3 - "$DIR" <<'PY'
103
+ import glob, json, os, re, sys
104
+ out = []
105
+ for f in sorted(glob.glob(os.path.join(sys.argv[1], "boot-*.json"))):
106
+ try:
107
+ s = json.load(open(f))
108
+ except Exception:
109
+ continue
110
+ alive = False
111
+ for p in s.get("pids", []):
112
+ try:
113
+ os.kill(p, 0)
114
+ alive = True
115
+ except Exception:
116
+ pass
117
+ if not s.get("stopped") and alive:
118
+ out.append(re.sub(r"^boot-(.+)[.]json$", r"\1", os.path.basename(f)))
119
+ print(" ".join(out))
120
+ PY
121
+ )"
122
+ if [[ $(wc -w <<<"$live") -ge 2 ]]; then
123
+ echo "NOT-STOPPED $(( $(wc -w <<<"$live") )) apps are running (ports ${live// /, }); stop yours with: bash .tfcore/utils/tf-verify-boot.sh stop --port <n>"; exit 2
124
+ fi
125
+ fi
126
+ python3 - "$DIR" "$SPORT" <<'PY'
127
+ import json, os, re, signal, subprocess, sys, time
128
+ d, port = sys.argv[1], sys.argv[2]
129
+ main = os.path.join(d, "boot.json")
130
+ path = os.path.join(d, f"boot-{port}.json") if port else main
131
+ s = json.load(open(path))
81
132
  for pid in s.get("pids", []):
82
133
  for sig in (signal.SIGTERM, signal.SIGKILL):
83
134
  try:
@@ -94,18 +145,64 @@ if image:
94
145
  if s.get("win_port"):
95
146
  ps = f"Get-NetTCPConnection -LocalPort {s['win_port']} -State Listen -ErrorAction SilentlyContinue | ForEach-Object {{ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }}"
96
147
  subprocess.run(["powershell.exe", "-NoProfile", "-Command", ps], capture_output=True)
148
+ # An app whose starter died is nobody's child any more, so the kills above miss it and it keeps its
149
+ # port and its files. The published copy names it: stop whatever runs from run-<port>/ (TF-043).
150
+ m = re.search(r":(\d+)$", s.get("url") or "")
151
+ 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
154
+ try:
155
+ procs = [p for p in os.listdir("/proc") if p.isdigit()]
156
+ except Exception:
157
+ procs = []
158
+ cmds = [] # (pid, command line)
159
+ for p in procs:
160
+ try:
161
+ cmd = open(f"/proc/{p}/cmdline", "rb").read().replace(b"\0", b" ").decode(errors="replace")
162
+ except Exception:
163
+ continue
164
+ cmds.append((int(p), cmd))
165
+ if not procs:
166
+ # No /proc (macOS): the same list from ps, full width so the run-<port>/ path is not cut off.
167
+ try:
168
+ out = subprocess.run(["ps", "-A", "-ww", "-o", "pid=,command="], capture_output=True).stdout.decode(errors="replace")
169
+ except Exception:
170
+ out = ""
171
+ for line in out.splitlines():
172
+ pid, _, cmd = line.strip().partition(" ")
173
+ if pid.isdigit():
174
+ cmds.append((int(pid), cmd))
175
+ for pid, cmd in cmds:
176
+ if mark in cmd and pid != os.getpid():
177
+ try:
178
+ os.kill(pid, signal.SIGKILL)
179
+ except Exception:
180
+ pass
97
181
  s["stopped"] = True
98
- json.dump(s, open("tests/.artifacts/verify/boot.json", "w"), indent=1)
182
+ json.dump(s, open(path, "w"), indent=1)
183
+ # the same app under its other name: boot.json and its own boot-<port>.json
184
+ twins = [main] if port else ([os.path.join(d, f"boot-{m.group(1)}.json")] if m else [])
185
+ for f in twins:
186
+ try:
187
+ o = json.load(open(f))
188
+ if o.get("pids") == s.get("pids"):
189
+ o["stopped"] = True
190
+ json.dump(o, open(f, "w"), indent=1)
191
+ except Exception:
192
+ pass
99
193
  print(f"STOPPED head={s.get('head')} mode={s.get('mode')} pids={s.get('pids')}")
100
194
  PY
101
195
  exit 0 ;;
102
196
  start) ;;
103
- *) sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 3 ;;
197
+ *) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 3 ;;
104
198
  esac
105
199
 
106
- HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""
200
+ HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""; PROBE="/"; DRYRUN=0; ENVNAME=""
107
201
  while [[ $# -gt 0 ]]; do
108
202
  case "$1" in
203
+ --environment) ENVNAME="${2:-}"; shift 2 ;; # the web head's ASPNETCORE_ENVIRONMENT (Lekhak TF-001)
204
+ --probe-path) PROBE="${2:-/}"; [[ "$PROBE" == /* ]] || PROBE="/$PROBE"; shift 2 ;;
205
+ --dry-run) DRYRUN=1; shift ;; # print which project and head would boot, and stop (TF-010)
109
206
  --head) HEAD="${2:-}"; shift 2 ;;
110
207
  --project) PROJECT="${2:-}"; shift 2 ;;
111
208
  --port) PORT="${2:-}"; shift 2 ;;
@@ -114,7 +211,6 @@ while [[ $# -gt 0 ]]; do
114
211
  *) echo "tf-verify-boot: unknown argument $1" >&2; exit 3 ;;
115
212
  esac
116
213
  done
117
- : > "$LOG"
118
214
 
119
215
  # ---- static ---------------------------------------------------------------------------
120
216
  if [[ "$HEAD" == "static" ]]; then
@@ -123,29 +219,58 @@ if [[ "$HEAD" == "static" ]]; then
123
219
  if curl -s -o /dev/null -m 2 "http://localhost:$PORT/" 2>/dev/null; then
124
220
  echo "NONE head=static reason=port $PORT is already in use; stop that process or pass --port"; exit 2
125
221
  fi
222
+ keyed "$PORT"
126
223
  nohup python3 -m http.server "$PORT" --directory "$STATIC" --bind 127.0.0.1 > "$LOG" 2>&1 &
127
224
  PID=$!
128
225
  if poll_http "http://localhost:$PORT/" 20 "$PID"; then
129
226
  write_state static base "http://localhost:$PORT" "$PID" "python3 http.server" "$STATIC" "" "" "$PLATFORM"
130
- echo "BOOTED head=static mode=base url=http://localhost:$PORT pid=$PID log=$LOG"; exit 0
227
+ echo "BOOTED head=static mode=base url=http://localhost:$PORT pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
131
228
  fi
132
229
  kill "$PID" 2>/dev/null; write_state static none "" "" "" "$STATIC" "http.server did not answer on $PORT" host
133
230
  echo "NONE head=static reason=http.server did not answer on $PORT (log $LOG)"; exit 2
134
231
  fi
135
232
 
136
233
  # ---- pick the project and the head ------------------------------------------------------
137
- mapfile -t ALL < <(find_projects)
234
+ tf_read_lines ALL < <(find_projects)
138
235
  WEB=(); WIN=()
139
- for p in "${ALL[@]}"; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); done
236
+ for p in ${ALL[@]+"${ALL[@]}"}; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); done
237
+ # Several web projects: the one that serves screens. The first in sorted order booted AppManager's
238
+ # external API, which has no screens, in front of its admin site (AppManager TF-010). A project
239
+ # serves screens when its own folder holds .razor or .cshtml pages, or it references a project
240
+ # built with the Razor SDK. One such project is taken and said so; several, or none, stop with the
241
+ # candidates named, because a guess between them grades the wrong app.
242
+ serves_screens() { # csproj -> 0 when it draws pages
243
+ local d; d="$(dirname "$1")"
244
+ find "$d" -maxdepth 3 \( -name '*.razor' -o -name '*.cshtml' \) -not -path '*/bin/*' -not -path '*/obj/*' -print -quit 2>/dev/null | grep -q . && return 0
245
+ local ref
246
+ while read -r ref; do
247
+ ref="$(sed 's#\\#/#g' <<<"$ref")"
248
+ [[ -f "$d/$ref" ]] && grep -qE 'Microsoft\.NET\.Sdk\.Razor' "$d/$ref" && return 0
249
+ done < <(grep -oE '<ProjectReference[^>]*Include="[^"]+"' "$1" 2>/dev/null | sed -E 's/.*Include="([^"]+)".*/\1/')
250
+ return 1
251
+ }
252
+ pick_web() { # sets PROJECT from WEB, or prints NONE and exits
253
+ if [[ ${#WEB[@]} -le 1 ]]; then PROJECT="${WEB[0]:-}"; return; fi
254
+ local ui=()
255
+ for p in "${WEB[@]}"; do serves_screens "$p" && ui+=("$p"); done
256
+ if [[ ${#ui[@]} -eq 1 ]]; then
257
+ PROJECT="${ui[0]}"
258
+ echo "tf-verify-boot: ${#WEB[@]} web projects; $PROJECT serves screens (.razor/.cshtml pages or a Razor SDK reference), the other(s) do not — booting it; --project names another"
259
+ else
260
+ write_state web none "" "" "" "" "${#WEB[@]} web projects and no single one serving screens" host "$PLATFORM"
261
+ echo "NONE head=web reason=${#WEB[@]} web projects (${WEB[*]}) and ${#ui[@]} of them serve screens${ui[*]:+ (${ui[*]})}; name one with --project"; exit 2
262
+ fi
263
+ }
140
264
  if [[ -n "$PROJECT" ]]; then
141
265
  [[ -f "$PROJECT" ]] || { echo "NONE reason=--project $PROJECT does not exist"; exit 3; }
142
266
  if [[ -z "$HEAD" ]]; then is_web "$PROJECT" && HEAD=web; [[ -z "$HEAD" ]] && is_maui_win "$PROJECT" && HEAD=windows; fi
143
267
  elif [[ -z "$HEAD" ]]; then
144
- if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web; PROJECT="${WEB[0]}"
268
+ if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web; pick_web
145
269
  elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"; fi
146
270
  else
147
- case "$HEAD" in web) PROJECT="${WEB[0]:-}" ;; windows) PROJECT="${WIN[0]:-}" ;; esac
271
+ case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-}" ;; esac
148
272
  fi
273
+ if [[ $DRYRUN -eq 1 ]]; then echo "PICK head=${HEAD:-none} project=${PROJECT:-none}"; exit 0; fi
149
274
  case "$HEAD" in
150
275
  android|ios|maccatalyst)
151
276
  write_state "$HEAD" none "" "" "" "${PROJECT:-}" "no driver for the $HEAD head ships in this framework version" no-driver "$PLATFORM"
@@ -171,6 +296,98 @@ if [[ "$HEAD" == "web" ]]; then
171
296
  echo "NONE head=web reason=port $PORT is already in use; stop that process or pass --port"; exit 2
172
297
  fi
173
298
  URL="http://localhost:$PORT"
299
+ keyed "$PORT"
300
+ # TF-043. `dotnet run` served the shared bin/ and obj/, which the next builder rewrites under a
301
+ # running app: on TfLens, 2026-09-12, four apps side by side, one sent its stylesheet as 200 with
302
+ # 0 bytes, one answered 500, and one ran a dll older than its own source. Each looked healthy and
303
+ # every measurement taken on it was wrong. So the app runs from a copy of its own, published by
304
+ # tf-build.sh, which lets one build run at a time; settings, secrets and files beside the project
305
+ # are read from the project folder, as `dotnet run` reads them.
306
+ if ! grep -q 'Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$PROJECT"; then
307
+ ROOTDIR="$PWD"; RUN="$DIR/run-$PORT"
308
+ rm -rf "$RUN"
309
+ echo "### publish $PROJECT into $RUN" >> "$LOG"
310
+ # Debug unless --config says otherwise: `dotnet publish` alone builds Release, which is not the app
311
+ # `dotnet run` ran, and a Release publish over a Debug-built tree stamped the pages with one scope
312
+ # and the stylesheet with another (TfLens cluster E, in TF-043)
313
+ pub="$(bash "$HERE/tf-build.sh" publish "$PROJECT" -- -o "$RUN" -c "${CONFIG:-Debug}" 2>&1)"; prc=$?
314
+ printf '%s\n' "$pub" >> "$LOG"
315
+ verdict="$(grep -E '^(PASS|FAIL|NOT-RUN)' <<<"$pub" | tail -1)"
316
+ if [[ $prc -eq 1 ]]; then
317
+ first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$pub" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
318
+ write_state web none "" "" "tf-build.sh publish" "$PROJECT" "build error: ${first:-$verdict}" build-error "$PLATFORM"
319
+ echo "NONE head=web kind=build-error reason=the code does not build: ${first:-$verdict} (log $LOG)"; exit 2
320
+ elif [[ $prc -ne 0 || ! -f "$RUN/$ASM.dll" ]]; then
321
+ reason="${verdict:-the publish wrote no $ASM.dll into $RUN}"
322
+ write_state web none "" "" "tf-build.sh publish" "$PROJECT" "$reason" host "$PLATFORM"
323
+ echo "NONE head=web kind=host reason=$reason (log $LOG)"; exit 2
324
+ fi
325
+ # what `dotnet run` sets from the launch profile; the environment above all, which picks the
326
+ # settings file and whether the secrets are read
327
+ tf_read_lines LSENV < <(TF_BOOT_ENV="$ENVNAME" python3 - "$PDIR/Properties/launchSettings.json" <<'PY'
328
+ import json, os, sys
329
+ try:
330
+ profiles = json.load(open(sys.argv[1], encoding="utf-8-sig")).get("profiles", {})
331
+ except Exception:
332
+ profiles = {}
333
+ env = next((dict(p.get("environmentVariables") or {}) for p in profiles.values() if p.get("commandName") == "Project"), {})
334
+ env.setdefault("ASPNETCORE_ENVIRONMENT", "Development")
335
+ if os.environ.get("ASPNETCORE_ENVIRONMENT"):
336
+ env["ASPNETCORE_ENVIRONMENT"] = os.environ["ASPNETCORE_ENVIRONMENT"]
337
+ if os.environ.get("TF_BOOT_ENV"):
338
+ env["ASPNETCORE_ENVIRONMENT"] = os.environ["TF_BOOT_ENV"]
339
+ for k, v in env.items():
340
+ print(f"{k}={v}")
341
+ PY
342
+ )
343
+ WIN=""
344
+ case "$verdict" in
345
+ *"via cmd.exe"*|*"via winrun"*|*"via powershell.exe"*)
346
+ WIN=1; WPD="$(winarg "$(wslpath -w "$PDIR")")"; WDLL="$(winarg "$(wslpath -w "$RUN/$ASM.dll")")"
347
+ WWEB=""; [[ -d "$RUN/wwwroot" ]] && WWEB="--webroot $(winarg "$(wslpath -w "$RUN/wwwroot")")"
348
+ sets=""; for kv in ${LSENV[@]+"${LSENV[@]}"}; do sets+="set $kv&& "; done
349
+ nohup cmd.exe /c "cd /d $WPD && ${sets}dotnet $WDLL --urls $URL --contentRoot $WPD $WWEB" >> "$LOG" 2>&1 < /dev/null &
350
+ PID=$!; label="cmd.exe /c dotnet" ;;
351
+ *)
352
+ runner="dotnet"; [[ "$verdict" == *"via ~/.dotnet/dotnet"* ]] && runner="$HOME/.dotnet/dotnet"
353
+ # Lekhak TF-001. A copy run on the WSL side reads user-secrets from ~/.microsoft/usersecrets,
354
+ # while `dotnet user-secrets set` on Windows wrote them to %APPDATA%\Microsoft\UserSecrets:
355
+ # "Required configuration value(s) not set" on an app that starts under `dotnet run`. The
356
+ # secrets reader looks under $APPDATA first on any system, so when only the Windows store
357
+ # holds this project's secrets, the app is pointed at it.
358
+ SID="$(grep -oE '<UserSecretsId>[^<]+' "$PROJECT" 2>/dev/null | head -1 | sed 's/<UserSecretsId>//')"
359
+ if [[ -n "$SID" && ! -f "$HOME/.microsoft/usersecrets/$SID/secrets.json" ]] && [[ "$PLATFORM" == wsl || -n "${TF_WIN_APPDATA:-}" ]]; then
360
+ WAD="${TF_WIN_APPDATA:-}"
361
+ if [[ -z "$WAD" ]] && command -v cmd.exe >/dev/null 2>&1; then
362
+ WAD="$(cmd.exe /c 'echo %APPDATA%' 2>/dev/null | tr -d '\r')"; [[ -n "$WAD" ]] && WAD="$(wslpath -u "$WAD" 2>/dev/null)"
363
+ fi
364
+ if [[ -n "$WAD" && -f "$WAD/Microsoft/UserSecrets/$SID/secrets.json" ]]; then
365
+ LSENV+=("APPDATA=$WAD")
366
+ echo "tf-verify-boot: user-secrets $SID are in the Windows store only; the app reads them from $WAD/Microsoft/UserSecrets"
367
+ fi
368
+ fi
369
+ WEBR=(); [[ -d "$RUN/wwwroot" ]] && WEBR=(--webroot "$ROOTDIR/$RUN/wwwroot")
370
+ # the redirections belong to the whole background group and the group becomes the app: a group
371
+ # left waiting on its app kept this script's output open, so a caller reading it waited for ever
372
+ ( cd "$PDIR" || exit 1; exec env ${LSENV[@]+"${LSENV[@]}"} nohup "$runner" "$ROOTDIR/$RUN/$ASM.dll" --urls "$URL" --contentRoot "$PWD" ${WEBR[@]+"${WEBR[@]}"} ) \
373
+ >> "$ROOTDIR/$LOG" 2>&1 < /dev/null &
374
+ PID=$!; label="$([[ "$runner" == dotnet ]] && echo dotnet || echo '~/.dotnet/dotnet')" ;;
375
+ esac
376
+ if poll_http "$URL$PROBE" 120 "$PID"; then
377
+ write_state web base "$URL" "$PID" "$label (published copy)" "$PROJECT" "" "" "$PLATFORM"
378
+ set_state run_dir "$(json_escape "$RUN")"
379
+ [[ -n "$WIN" ]] && set_state win_port "$PORT"
380
+ echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT copy=$RUN pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
381
+ fi
382
+ pkill -P "$PID" 2>/dev/null; kill "$PID" 2>/dev/null
383
+ [[ -n "$WIN" ]] && powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
384
+ last="$(grep -vE '^[[:space:]]*$' "$LOG" | tail -3 | tr '\n' ' ' | cut -c1-240)"
385
+ # say what the log says: "no rung brought it up" sent a reader to the build when the app had started
386
+ said=""; grep -q 'Now listening on' "$LOG" && said=" although its log says it is listening"
387
+ write_state web none "" "" "$label (published copy)" "$PROJECT" "the published copy did not answer on $URL$PROBE within 120 s$said" host "$PLATFORM"
388
+ echo "NONE head=web kind=host reason=the published copy of $PROJECT did not answer on $URL$PROBE within 120 s$said; last lines: $last (log $LOG)"; exit 2
389
+ fi
390
+ # a standalone WebAssembly project has no server of its own to publish: `dotnet run` serves it
174
391
  rungs=()
175
392
  case "$PLATFORM" in
176
393
  wsl) command -v dotnet >/dev/null 2>&1 && rungs+=("dotnet"); [[ -x "$HOME/.dotnet/dotnet" ]] && rungs+=("$HOME/.dotnet/dotnet"); command -v cmd.exe >/dev/null 2>&1 && rungs+=("cmd.exe") ;;
@@ -183,23 +400,26 @@ if [[ "$HEAD" == "web" ]]; then
183
400
  case "$r" in
184
401
  cmd.exe)
185
402
  WP="$(winarg "$(wslpath -w "$PROJECT")")"
186
- nohup cmd.exe /c "dotnet run --project $WP ${CFGARGS[*]} --urls $URL" >> "$LOG" 2>&1 < /dev/null &
403
+ nohup cmd.exe /c "dotnet run --project $WP ${CFGARGS[*]:-} --urls $URL" >> "$LOG" 2>&1 < /dev/null &
187
404
  PID=$!; label="cmd.exe /c dotnet" ;;
188
405
  *)
189
- ASPNETCORE_ENVIRONMENT="${ASPNETCORE_ENVIRONMENT:-Development}" nohup "$r" run --project "$PROJECT" "${CFGARGS[@]}" --urls "$URL" >> "$LOG" 2>&1 < /dev/null &
406
+ ASPNETCORE_ENVIRONMENT="${ENVNAME:-${ASPNETCORE_ENVIRONMENT:-Development}}" nohup "$r" run --project "$PROJECT" ${CFGARGS[@]+"${CFGARGS[@]}"} --urls "$URL" >> "$LOG" 2>&1 < /dev/null &
190
407
  PID=$!; label="$r" ;;
191
408
  esac
192
409
  tried+=("$label")
193
- if poll_http "$URL/" 120 "$PID"; then
410
+ if poll_http "$URL$PROBE" 120 "$PID"; then
194
411
  WP_PORT=""; [[ "$r" == "cmd.exe" ]] && WP_PORT="$PORT"
195
412
  write_state web base "$URL" "$PID" "$label" "$PROJECT" "" "" "$PLATFORM"
196
- [[ -n "$WP_PORT" ]] && python3 -c "import json;p='$STATE';s=json.load(open(p));s['win_port']=$WP_PORT;json.dump(s,open(p,'w'),indent=1)"
197
- echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT pid=$PID log=$LOG"; exit 0
413
+ [[ -n "$WP_PORT" ]] && set_state win_port "$WP_PORT"
414
+ echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
198
415
  fi
199
416
  pkill -P "$PID" 2>/dev/null; kill "$PID" 2>/dev/null
417
+ # killing cmd.exe on this side leaves the Windows-side app running and holding the build output
418
+ # (TF-034): stop whatever listens on THIS start's port, never by program name
419
+ [[ "$r" == "cmd.exe" ]] && powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
200
420
  seg="$(awk '/^### rung: /{p=0} index($0,"### rung: '"$r"'")==1{p=1} p' "$LOG")"
201
421
  if grep -qE "$CODE_ERR" <<<"$seg" && ! grep -qE "$WRONG_RUNG" <<<"$seg"; then
202
- first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^\s*//' | cut -c1-160)"
422
+ first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
203
423
  write_state web none "" "" "$label" "$PROJECT" "build error: $first" build-error "$PLATFORM"
204
424
  echo "NONE head=web kind=build-error reason=the code does not build via $label: $first (log $LOG)"; exit 2
205
425
  fi
@@ -212,10 +432,10 @@ fi
212
432
  [[ "$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; }
213
433
  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; }
214
434
  TFM="$(grep -oE 'net[0-9.]+-windows[0-9.]*' "$PROJECT" | head -1)"
215
- CDP_PORT=9222; RELAY_PORT="${PORT:-9223}"
435
+ CDP_PORT=9222; RELAY_PORT="${PORT:-9223}"; keyed "$RELAY_PORT"
216
436
  WP="$(winarg "$(wslpath -w "$PROJECT")")"; RELAY="$(wslpath -w "$HERE/tf-cdp-relay.ps1")"
217
437
  echo "### windows head: $PROJECT -f $TFM" >> "$LOG"
218
- 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 &
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 &
219
439
  APP_PID=$!
220
440
  nohup powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$RELAY" -ListenPort "$RELAY_PORT" -TargetPort "$CDP_PORT" >> "$DIR/relay.log" 2>&1 < /dev/null &
221
441
  RELAY_PID=$!
@@ -230,7 +450,7 @@ while [[ $i -lt 300 && -z "$URL" ]]; do
230
450
  if ! kill -0 "$APP_PID" 2>/dev/null; then
231
451
  seg="$(cat "$LOG")"
232
452
  if grep -qE "$CODE_ERR" <<<"$seg"; then
233
- first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^\s*//' | cut -c1-160)"
453
+ first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
234
454
  kill "$RELAY_PID" 2>/dev/null
235
455
  write_state windows none "" "" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "build error: $first" build-error "$PLATFORM"
236
456
  echo "NONE head=windows kind=build-error reason=the code does not build: $first (log $LOG)"; exit 2
@@ -245,6 +465,6 @@ if [[ -z "$URL" ]]; then
245
465
  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
246
466
  fi
247
467
  write_state windows cdp "$URL" "$APP_PID $RELAY_PID" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "" "" "$PLATFORM"
248
- python3 -c "import json;p='$STATE';s=json.load(open(p));s['win_image']='$ASM.exe';json.dump(s,open(p,'w'),indent=1)"
468
+ set_state win_image "\"$ASM.exe\""
249
469
  echo "BOOTED head=windows mode=cdp url=$URL project=$PROJECT tfm=$TFM pids=$APP_PID,$RELAY_PID image=$ASM.exe log=$LOG"
250
470
  exit 0
@@ -39,6 +39,26 @@ v = json.load(open(os.path.join(d, "verdicts.json"), encoding="utf-8"))
39
39
  run_id = started or v.get("started") or ""
40
40
  now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
41
41
 
42
+ # A verify chained inside another command is handed that command's start as "the step-0 time", and
43
+ # its record then covered the caller's whole window, so the caller's own record was refused (TF-052).
44
+ # This run's start is its own marker's; with no verify-phase marker (step 0 skipped), when the list
45
+ # was written.
46
+ mk = {}
47
+ try:
48
+ pj = os.path.join(".tfcore", ".session", "phase.json")
49
+ if datetime.datetime.now().timestamp() - os.path.getmtime(pj) < 24 * 3600:
50
+ mk = json.load(open(pj, encoding="utf-8")) or {}
51
+ except Exception:
52
+ pass
53
+ own, caller = "", (mk.get("outer") or {}).get("cmd") or "an earlier command"
54
+ if mk.get("cmd") == "verify-phase":
55
+ own = mk.get("started") or ""
56
+ elif mk.get("started") and mk.get("started") == run_id and os.path.isfile(os.path.join(d, "list.json")):
57
+ own, caller = datetime.datetime.fromtimestamp(os.path.getmtime(os.path.join(d, "list.json")), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), mk.get("cmd")
58
+ if own and run_id and own > run_id:
59
+ print(f"tf-verify-emit: {run_id} is when {caller} started; this verify started {own} and is recorded from then")
60
+ run_id = own
61
+
42
62
  def emit(stream, rec):
43
63
  p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
44
64
  msg = (p.stdout + p.stderr).strip()
@@ -4,17 +4,30 @@
4
4
  # bash .tfcore/utils/tf-verify-env.sh [--check]
5
5
  #
6
6
  # Ensures, in the current project: package.json; the playwright and @playwright/test packages;
7
- # the Chromium browser; playwright.config.ts with its output pinned under tests/.artifacts/;
7
+ # the Chromium browser; playwright.config.ts with its output pinned under tests/.artifacts/ and its
8
+ # address read from BASE_URL, which tf-verify-tests.sh --base sets (TF-031);
8
9
  # the .gitignore lines for everything this tooling generates. --check reports and changes nothing.
9
10
  # Prints one line per item and READY at the end, or NOT-READY with the one command the owner
10
11
  # must run once (only when a browser install needs sudo and sudo is unavailable).
11
12
  # Exit 0 ready · 1 not ready · 2 node or npm missing.
12
13
  set -u
14
+ source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # tf_sed_inplace: GNU and BSD sed
13
15
  CHECK=0; [[ "${1:-}" == "--check" ]] && CHECK=1
14
16
  command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 || { echo "NOT-READY node and npm are required"; exit 2; }
15
17
  ok=1
16
18
 
17
19
  say() { echo " $*"; }
20
+ # cfg_sub FILE ALL ERE TEXT: replace the first match of ERE on each line (ALL=1), or only on the
21
+ # first line that matches (ALL=0, what GNU sed's 0,/re/ address did), with TEXT; \n in TEXT is a
22
+ # newline. awk, so it reads the same on GNU and BSD systems.
23
+ cfg_sub() {
24
+ local tmp
25
+ tmp="$(mktemp "$1.XXXXXX")" || return 1
26
+ awk -v all="$2" -v re="$3" -v rep="$4" '
27
+ (all == 1 || !done) && match($0, re) { $0 = substr($0, 1, RSTART - 1) rep substr($0, RSTART + RLENGTH); done = 1 }
28
+ { print }' "$1" > "$tmp" && cat "$tmp" > "$1"
29
+ rm -f "$tmp"
30
+ }
18
31
 
19
32
  # package.json
20
33
  if [[ ! -f package.json ]]; then
@@ -55,7 +68,7 @@ export default defineConfig({
55
68
  testDir: '"'"'./tests/verify'"'"',
56
69
  outputDir: '"'"'./tests/.artifacts/test-results'"'"',
57
70
  reporter: '"'"'line'"'"',
58
- use: { headless: true, screenshot: '"'"'only-on-failure'"'"', trace: '"'"'retain-on-failure'"'"' },
71
+ use: { baseURL: process.env.BASE_URL, headless: true, screenshot: '"'"'only-on-failure'"'"', trace: '"'"'retain-on-failure'"'"' },
59
72
  });'
60
73
  if [[ ! -f playwright.config.ts ]]; then
61
74
  if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: missing"; ok=0; else printf '%s\n' "$CFG" > playwright.config.ts; say "playwright.config.ts: written"; fi
@@ -63,13 +76,29 @@ elif ! grep -q "tests/.artifacts" playwright.config.ts; then
63
76
  if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: outputDir not under tests/.artifacts"; ok=0
64
77
  else
65
78
  if grep -q "outputDir" playwright.config.ts; then
66
- sed -i "s#outputDir:[^,]*,#outputDir: './tests/.artifacts/test-results',#" playwright.config.ts
79
+ tf_sed_inplace "s#outputDir:[^,]*,#outputDir: './tests/.artifacts/test-results',#" playwright.config.ts
67
80
  else
68
- sed -i "s#defineConfig({#defineConfig({\n outputDir: './tests/.artifacts/test-results',#" playwright.config.ts
81
+ cfg_sub playwright.config.ts 1 "defineConfig[(][{]" "defineConfig({\n outputDir: './tests/.artifacts/test-results',"
69
82
  fi
70
83
  say "playwright.config.ts: outputDir pinned under tests/.artifacts"
71
84
  fi
72
85
  else say "playwright.config.ts: present, output pinned"; fi
86
+ # the address: tf-verify-tests.sh --base passes it as BASE_URL, which Playwright never reads by
87
+ # itself; a config that ignores it tests whatever is on its default port, an older build included
88
+ # (TfLens TF-031, 2026-09-11). A baseURL of the project's own is kept, as the fallback.
89
+ if [[ -f playwright.config.ts ]] && ! grep -q "BASE_URL" playwright.config.ts; then
90
+ if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: does not read BASE_URL, so --base is ignored"; ok=0
91
+ elif grep -qE "baseURL[\"']?[[:space:]]*:" playwright.config.ts; then
92
+ tf_sed_inplace -E "s#(baseURL[\"']?[[:space:]]*:[[:space:]]*)#\1process.env.BASE_URL || #" playwright.config.ts
93
+ say "playwright.config.ts: baseURL now reads BASE_URL first, its own address after"
94
+ elif grep -qE "use[[:space:]]*:[[:space:]]*\{" playwright.config.ts; then
95
+ cfg_sub playwright.config.ts 0 "use[[:space:]]*:[[:space:]]*[{]" "use: { baseURL: process.env.BASE_URL,"
96
+ say "playwright.config.ts: baseURL added, read from BASE_URL"
97
+ else
98
+ cfg_sub playwright.config.ts 0 "defineConfig[(][{]" "defineConfig({\n use: { baseURL: process.env.BASE_URL },"
99
+ say "playwright.config.ts: baseURL added, read from BASE_URL"
100
+ fi
101
+ fi
73
102
 
74
103
  # .gitignore
75
104
  missing=()