acstack 0.0.0-stage → 0.47.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-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +389 -2
- package/THIRD_PARTY_NOTICES +14 -0
- package/TROUBLESHOOTING.md +225 -0
- package/agents/codex/frontend-verifier.toml +352 -0
- package/agents/codex/task-checker.toml +345 -0
- package/agents/codex/task-executor.toml +157 -0
- package/agents/codex/test-writer.toml +149 -0
- package/agents/frontend-verifier.md +351 -0
- package/agents/task-checker.md +344 -0
- package/agents/task-executor.md +156 -0
- package/agents/test-writer.md +148 -0
- package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
- package/assets/playwriter-bridge/doctor.ps1 +450 -0
- package/assets/playwriter-bridge/launcher-src.cs +131 -0
- package/assets/playwriter-bridge/opener.js +330 -0
- package/assets/playwriter-bridge/opener.ps1 +63 -0
- package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
- package/assets/playwriter-bridge/relay.ps1 +50 -0
- package/assets/playwriter-bridge/stop.ps1 +125 -0
- package/assets/playwriter-bridge/sync.ps1 +271 -0
- package/assets/playwriter-bridge/tunnel.ps1 +325 -0
- package/assets/playwriter-bridge/vm-free-port.sh +166 -0
- package/assets/terminal-workspace/client-keybindings.md +87 -0
- package/assets/terminal-workspace/config.example.yaml +329 -0
- package/assets/terminal-workspace/setup.sh +4317 -0
- package/cli/README.md +47 -0
- package/cli/bin/acstack.js +3 -0
- package/cli/dist/index.js +25984 -0
- package/cli/dist/index.js.map +7 -0
- package/cli/package.json +59 -0
- package/docs/adr/0001-ste-writing-standard.md +16 -0
- package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
- package/docs/adr/0003-handoff-ack-command.md +19 -0
- package/docs/adr/0004-dev-setup-per-host.md +13 -0
- package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
- package/docs/adr/0006-document-opener-service.md +89 -0
- package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
- package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
- package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
- package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
- package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
- package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
- package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
- package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
- package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
- package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
- package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
- package/docs/adr/index.md +17 -0
- package/docs/config-reference.md +572 -0
- package/docs/dev-setups.md +164 -0
- package/docs/migrate-0.8.md +28 -0
- package/docs/playwriter-bridge-windows.md +15 -0
- package/docs/terminal-workspace-keys.md +137 -0
- package/docs/terminal-workspace.md +1774 -0
- package/docs/workflows.md +309 -0
- package/hooks/ac-writing-style-reminder.sh +86 -0
- package/hooks/context-pressure.sh +583 -0
- package/hooks/hooks.json +50 -0
- package/hooks/session-start-handoff.sh +165 -0
- package/hooks/session-start.sh +214 -0
- package/orchestrator/AGENTS.md +532 -0
- package/output-styles/ac-writing-style.md +286 -0
- package/package.json +50 -4
- package/pi/agents/frontend-verifier.md +357 -0
- package/pi/agents/task-checker.md +348 -0
- package/pi/agents/task-executor.md +160 -0
- package/pi/agents/test-writer.md +154 -0
- package/pi/extension.ts +723 -0
- package/profiles/fullstack-python.yaml +29 -0
- package/profiles/node-typescript.yaml +23 -0
- package/profiles/python.yaml +24 -0
- package/skills/archive/SKILL.md +160 -0
- package/skills/bootstrap/SKILL.md +109 -0
- package/skills/bridge/HANDOFF.md +93 -0
- package/skills/bridge/SKILL-VM.md +292 -0
- package/skills/bridge/SKILL-WINDOWS.md +526 -0
- package/skills/bridge/SKILL.md +121 -0
- package/skills/browser-verify/SKILL.md +229 -0
- package/skills/code-review/SKILL.md +44 -0
- package/skills/commit/SKILL.md +106 -0
- package/skills/compact-and-continue/SKILL.md +241 -0
- package/skills/explore/SKILL.md +149 -0
- package/skills/grilling/SKILL.md +134 -0
- package/skills/handoff/SKILL.md +250 -0
- package/skills/handoff/launch-herdr.md +197 -0
- package/skills/handoff/launch-without-herdr.md +30 -0
- package/skills/handoff/lead-check.md +59 -0
- package/skills/handoff/outcomes.md +162 -0
- package/skills/handoff/running-agent.md +50 -0
- package/skills/handoff/watch.md +115 -0
- package/skills/host/SKILL.md +247 -0
- package/skills/host/detect-host.sh +297 -0
- package/skills/host/herdr.md +79 -0
- package/skills/host/install-herdr-block.sh +467 -0
- package/skills/host/install-orca.sh +512 -0
- package/skills/host/notes.md +97 -0
- package/skills/host/orca.md +160 -0
- package/skills/host/questions.md +163 -0
- package/skills/host/report.md +42 -0
- package/skills/implement/SKILL.md +230 -0
- package/skills/implement/decision-log.md +62 -0
- package/skills/implement/design-pass.md +32 -0
- package/skills/implement/relaxed-tdd.md +28 -0
- package/skills/map/SKILL.md +250 -0
- package/skills/map/frontend.md +227 -0
- package/skills/map/trackers/files.md +74 -0
- package/skills/map/trackers/github.md +180 -0
- package/skills/map/visuals.md +387 -0
- package/skills/new-worktree/SKILL.md +155 -0
- package/skills/new-worktree/detect-orchestrator.sh +328 -0
- package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
- package/skills/new-worktree/herdr.md +84 -0
- package/skills/new-worktree/orca.md +101 -0
- package/skills/pre-commit-review/SKILL.md +242 -0
- package/skills/pre-commit-review/auto-fix.md +19 -0
- package/skills/pre-commit-review/interactive.md +35 -0
- package/skills/quick-fix/SKILL.md +125 -0
- package/skills/record-context/SKILL.md +106 -0
- package/skills/setup/SKILL.md +250 -0
- package/skills/setup/browser.md +123 -0
- package/skills/setup/design-review.md +97 -0
- package/skills/setup/first-run.md +114 -0
- package/skills/setup/report.md +68 -0
- package/skills/setup/row-fixes.md +196 -0
- package/skills/setup/terminal-workspace.md +153 -0
- package/skills/sync/SKILL.md +171 -0
- package/skills/teardown-worktree/SKILL.md +245 -0
- package/skills/teardown-worktree/detector.md +39 -0
- package/skills/teardown-worktree/herdr.md +113 -0
- package/skills/teardown-worktree/orca.md +61 -0
- package/skills/terminal-workspace/SKILL.md +249 -0
- package/skills/terminal-workspace/components.md +36 -0
- package/skills/terminal-workspace/config-choices.md +69 -0
- package/skills/terminal-workspace/emulator-keys.md +48 -0
- package/skills/terminal-workspace/headless-clipboard.md +175 -0
- package/skills/terminal-workspace/turn-off-and-update.md +104 -0
- package/skills/test-conventions/SKILL.md +59 -0
- package/skills/visual-docs/SKILL.md +246 -0
- package/skills/writing/SKILL.md +231 -0
- package/skills/writing/examples.md +153 -0
- package/skills/writing/vocabulary.md +96 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Playwriter bridge – Linux / VM runbook
|
|
2
|
+
|
|
3
|
+
> Stop reading if you are on Windows. This file is bash-only and assumes a
|
|
4
|
+
> Linux or macOS shell. Windows agents: read
|
|
5
|
+
> [`SKILL-WINDOWS.md`](SKILL-WINDOWS.md) instead.
|
|
6
|
+
|
|
7
|
+
## What the VM side owns
|
|
8
|
+
|
|
9
|
+
- An SSH service that accepts a reverse forward from the laptop and
|
|
10
|
+
binds `127.0.0.1:19988` on this host.
|
|
11
|
+
|
|
12
|
+
- `agent-browser` (installed per the `browser-verify` skill) pointed at
|
|
13
|
+
that endpoint via `agent-browser.json`.
|
|
14
|
+
|
|
15
|
+
- `vm-free-port.sh` for clearing a stuck port 19988 after a dropped
|
|
16
|
+
session.
|
|
17
|
+
|
|
18
|
+
- An optional `sshd_config` tweak that reaps dead client sessions
|
|
19
|
+
naturally.
|
|
20
|
+
|
|
21
|
+
The VM does **not** run the relay and does **not** run Chrome. Those
|
|
22
|
+
live on the laptop. See [`SKILL-WINDOWS.md`](SKILL-WINDOWS.md) if you
|
|
23
|
+
need to understand what the other side is doing.
|
|
24
|
+
|
|
25
|
+
## Architecture (from the VM's viewpoint)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
laptop opens:
|
|
29
|
+
ssh -N -R 19988:127.0.0.1:19988 <you>@<this-vm>
|
|
30
|
+
|
|
31
|
+
sshd on this VM then binds 127.0.0.1:19988 and forwards it back to the
|
|
32
|
+
laptop's 127.0.0.1:19988, where the Playwriter relay is listening.
|
|
33
|
+
|
|
34
|
+
agent-browser on this VM then speaks CDP against
|
|
35
|
+
http://127.0.0.1:19988
|
|
36
|
+
and is driving a real Chrome on the laptop.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Setup (once per VM)
|
|
40
|
+
|
|
41
|
+
### 1. Install agent-browser
|
|
42
|
+
|
|
43
|
+
See the `browser-verify` skill for the install command.
|
|
44
|
+
|
|
45
|
+
### 2. Create `agent-browser.json` in each project root
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "cdp": "19988" }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Accept the laptop's key
|
|
52
|
+
|
|
53
|
+
Append the laptop's SSH public key to `~/.ssh/authorized_keys`.
|
|
54
|
+
|
|
55
|
+
If the Windows side asks for a handoff to fix this (they typically will
|
|
56
|
+
during initial setup), confirm by running on this VM:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ssh-keygen -l -f ~/.ssh/authorized_keys | tail
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
and pasting the tail output back to the Windows agent as the handoff
|
|
63
|
+
"Report back".
|
|
64
|
+
|
|
65
|
+
### 4. Enable server-side keepalive (required for self-heal)
|
|
66
|
+
|
|
67
|
+
When the laptop drops abruptly (sleep, network change, VPN flap), the
|
|
68
|
+
half-open SSH session leaves an `sshd` forward channel bound to port
|
|
69
|
+
19988, now orphaned. The VM only releases it once `sshd` reaps the
|
|
70
|
+
dead client, which takes `ClientAliveInterval × ClientAliveCountMax`
|
|
71
|
+
seconds. Until then the laptop's reconnect hits
|
|
72
|
+
`remote port forwarding failed`.
|
|
73
|
+
|
|
74
|
+
The Windows `tunnel.ps1` retries a stuck port forever. It waits 30 s, then
|
|
75
|
+
doubles the wait to a 120 s cap. The bridge therefore self-heals whenever
|
|
76
|
+
the reap happens.
|
|
77
|
+
|
|
78
|
+
What keepalive changes is how long that takes:
|
|
79
|
+
|
|
80
|
+
- **Keepalive disabled** (`ClientAliveInterval 0`) – the orphan never reaps
|
|
81
|
+
and the port stays wedged until someone frees it by hand. The tunnel
|
|
82
|
+
keeps retrying, and writes `tunnel-<host>[-<port>].stuck` after 8
|
|
83
|
+
consecutive stuck attempts (~11 min) so `doctor.ps1` reports it in red.
|
|
84
|
+
|
|
85
|
+
Run `vm-free-port.sh` and the next attempt binds. You restart nothing.
|
|
86
|
+
|
|
87
|
+
- **Keepalive very loose** – recovery is slow. A default Azure / cloud
|
|
88
|
+
image ships `ClientAliveInterval 120` (≈360 s reap); the runbook value
|
|
89
|
+
of 30 (≈90 s) recovers about 4× faster.
|
|
90
|
+
|
|
91
|
+
Check the *effective* value. This needs root – the drop-in files under
|
|
92
|
+
`sshd_config.d/` are not world-readable, so a non-root `sshd -T` prints
|
|
93
|
+
nothing useful:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
sudo sshd -T | grep -E '^(clientaliveinterval|clientalivecountmax)'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Want:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
clientaliveinterval 30
|
|
103
|
+
clientalivecountmax 3
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Do not just append to `/etc/ssh/sshd_config`.** Cloud images set this
|
|
107
|
+
in a drop-in (e.g. `/etc/ssh/sshd_config.d/50-cloudimg-settings.conf`),
|
|
108
|
+
and sshd uses the *first* value it reads. The
|
|
109
|
+
`Include /etc/ssh/sshd_config.d/*.conf` line sits near the top of the
|
|
110
|
+
main config and globs in lexical order, so a later append loses to the
|
|
111
|
+
existing `50-` drop-in.
|
|
112
|
+
|
|
113
|
+
Win by adding a drop-in that sorts *before* it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
printf 'ClientAliveInterval 30\nClientAliveCountMax 3\n' | \
|
|
117
|
+
sudo tee /etc/ssh/sshd_config.d/10-playwriter-keepalive.conf
|
|
118
|
+
sudo systemctl reload ssh || sudo systemctl reload sshd
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Re-run the `sudo sshd -T` check. Confirm it now reports 30 / 3.
|
|
122
|
+
|
|
123
|
+
If it still shows the loose value, this distro orders `Include`
|
|
124
|
+
differently. Edit the drop-in that set it instead of adding a new one.
|
|
125
|
+
|
|
126
|
+
### 5. The SessionStart hook auto-links `vm-free-port.sh`
|
|
127
|
+
|
|
128
|
+
The ACStack SessionStart hook links the helper to an invariant path on
|
|
129
|
+
each Claude Code launch:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
~/.claude/acstack/bin/vm-free-port.sh -> <plugin cache>/assets/playwriter-bridge/vm-free-port.sh
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Invoke it by that path. The symlink always points at the current plugin
|
|
136
|
+
version, so the helper stays current with no manual copy. If you prefer a
|
|
137
|
+
copy on your `PATH` instead, copy the file and `chmod +x` it – but then you
|
|
138
|
+
own keeping it updated.
|
|
139
|
+
|
|
140
|
+
The script has a guardrail: it refuses to kill the port holder if that
|
|
141
|
+
holder is your own interactive SSH session. The guardrail scopes it
|
|
142
|
+
strictly to port 19988.
|
|
143
|
+
|
|
144
|
+
## Routine diagnostics on the VM
|
|
145
|
+
|
|
146
|
+
### Is the tunnel up?
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
ss -lnt 'sport = :19988'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If empty: the tunnel from the laptop has not connected. Produce a
|
|
153
|
+
VM-side handoff to the Windows agent – see [`HANDOFF.md`](HANDOFF.md).
|
|
154
|
+
|
|
155
|
+
### Does the relay respond?
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
curl -sS -m 3 http://127.0.0.1:19988/json/version
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Playwriter-flavored JSON means the full path (tunnel + relay) is up.
|
|
162
|
+
Empty or timeout with a listener present on 19988 means the tunnel is
|
|
163
|
+
half-open – ask the Windows side to restart the tunnel task.
|
|
164
|
+
|
|
165
|
+
### Is the extension armed?
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
curl -sS -m 3 http://127.0.0.1:19988/json/list
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`[]` means the extension has not attached to any tab. Ask the user to
|
|
172
|
+
click the Playwriter icon on a normal web page in the Playwriter Dev
|
|
173
|
+
Chrome profile. (This is a user action, not a coding-agent one, unless
|
|
174
|
+
a Windows agent is available to drive Chrome.)
|
|
175
|
+
|
|
176
|
+
### Smoke test
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
agent-browser open https://example.com
|
|
180
|
+
agent-browser screenshot /tmp/ok.png
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## When port 19988 is stuck
|
|
184
|
+
|
|
185
|
+
Symptom: the Windows `tunnel-<this-host>.log` shows
|
|
186
|
+
`remote port forwarding failed for listen port 19988` repeatedly, or
|
|
187
|
+
`ss -lnt 'sport = :19988'` shows an `sshd` bound but the relay is
|
|
188
|
+
unreachable.
|
|
189
|
+
|
|
190
|
+
The Windows agent sees this first. `doctor.ps1` turns its `Tunnel logs` row
|
|
191
|
+
amber as soon as `tunnel-*.log` carries a `stuck-retry` line under 5 minutes
|
|
192
|
+
old. The row names the host and the port. It goes red later, once the `.stuck`
|
|
193
|
+
marker file lands.
|
|
194
|
+
|
|
195
|
+
Either colour gives you the same instruction below.
|
|
196
|
+
|
|
197
|
+
Run:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
bash ~/.claude/acstack/bin/vm-free-port.sh
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The script walks the situation and either clears the socket or refuses
|
|
204
|
+
with a clear reason. Common outcomes:
|
|
205
|
+
|
|
206
|
+
- **"port 19988 is already free"** – nothing to do. The Windows tunnel
|
|
207
|
+
should reconnect within the backoff window.
|
|
208
|
+
|
|
209
|
+
- **"port is held by your own interactive session"** – you have an SSH
|
|
210
|
+
session alive that is carrying the forward. Exit that session (from
|
|
211
|
+
another terminal) and re-run.
|
|
212
|
+
|
|
213
|
+
- **Holder is a non-`sshd` process** – the script refuses. Something
|
|
214
|
+
else (local test server, stray `nc`) is on 19988. Stop it manually.
|
|
215
|
+
|
|
216
|
+
- **Holder is an `sshd` forward channel, now orphaned** – the script
|
|
217
|
+
kills it. `ClientAliveInterval` would have prevented this; consider
|
|
218
|
+
adding the config from step 4.
|
|
219
|
+
|
|
220
|
+
- **"port 19988 is carrying a live tunnel"** – the script refuses and exits
|
|
221
|
+
non-zero without killing anything. Before it looks at the holder at all it
|
|
222
|
+
asks the port for an HTTP response, bounded at 3 seconds.
|
|
223
|
+
|
|
224
|
+
Any answer counts as alive. It asks the port rather than the process
|
|
225
|
+
because a non-root `ss` cannot name who owns someone else's socket.
|
|
226
|
+
|
|
227
|
+
A 401 or a 404 still means something is serving. Only silence means a
|
|
228
|
+
half-open forward.
|
|
229
|
+
|
|
230
|
+
That last refusal matters because the amber `Tunnel logs` row trails five
|
|
231
|
+
minutes behind the log line that raised it.
|
|
232
|
+
|
|
233
|
+
A tunnel often wedges and then heals itself while the row stays amber. So
|
|
234
|
+
you usually reach this script after the forward has already healed.
|
|
235
|
+
Running it then costs nothing.
|
|
236
|
+
|
|
237
|
+
No flag overrides the refusal. To free a port that a live forward holds,
|
|
238
|
+
close the session holding it. On the laptop, run `stop.ps1`.
|
|
239
|
+
|
|
240
|
+
After clearing, confirm:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
ss -lnt 'sport = :19988'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Should be empty.
|
|
247
|
+
|
|
248
|
+
## What you CANNOT do from the VM
|
|
249
|
+
|
|
250
|
+
- **Rearm the extension on a tab.** That is a click in the user's Chrome,
|
|
251
|
+
inside a third-party extension. Nothing on either machine can press it.
|
|
252
|
+
|
|
253
|
+
- **Restart the relay or a tunnel task.** Both are Windows-side scheduled
|
|
254
|
+
tasks. The VM reaches them only when the opener is up, which the next
|
|
255
|
+
section covers.
|
|
256
|
+
|
|
257
|
+
For anything left, produce a `VM-SIDE HANDOFF` block per
|
|
258
|
+
[`HANDOFF.md`](HANDOFF.md). Hand it to the Windows agent, or tell the
|
|
259
|
+
user to paste it into PowerShell themselves.
|
|
260
|
+
|
|
261
|
+
## Restarting the laptop's tasks from here
|
|
262
|
+
|
|
263
|
+
When the **opener** is up, the restart above is no longer a handoff.
|
|
264
|
+
|
|
265
|
+
The opener is a small service on the laptop. It takes a page from the VM and
|
|
266
|
+
puts it in the default browser there. Because it runs on the laptop, it can also
|
|
267
|
+
restart the scheduled tasks the VM cannot reach.
|
|
268
|
+
|
|
269
|
+
See section 9.5, "The opener", of `docs/terminal-workspace.md`.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
acstack-bridge status # both machines' view, including the tasks
|
|
273
|
+
acstack-bridge fix [relay|tunnel|both]
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`fix` reports success only once the relay answers on the VM again. The opener
|
|
277
|
+
can accept a restart request while the bridge is still down. That is why `fix`
|
|
278
|
+
waits for the relay itself, rather than trusting the opener's reply.
|
|
279
|
+
|
|
280
|
+
When the opener is unreachable too, `acstack-bridge` prints the
|
|
281
|
+
`VM-SIDE HANDOFF` block for you rather than leaving you to write one.
|
|
282
|
+
|
|
283
|
+
The opener still does not cover arming. Nothing changes that.
|
|
284
|
+
|
|
285
|
+
## What this intentionally does NOT do
|
|
286
|
+
|
|
287
|
+
- **No relay on the VM.** The Playwriter extension hard-rejects any
|
|
288
|
+
`/extension` client that is not `127.0.0.1`, so the relay must run
|
|
289
|
+
next to Chrome.
|
|
290
|
+
|
|
291
|
+
- **No unprompted port scan.** `vm-free-port.sh` scopes itself strictly
|
|
292
|
+
to 19988 and refuses anything ambiguous. Do not generalise it.
|