Skip to content

fix(browser-bridge): nav/open --wake, fast ping, split routing errors, orientation telemetry, screenshot Read hint - #278

Merged
ZacxDev merged 7 commits into
mainfrom
fix/browser-cli-audit-2026-08-02
Aug 2, 2026
Merged

fix(browser-bridge): nav/open --wake, fast ping, split routing errors, orientation telemetry, screenshot Read hint#278
ZacxDev merged 7 commits into
mainfrom
fix/browser-cli-audit-2026-08-02

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Five CLI/behaviour fixes from the 2026-08-02 usage audit (claudedocs/browser-bridge-usage-audit-2026-08-02.md, PR #274), plus one regression found while measuring them.

Files: scripts/browser-bridge/browser, scripts/browser-bridge/server.py, scripts/browser-bridge/tests/test_browser_cli_args.py, scripts/browser-bridge/tests/test_server.py. No SKILL.md, no reference/**, no browser_tool.test.mjs, no extension.

What changed

S1 — --wake[=MS] on nav and open (audit F2). The flag existed on exactly html/text/js. The three copy-pasted parse blocks are consolidated into ONE _wake_flag helper that all five branches call; validation still happens at PARSE time (the die is in the flag loop, never inside wake_fields' command substitution).

nav/open send no wake field on the wire — the extension honours cmd.wake only on getHtml/text/eval, and the extension is out of this PR's scope. --wake is therefore a client-side compose (_wake_after): the nav/open op, then the existing wake op, annotated at result.data.wake — the same place a --wake read reports it. The round trip it removes is the agent's, not the bridge's. Two wire ops, one invocation.

S2 — ping gets its own 2 s deadline (F7). New PING_TIMEOUT_DEFAULT = 2.0 + BROWSER_BRIDGE_PING_TIMEOUT, resolved through a new _env_float that falls back on an unparseable value rather than killing the service at startup. Applied at exactly one place, op_timeout = ping_timeout if op == "ping" else cmd_timeout — no other op's timeout moves.

S3 — unknown_instance / no_extension split into two messages (F6). 48 of 52 unknown_instance failures used the correct label, with eval retried 37×. The server already returns known_instances in both bodies; the CLI now reads it. Key known-but-disconnected → names when it went away, says FULLY RESTART Brave, says DO NOT RETRY. Key never seen → says WRONG LABEL and lists the keys that exist, with no restart advice. no_extension gets the same split.

S4 — /whoami and /health emit telemetry (F8). 53 invocations were invisible to the only structured source. Metadata-only via the existing emit_cmd_event, after the response, best-effort. No domain and no key — these endpoints are global (they describe every connected profile at once), so emitting per-profile domains would widen the privacy contract at server.py's PRIVACY CONTRACT. /instances and /poll still emit nothing. health's output shape is untouched.

S5 — explicit-path screenshot prints a Read hint (F4; 7 of 63 captures never Read). Line 1 stays the bare path; line 2 is # Read <path> to view it. The # prefix keeps it from ever being mistaken for a second path. Not printed for --data-url or the temp-file form.

S6 — the broken doc pointer. browser:582/:588 pointed at "SKILL.md → Concurrency", a heading that has never existed. Repointed at reference/tabs-instances.md + the Concurrent-drivers paragraph of SKILL.md → This is the user's LIVE session, and a test now proves every SKILL.md → <heading> the CLI names resolves.

Bonus (second commit) — --help stopped dumping the whole file. Found while measuring S1–S6's byte cost: --help was grep -E '^#( |$)' "$0", i.e. every column-0 comment in the file. It shipped 25,440 bytes of implementation commentary as user-facing help and grew with every internal comment — S1–S6's comments alone would have added +4,129 bytes to every --help. Now the contiguous # block after the shebang, stopping at the first line of code.

Red → green matrix

Base = origin/main (ff0fe41). Method: commit, then git checkout origin/main -- browser server.py keeping the new tests, run, restore with git checkout HEAD -- …. No git stash anywhere (git stash list verified unchanged: 58 entries, stash@{0} intact).

Harness validation first. The initial base run reported 33 failures — contaminated: the new _serve(..., ping_timeout=) raises TypeError against the pre-S2 make_handler, so every _serve() call errored and pre-existing tests failed for a reason that had nothing to do with the change. That green/red was worthless. Re-run with a measurement-only signature shim (reverted, not shipped) and the base run came back exactly 17 failures, all of them new tests, every pre-existing test green — which is also the control proving the test-file helper edits are base-compatible.

test base HEAD kind
test_wake_on_nav_and_open_issues_the_wake_in_one_invocation[nav], [open] RED green regression (S1)
test_wake_on_nav_and_open_is_order_free_and_carries_ms[nav], [open] RED green regression (S1)
test_nav_and_open_validate_wake_ms_at_parse_time[nav], [open] RED green regression (S1)
test_nav_and_open_still_reject_an_unknown_flag[nav], [open] RED green regression (S1)
test_no_wake_on_nav_and_open_sends_exactly_one_unchanged_command[nav], [open] green green INVARIANT GUARD — back-compat
test_open_with_no_url_still_means_about_blank green green INVARIANT GUARD
test_ping_does_not_wait_out_cmd_timeout RED (8.0 s) green regression (S2)
test_ping_timeout_is_env_overridable_and_survives_a_malformed_value RED (8.0 s) green regression (S2)
test_the_short_deadline_applies_to_ping_ONLY green green NEGATIVE CONTROL
test_a_healthy_ping_is_unaffected_and_answers_in_milliseconds green green NEGATIVE CONTROL
test_unknown_instance_KNOWN_but_disconnected_says_stop_retrying RED green regression (S3)
test_unknown_instance_NEVER_SEEN_key_says_wrong_label RED green regression (S3) + branch control
test_no_extension_distinguishes_dropped_from_never_wired_up RED green regression (S3)
test_routing_failure_explainer_degrades_against_an_OLD_server green green INVARIANT GUARD — the CLI goes live on pull, before the switch restarts server.py
test_orientation_ops_emit_exactly_one_metadata_only_event[/whoami], [/health] RED (0 events) green regression (S4)
test_instances_and_poll_still_do_not_emit green green INVARIANT GUARD
test_orientation_emit_never_breaks_the_response green green INVARIANT GUARD — best-effort contract at the new call site
test_browser_cli_screenshot_explicit_path_prints_path_then_a_read_hint RED green regression (S5)
test_browser_cli_screenshot_read_hint_not_printed_where_it_would_be_wrong green green NEGATIVE CONTROL
test_every_skill_md_heading_the_cli_points_at_actually_exists RED (['Concurrency']) green regression (S6)
test_help_prints_the_HEADER_block_only_not_the_whole_files_comments RED green regression (help)

Mutation results (each confirmed red with THAT guard's own error)

# mutation result
M1 op_timeout = cmd_timeout (ping loses its deadline) RED — ping waited 8.0s — cmd_timeout, not its own. The negative control stayed green, proving it discriminates.
M2 op_timeout = ping_timeout (leaks to every op) RED — getHtml returned in 2.00s — the short ping deadline leaked onto every op. Only the negative control fired.
M3 delete the /health emit RED — expected exactly one event, got [], [/health] only; [/whoami] stayed green.
M4 emit a domain (privacy weakening) RED on both [/whoami] and [/health] — payload equality catches it.
M5 hard-wire S3 to the disconnected branch RED — assert 'is UNKNOWN' in "…is KNOWN but NOT CONNECTED…".
M6 hard-wire S3 to the unknown branch (if False:) RED — the mirror assertion. Both branches proven reachable.
M7 move the screenshot hint to the temp-file path RED twice: the positive test (hint absent from explicit path) and the negative control (hint wrongly present on the temp path).
M8 nav parses --wake but never composes the wake RED — assert ['nav'] == ['nav', 'wake'].
M9 delete the parse-time --wake=MS validation from _wake_flag RED on [nav] and [open] from a single deletion — the consolidation is real, one rule in one place.
M10 restore SKILL.md -> Concurrency RED — assert ['Concurrency'] == [].
M11 restore the grep-everything --help RED — --help leaked an internal comment: wake_fields WAKE WAITMS.

The S6 pointer test carries its own inline harness negative control: the extractor is run against a known-bad pointer (must report ["Concurrency"]) and a known-good one (must report []) before its verdict on the real file is read.

Test counts (counted, not exit codes)

  • pytest: 393 passed, 0 failed (baseline 369 → +24). Clean tree at the time of the run (git status -s empty).
  • node: tests 454 / pass 454 / fail 0 / skipped 0. grep -ci 'test timed out' → 0. No .mjs file was touched, so 454 is also the base count.

Verified live vs. tests only

Live host: laptop 192.168.50.155, profile personal, extension 0.7.0, deployed server git f5fbe82. My worktree was CLEAN at the time (committed). The browser CLI is a working-tree symlink so the CLI under test was my branch's code; server.py was the deployed artifact, i.e. pre-S2/S4.

S1 — live-verified end to end against tests/fixtures/oopif-rig/wake-rig.html served on 127.0.0.1:8901:

NEGATIVE CONTROL, same tab, read with NO wake:
  {"raf":1,"timer":16,"rendered":false,…} | app=WAKE-RIG-SHELL (waiting for frames) | vis=hidden
AFTER `browser nav --wake <rig>` (ONE invocation), plain read, no wake flag:
  {"raf":30,"timer":367,"rendered":true,"ms":544,…} | app=WAKE-RIG-RENDERED | vis=hidden
AFTER `browser open --wake <rig>`:
  WAKE-RIG-RENDERED | raf=30 | vis=hidden

raf pinned at 1 while timer climbed — live and throttled, not dead — so the control is not vacuous. vis=hidden throughout: no focus was moved, activate was never called. Tab closed afterwards.

S3 — live-verified for the never-seen branch (browser --instance nosuchlabel tabs printed the WRONG-LABEL message and listed personal, work). The known-but-disconnected branch was NOT live-verified — producing it means killing a Brave profile's long-poll, which I would not do to the operator's session. It is covered by canned-server tests only.

S2 — NOT live-verified. The change is in server.py, which is not deployed. I confirmed only that the healthy path is unaffected: browser --instance personal pingpong, extension 0.7.0, 68 ms end to end against the deployed server. The 2 s failure path is covered by wall-clock tests only.

S4 — NOT live-verified (same reason: server.py is not deployed). Covered by tests.

S5, S6, --help — tests + direct CLI invocation only (no bridge involved).

🔴 server.py changes need home-manager switch + systemctl --user restart browser-bridge to go live. The CLI half (S1, S3, S5, S6, --help) is a mkOutOfStoreSymlink onto the working tree and goes live on pull. Until the switch, S1's nav --wake works (it is pure CLI) but S2/S4 do not.

Per-call byte deltas of added output

surface before after delta
browser --help 25,440 B 15,532 B −9,908 B
screenshot <path> stdout path + \n + one line +20 B + len(path) (60 B for a 40-char path)
unknown_instance stderr 216 B 542 B +326 B, error path only (52 occurrences in the 2-day window)
nav/open without --wake 0 — same single wire op, same bytes
nav/open with --wake n/a + result.data.wake object replaces a separate browser wake invocation, which printed a larger standalone envelope — net negative
whoami / health / every read op 0

Could not verify

  • S2's timeout path and S4's emit on the real bridge — both require a home-manager switch this PR does not perform.
  • S3's known-but-disconnected branch live (would require killing a profile's long-poll).
  • Whether the audit's claim that health prints both profiles' full active-tab URLs to its caller is a problem worth fixing — explicitly out of scope; health's output shape is unchanged here.

Note on a test-harness change

test_server.py's _wait_connected now polls /instances instead of /health, and a new _wait_count does the same for two telemetry tests. Reason: S4 makes /health emit, and these helpers call it in a tight loop, so they injected spurious events into the spool of every test that waits for a connection (14 went red). The fix makes the harness silent rather than loosening the exact-count assertions. _wait_instances stays on /health because its callers read health-only fields. Both are commented in place so nobody points them back.

🤖 Generated with Claude Code

@ZacxDev

ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial audit run. Verdict: merge after fixing one 🔴. Two corrections to this PR body first, posted here rather than edited in, since a reviewer may already have read the original.

Correction 1 — the base-red count is 18, not 17

Re-measured as three disjoint runs at base: test_browser_cli_args.py against the base CLI = 9 failed / 40 passed; test_server.py against base server.py (+ signature shim) = 4 failed / 251 passed; the S3/S5/S6 subset against the base CLI = 5 failed / 26 passed. 9+4+5 = 18. The red→green table in the body itself lists 18 RED rows.

The substantive half of the claim — all new tests, every pre-existing test green — is confirmed.

Correction 2 — one base-red row is red for the wrong reason

test_help_prints_the_HEADER_block_only_not_the_whole_files_comments is listed as RED → green | regression (help). At base it actually fails on the landmark assertion "browser nav <url> [--wake[=MS]]" in cp.stdout — text that S1 added to the header — not on the comment-leak assertion the test is named for. The leak half is genuinely pinned (the author's M11 mutation exercises it), but as a base-red it is entangled with S1 rather than being independent evidence for the --help fix.

Commit isolation is otherwise clean: git log -S confirms the help test lives in 8a26983, so dropping the bonus commit drops its test with it.


🔴 The blocking finding: the 2 s ping deadline false-negatives a busy extension

server.py:1998. The extension's poll loop is strictly serial (service_worker.js:1646-1652: await execute()await postResult() → next pollOnce()). ping skips the per-tab FIFO (it isn't in TAB_SCOPED_OPS) but still cannot be dequeued until the extension finishes its current command — and execute() self-bounds at EXEC_OP_BUDGET_MS = 18000.

Measured at two points on one rig (a FakeExtension running a legitimate 6 s getHtml, then a ping):

ping_timeout result
2.0 s (this PR) 504 timeout at 2.00 s — while the getHtml completed fine
20.0 s (pre-change) 200 pong at 5.51 s

Failure scenario: two sessions drive one Brave profile — the documented, rate-limit-backstopped case. Agent B runs browser ping (the skill's documented first thing to run) while Agent A is mid-nav on a heavy page or a --fullpage screenshot. B gets "timeout waiting for the extension to answer (is Brave focused / responsive?)" and concludes the extension is dead or stale — whose documented remedy is a full Brave restart of the operator's live session. The extension was healthy.

The 68 ms healthy-path measurement that motivated 2 s was taken against an idle extension. Both S2 tests use a wedged or an idle extension; none uses a busy one — so the negative control that would have caught this was never constructed.

Confirmed clean

  • Privacy contract intact_emit_diag_event (server.py:721) passes only literals to emit_cmd_event; no URL, query, token, profile key or insts data can reach the spool. This was the highest-risk item on the brief.
  • Wire bodies byte-identical base↔head on nav, open, text, html, js across nine flag combinations — which is also the proof that the _wake_flag consolidation did not alter the three pre-existing read paths.
  • The _wait_connected harness move is provably equivalent, not a weakening: /health's extension_connected = bool(insts) and /instances' count = len(insts) come from the same registry.snapshot().
  • No caller depends on the old error strings — the opencode tool talks HTTP directly and browser-agent branches on exit codes.
  • --help lost nothing user-facing: the first 216 lines are identical; everything dropped is implementation commentary.

Follow-ups being addressed in the same pass: nav --wake discarding the successful nav's result when only the wake fails; an unguarded --frame + --wake half-state; the 503 path no longer echoing last_unanswered_op; reference/errors.md:28 still describing the conflation S3 exists to break; and SKILL.md not documenting the new flag at all (nav's row lists no flags), which is what makes S1 invisible to the agents it was built for.

ZacxDev and others added 4 commits August 2, 2026 01:10
…ge audit

S1 --wake[=MS] on `nav` and `open` (F2). The flag existed on exactly html/text/js;
   13 nav->wake / open->wake adjacent pairs in transcripts and one opencode session
   that is literally `nav wake eval` x5 were paying for the gap. The three copied
   parse blocks are consolidated into one `_wake_flag` helper (validation still at
   PARSE time). nav/open send NO `wake` field on the wire — the extension honours
   cmd.wake only on getHtml/text/eval — so `--wake` is a client-side compose
   (`_wake_after`): the nav/open op, then the existing `wake` op, annotated at
   result.data.wake exactly where a `--wake` read reports it.

S2 `ping` gets its own 2s deadline (F7): 38.2% failure rate, every failure burning
   the full 20s CMD_TIMEOUT while 21 healthy pings averaged 3-4ms. New
   BROWSER_BRIDGE_PING_TIMEOUT / PING_TIMEOUT_DEFAULT; no other op's timeout moves.

S3 unknown_instance / no_extension now separate "wrong label" from "right label,
   profile disconnected" (F6): 48 of 52 failures used the CORRECT label, with
   `eval` retried 37 times. The disconnected branch names when the profile went
   away, says FULLY RESTART Brave, and says DO NOT RETRY.

S4 /whoami and /health emit metadata-only telemetry (F8) — 53 invocations were
   invisible to the only structured source. No domain (they are global ops), no
   key; /instances and /poll still emit nothing.

S5 an explicit-path `screenshot` prints one `#`-prefixed hint that the .png must
   be Read (F4: 7 of 63 captures never read). Line 1 stays the bare path.

S6 the CLI's "SKILL.md -> Concurrency" pointers named a heading that has never
   existed; a test now proves every SKILL.md heading the CLI names resolves.

server.py changes need a `home-manager switch` + `systemctl --user restart
browser-bridge` to go live; the CLI is a working-tree symlink and is live on pull.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…omment

`--help` was `grep -E '^#( |$)' "$0"`, which matched EVERY column-0 comment in the
file — so it shipped the whole implementation commentary as user-facing help:
25,440 bytes against a ~15 KB header, and it grew with every internal comment
anyone added (the five audit fixes in the parent commit would alone have put
4,129 bytes of maintainer notes onto the help screen).

Now: the contiguous `#` block after the shebang, stopping at the first line of
code. Measured on this branch: 25,440 -> 15,532 bytes per `--help`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y extension

Adversarial audit of #278, confirmed by reproduction. The extension's poll loop is
strictly serial, so `ping` skips the per-tab FIFO but still cannot be DEQUEUED
until the running command finishes. Measured on a FakeExtension running a
legitimate 6s getHtml, then a ping:

  ping_timeout=2.0  -> 504 timeout at 2.00s   (the getHtml completed fine)
  ping_timeout=10.0 -> 200 pong  at ~6.0s

Real blast radius: two sessions on one profile; agent B runs `browser ping` — the
skill's documented FIRST action — while agent A is mid-nav. B was told "is Brave
focused / responsive?", whose documented remedy is a FULL Brave restart of the
operator's live session.

Root cause of the miss: the healthy 68ms measurement was against an IDLE
extension, and both S2 tests used a wedged or idle one. NO test used a busy one.

Fixes:
  * PING_TIMEOUT_DEFAULT 2.0 -> 10.0, justified as an INVARIANT rather than a
    number: it must exceed the longest a HEALTHY extension can be busy with a
    command the CLI can ASK for. ACTIVATE_WAIT_MAX_MS (8s) is that ceiling; 10s is
    the smallest value above it with slack for the result POST + poll turnaround.
    The residual (10s < EXEC_OP_BUDGET_MS 18s) is stated, not hidden, and the
    rejected "escalate when the instance is busy" alternative is recorded with the
    reason it does not work (pending>0 cannot tell BUSY from WEDGED).
  * The 504 message is now op-aware: a ping timeout says the extension may be BUSY,
    says to re-run once the other op finishes, names the env override, and
    explicitly refuses to recommend a Brave restart.
  * New regression test with a BUSY (not wedged, not idle) extension. Red at the
    previous tip at exactly 2.00s.

Also hardened test_the_short_deadline_applies_to_ping_ONLY, which silently stopped
discriminating when the default rose above its cmd_timeout: a lower bound alone
now survives the `op_timeout = ping_timeout` mutant (10 >= 3.5). It is bounded on
both sides and asserts its own precondition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e, docs

A. A failing `--wake` no longer swallows the successful primary op. `_wake_after`
   used `|| exit 1`, which exited rc 1 with EMPTY stdout on all three measured
   failure modes (429, 504, op-level unknown_op) — so `T=$(browser nav --wake
   "$url")` yielded an empty T for a tab that really had navigated, and only the
   op-level branch even mentioned `wake`. Now: the primary JSON is always emitted,
   the failure is attached at result.data.wake.ok=false, stderr says the primary
   SUCCEEDED and which half failed, and the exit code is a distinct 3.

B. `--frame X nav --wake <url>` is refused at PARSE time. `--frame` is spliced into
   every op by cmd_op; `nav`/`open` ignore it but the standalone `wake` is refused
   by the extension's assertWakeNotFramed — so the composed form navigated and THEN
   failed the wake. This half-state did not exist before `--wake` was added to
   nav/open (it was a hard parse error), i.e. the feature opened it. One shared
   predicate, both callers. VERIFIED LIVE, not inferred.

C. Restored the drop evidence the S3 rewrite dropped: `last_unanswered_op` on every
   rendered instance, and the last-seen/last-op lines on the never-seen 404 branch
   (render_missing used to print them). server.py calls that field "the single fact
   that turns the next silent drop from inference into evidence".

D. reference/errors.md: `404 unknown_instance` said "the --instance key matches no
   connected instance" — exactly the conflation S3 exists to break. Now documents
   both branches with their opposite actions, plus the same split for 503 and the
   `ping` exception to `504 timeout`.

E. SKILL.md documents the flag where agents actually read it: `nav`/`open` rows and
   the `wake` row. 11,911 -> 12,010 B; 278 B under the 12,288 ceiling, 28 B above
   the 250 B floor. Agents read SKILL.md, not `browser --help`, so without this S1
   changed agent behaviour approximately zero.

F. The --help test now pins the LAST line and the full header line-for-line. `awk`
   exits at the first non-`#` line, so a blank line inserted near the end of the
   header truncated --help with landmarks + an upper length bound still green.

G. `_wait_connected`/`_wait_count` use `body["count"]`, not `.get("count", 0)`: a
   missing key must raise, not read as "nothing connected" — which for want=False
   was an instantly vacuous pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev force-pushed the fix/browser-cli-audit-2026-08-02 branch from 8a26983 to 8daee7f Compare August 2, 2026 06:37
@ZacxDev

ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Audit round 1 addressed — 🔴 + A–G

Rebased onto origin/main (0cb7324, after #266/#274/#275). Rebase, not merge — no overlap with any of the three merged PRs, so it was clean. Two new commits: 5c7897b (🔴) and 8daee7f (A–G).

Two corrections to my original PR body — acknowledged, not silently edited

  • Base-red count is 18, not 17. My table listed 18 RED rows against a 17-failure run because test_nav_and_open_still_reject_an_unknown_flag[nav]/[open] were red in a later disjoint run, not the one I quoted the total from. The audit's reconciliation (9 + 4 + 5 across three runs) is correct; the "all new tests, no pre-existing red" half stands.
  • test_help_prints_the_HEADER_block_only... is red at base on the LANDMARK assertion, not the comment-leak one — the landmark it trips on is "browser nav <url> [--wake[=MS]]", text S1 itself added. So as a base-red it is entangled with S1 and is not clean evidence for the --help fix on its own. The clean evidence for that fix is mutation M11 (restore the grep-everything extractor → --help leaked an internal comment: wake_fields WAKE WAITMS).

🔴 BLOCKING — fixed, and reproduced first

Reproduced exactly as reported before changing anything: a FakeExtension running a legitimate 6 s getHtml, then a ping → 504 at 2.00 s.

1. PING_TIMEOUT_DEFAULT 2.0 → 10.0, expressed as an invariant. The code now states the property rather than the number:

PING_TIMEOUT_DEFAULT > the longest a HEALTHY extension can be busy with a command the CLI is able to ASK FOR.

Justified against the extension's own constants: ACTIVATE_WAIT_MAX_MS 8 s is the largest wait the CLI can request (activate --wait 8000; WAKE_SETTLE_MAX_MS is 6 s), CDP_OP_BUDGET_MS 15 s, EXEC_OP_BUDGET_MS 18 s. 10 s is the smallest value above the 8 s caller-requestable ceiling, plus ~2 s for the result POST and poll turnaround.

I did not take the suggested ~6 s: 6 < ACTIVATE_WAIT_MAX_MS, so it would still false-negative on a perfectly legal activate --wait 8000.

Residual, stated in the code rather than hidden: 10 s < EXEC_OP_BUDGET_MS, so an op that legitimately runs 10–18 s can still time a ping out. Only ≥18 s eliminates the class, and that is the 20 s this exists to avoid.

Structural alternative considered and REJECTED, recorded in the code so nobody re-derives it: "use the short deadline only when the instance is idle, escalate when busy." inst.pending cannot distinguish BUSY from WEDGED — a wedged extension is precisely one that took a command and never answered, i.e. pending > 0 — so the escalation would fire in exactly the case the short deadline exists for.

2. The 504 is now op-aware. A ping timeout says the extension executes one command at a time, that a queued ping times out while the profile is healthy, to wait and re-run, that only a repeat failure on an idle profile implicates the build, and names the env override. It ends: "do NOT restart Brave on the strength of this alone." The generic message is unchanged for every other op.

3. The busy-extension test. Establishes the precondition (polls ext.dispatched until the slow op is genuinely dequeued) rather than assuming it, and carries a lower bound on elapsed so it cannot pass against an extension that was never busy.

test at 8a26983 (prev tip) at 8daee7f
test_ping_survives_a_BUSY_but_perfectly_healthy_extension RED — 504 at 2.00 s green — 200 pong at ~6 s

A second bug the raise exposed, which I would have shipped otherwise: test_the_short_deadline_applies_to_ping_ONLY silently stopped discriminating when the default rose above its cmd_timeout. It had a lower bound only, so the op_timeout = ping_timeout mutant (10 s leaked onto getHtml) passed 10 >= 3.5. It is now bounded on both sides and asserts its own precondition (PING_TIMEOUT_DEFAULT > cmd_timeout). Re-verified: M2 → getHtml waited 10.00s — the ping deadline leaked onto every op.


🟡 A–G

A — a failing wake no longer swallows the primary result. Always emits the primary JSON, attaches the failure at result.data.wake.ok=false, stderr says which half failed, and exits a distinct 3 ("primary ok, wake failed"). pipefail carries rc 3 through | pretty.

B — --frame + --wake refused at PARSE time, one shared predicate for both callers. Verified LIVE, not by reading — see below.

C — restored the lost evidence. last_unanswered_op on every rendered instance, and the last-seen/last-op lines on the never-seen 404 branch. A typo and a dead profile are frequently the same incident, which is what that branch was hiding.

D — reference/errors.md now documents both unknown_instance branches with their opposite actions, the same split for 503, and a ping exception under 504 timeout.

E — SKILL.md. nav/open rows gained [--wake[=MS]], and the wake row now reads text|html|js|nav|open --wake[=MS] — the row an agent actually reads to learn the flag. 11,911 → 12,010 B (+99): 278 B under the 12,288 ceiling, 28 B above the 250 B floor. Gate green. My first draft was +134 and failed the floor by 7 B; the prose was cut, not the floor.

F — the --help last-line pin. Now pins the last line and the full header line-for-line, both derived from the source.

G — body["count"], not .get("count", 0).


Red → green (this round). Base = 5c7897b unless noted

test base HEAD kind
test_ping_survives_a_BUSY_but_perfectly_healthy_extension RED @ 8a26983 (504 @ 2.00 s) green regression (🔴)
test_a_failing_wake_never_swallows_the_primary_result × 6 (nav/open × op_error/429/504) RED green regression (A)
test_a_SUCCESSFUL_wake_still_exits_zero[nav]/[open] green green NEGATIVE CONTROL for rc 3
test_frame_plus_wake_is_refused_before_anything_reaches_the_wire[nav]/[open] RED green regression (B)
test_frame_WITHOUT_wake_and_wake_WITHOUT_frame_both_still_work[nav]/[open] green green NEGATIVE CONTROL — the guard must fire on the conjunction only
test_unknown_instance_KNOWN_but_disconnected... RED green regression (C)
test_unknown_instance_NEVER_SEEN_key_says_wrong_label RED green regression (C) + branch control
test_no_extension_distinguishes_dropped_from_never_wired_up RED green regression (C)
test_help_prints_the_HEADER_block_only... (F's new pins) green green INVARIANT GUARD — test-only hardening; its evidence is MF1, below

Mutations (each red with THAT guard's own error)

# mutation result
M1/M2 re-run against the 10 s default both still RED with their own messages
MA1 if true — exit 3 unconditional RED, and the negative control test_a_SUCCESSFUL_wake_still_exits_zero is what fired
MA2 drop the data["wake"] failure annotation RED — KeyError: 'wake'
MB1 neuter _reject_frame_with_wake RED on both frame_plus_wake cases only
MB2 fire the guard on --frame alone (over-broad) RED on the negative control only — positive test stayed green
MC1 drop last_unanswered_op RED ×3 — assert 'last unanswered op: eval' in …
MF1 blank line 6 lines from the end of the header RED — "--help was truncated before the end of the header block"
MG1 /instances loses count KeyError: 'count' — raises instead of reading as "not connected"
M5/M6 re-run the S3 branch mutants after C's edit both still RED, each on its own branch

MF1 is worth calling out. I checked whether F's pin was actually necessary by running the pre-F assertions against the mutant — every one of them passes while 7 lines of help are silently lost:

landmark 'FLAG ORDER / END OF FLAGS'            -> PASS
landmark 'browser nav <url> [--wake[=MS]]'      -> PASS
landmark 'Global flags (before the subcommand)' -> PASS
no-leak  (all four)                             -> PASS
length bound 15049 < 15856                      -> PASS
LOST LINES: 7

So the audit's F was right and the pin is load-bearing, not decorative.

Counts (counted, not exit codes)

  • pytest 407 passed, 0 failed (was 393 → +14). Clean tree at the time of the run.
  • node tests 454 / pass 454 / fail 0 / skipped 0; grep -ci 'test timed out'0.

Verified live vs. tests only

Laptop .155, profile personal, ext 0.7.0, deployed server f5fbe82, worktree clean and committed at the time.

B — verified LIVE, not inferred. Both halves:

  1. The premise: browser --frame wake-rig --tab <T> wakewake_with_frame_unsupported: un-throttling is tab-level, not per-frame. The extension really does refuse a framed wake.
  2. The guard, with a before/after URL proving nothing was half-applied:
url BEFORE: "/deep0.html"
$ browser --tab <T> --frame child nav --wake http://127.0.0.1:8901/wake-rig.html
browser: --frame and --wake cannot be combined on 'nav'.
browser: refused before anything was sent (nothing has been navigated or opened)
rc=1
url AFTER:  "/deep0.html"      <- unchanged

S1 re-verified live after the _wake_after rewrite (it is on the hot path of A): nav --wakewoke:true, WAKE-RIG-RENDERED | raf=30 | vis=hidden, rc 0. No focus moved; tab closed afterwards.

S3 re-verified live for the never-seen branch, now including the drop evidence.

Still NOT verified live, unchanged from before: S2's timeout path and S4's emit (both in server.py, which is not deployed — needs home-manager switch + systemctl --user restart browser-bridge); S3's known-but-disconnected branch (would require killing a Brave profile's long-poll on the operator's live session). A is covered by canned-server tests only — all three failure modes (429/504/op-level) are exercised there, but I did not manufacture a live wake failure.

Byte deltas, updated

surface delta
browser --help −9,908 B (25,440 → 15,532)
SKILL.md +99 B (11,911 → 12,010) — every browser task pays this
screenshot <path> stdout +20 B + len(path)
unknown_instance stderr +326 B → now ~+390 B with the restored evidence; error path only
ping 504 stderr ~+700 B, and only on a ping timeout (13 in the 2-day window)
nav/open without --wake, all read ops, whoami/health 0

…t a constant

PRIMARY (audit 🟡-2, and it dissolves 🟡-1 and 🟢-3). The audit was right that a
distinguishing signal exists on the Instance, and right that my ✗REJECTED note was
about the wrong field. `inst.pending` cannot tell BUSY from WEDGED — but the AGE of
the outstanding work can, because the extension self-bounds one command at
EXEC_OP_BUDGET_MS = 18s.

`inst.last_dispatch` cannot carry it, for two concrete reasons:
  * it is OVERWRITTEN by every new enqueue, so it names the NEWEST outstanding
    command, not the oldest — a fresh enqueue behind a wedged one makes the wedge
    look young;
  * its `at` is wall-clock time.time(), while all deadline math uses the injectable
    monotonic self._clock(); an NTP step would move it.
Both are properties of that field, not of the idea. So `last_dispatch` stays
DIAGNOSTIC and a new `Instance.inflight` (cid -> monotonic enqueue reading, every
outstanding command) does the timing.

Registry._effective_timeout_locked is the gate:
  * nothing in flight -> a ping that does not answer within the fast deadline IS
    the wedge. PING_TIMEOUT_DEFAULT is back to 2s and is now ONLY the idle floor.
  * work in flight    -> allow it its own budget: N * EXEC_OP_BUDGET_S +
    WEDGE_GRACE_S - age(oldest). N scales it because the loop drains serially.
  * clamped to cmd_timeout above (so this can never be slower than the behaviour
    predating the fast ping) and to fast_timeout below (an overdue instance still
    gets its full fast deadline, not an instant fail).

This dominates the tuned constant on both axes: wedge detection returns to ~2s
(it was ~10s), and a busy profile is never reported dead — including the case a
constant cannot express at all, busy AND wedged.

🟡-1 is therefore moot rather than fixed: the invariant no longer has to out-bid
any ceiling. The old comment claimed 8s was "the caller-requestable ceiling" and
was wrong twice over — `screenshot --fullpage` (CDP_OP_BUDGET_MS 15s) and
`wake --wait 6000` composed with an 8s CDP attach both exceed 10s. Both are now
named in the code as the reason the tuned approach was abandoned.

EXEC_OP_BUDGET_S mirrors a constant across a language boundary, so
test_exec_budget_matches_the_extension reads protocol.js and fails on drift.

🟡-3 The ping-timeout guidance — the entire mitigation for the disclosed residual —
had zero test coverage; deleting the block left both suites green. Now pinned, with
a negative control asserting a NON-ping timeout keeps the generic wording. The text
is also rewritten for the new semantics (it claimed a "default 10s SHORT deadline").

🟡-4 result.data.wake.error now carries the REAL cause (`rate_limited`, `timeout`,
`wake_with_frame_unsupported`) instead of the constant "wake_failed_after_nav", so
a script can tell transient from permanent. cmd_op stderr is captured, re-emitted
VERBATIM, then classified. The test pins the error VALUE per failure mode.

🟢-1 result.data.wake is now shape-symmetric: `ok` is always present. Success used
to write the raw payload, which has no `ok` key, so `if (data.wake.ok)` was falsy
on every successful wake. Three shapes became two.

🟢-2 Exit code 3 is documented in the CLI header block (not SKILL.md, which has
28 B of slack).

Also fixed: two background threads in the new tests raised into pytest as
unhandled daemon-thread exceptions and were attributed to an unrelated later test
(test_release_drops_ownership_without_dispatch). The suite now runs clean under
-W error::pytest.PytestUnhandledThreadExceptionWarning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Round 2 addressed — structural gate implemented (a0a0021)

PRIMARY 🟡-2 — implemented, not the fallback

The audit was right on both counts: a distinguishing signal does exist on the Instance, and my ✗REJECTED note argued about the wrong field. inst.pending genuinely cannot separate BUSY from WEDGED — but the age of outstanding work can, because the extension self-bounds one command at EXEC_OP_BUDGET_MS = 18 s.

inst.last_dispatch cannot carry it, for two properties of that field (not of the idea):

  1. It is overwritten by every new enqueue (server.py:1263), so it names the newest outstanding command, not the oldest. A fresh enqueue behind a wedged one makes the wedge look young — the discriminator reads "busy" exactly when it should read "wedged".
  2. Its at is wall-clock time.time(), while every deadline uses the injectable monotonic self._clock(). Comparing them is a bug an NTP step would surface.

Both are cheap to route around, so the idea holds and I implemented it. last_dispatch stays diagnostic (now labelled as such); a new Instance.inflightcid → monotonic enqueue reading, for every outstanding command — does the timing.

Registry._effective_timeout_locked:

  • nothing in flight → nothing can be ahead of the ping, so a stalled reply is the wedge. PING_TIMEOUT_DEFAULT is back to 2 s and is now only the idle floor.
  • work in flight → give it its own budget: N * EXEC_OP_BUDGET_S + WEDGE_GRACE_S − age(oldest). N scales it because the loop drains serially — otherwise a legitimate queue would false-negative.
  • clamped both sides, and both clamps are load-bearing: never above cmd_timeout (so this can never be slower than the pre-fast-ping behaviour), never below fast_timeout (an overdue instance still gets its full fast deadline rather than an instant fail).

It dominates the constant on both axes, as you predicted: wedge detection returns to ~2 s (was ~10 s), a busy profile is never reported dead, and it covers the case a constant cannot express at all — busy and wedged, which is now its own test.

🟡-1 is moot rather than fixed. With the deadline derived, the invariant no longer has to out-bid any ceiling. You were right that the old paragraph asserted something the value did not satisfy, and it was wrong twice — I have named both screenshot --fullpage (15 s) and wake --wait 6000 composed with an 8 s CDP attach in the code, as the evidence that the tuned approach was unsalvageable rather than mistuned. 🟢-3 dissolves with it.

EXEC_OP_BUDGET_S mirrors a constant across a language boundary, so test_exec_budget_matches_the_extension reads protocol.js and fails on drift.

🟡-3 / 🟡-4 / 🟢-1 / 🟢-2

  • 🟡-3 Confirmed: deleting the block left both suites green. Now pinned, with a negative control asserting a non-ping 504 keeps the generic wording. The text is also rewritten — it claimed a "default 10 s SHORT deadline", which the structural gate makes false.
  • 🟡-4 result.data.wake.error carries the real cause. cmd_op stderr is captured, re-emitted verbatim (nothing hidden), then classified — op-level line first, then the server body the HTTP branches echo. The test pins the error value per failure mode.
  • 🟢-1 result.data.wake is shape-symmetric: ok always present, assigned after the copy so it wins deterministically. Three shapes → two.
  • 🟢-2 Exit code 3 documented in the CLI header block. SKILL.md untouched — still 12,010 B, 278 B under the ceiling, 28 B of slack.

Red → green (base 8daee7f)

test base HEAD kind
test_ping_fast_fails_even_while_BUSY_once_the_work_has_blown_its_budget RED green regression (🟡-2)
test_effective_timeout_gate_unit RED green regression (🟡-2)
test_exec_budget_matches_the_extension RED green regression (🟡-2)
test_inflight_is_released_on_every_exit_path RED green regression (🟡-2)
test_ping_does_not_wait_out_cmd_timeout RED (10 s) green (~2 s) regression — retargeted to the idle path
test_the_short_deadline_applies_to_ping_ONLY RED green control, re-bounded for the new semantics
test_a_failing_wake_never_swallows_the_primary_result × 6 RED (wake_failed_after_nav) green regression (🟡-4)
test_a_SUCCESSFUL_wake_still_exits_zero[nav]/[open] RED (KeyError: 'ok') green regression (🟢-1)
test_a_ping_timeout_does_NOT_steer_the_operator_into_restarting_brave RED green entangled — red on the reworded text, since the block existed at 8daee7f. Its clean evidence is MS7 below.
test_a_NON_ping_timeout_keeps_the_generic_wording green green NEGATIVE CONTROL

15 red at 8daee7f, all new/retargeted; no pre-existing test red.

Mutations — each red with THAT guard's own error

# mutation result
MS1 gate ignores fast_timeout (always cmd_timeout) RED ×4 incl. the unit gate
MS2 gate is busy-blind (always fast_timeout) RED — "a BUSY but healthy extension was reported dead after 2.00s"
MS3 drop inflight.pop in the finally RED — "leaked after success"
MS4 EXEC_OP_BUDGET_S 18 → 17 RED — "protocol.js says 18000ms, server.py says 17.0s"
MS5 drop the LOWER clamp RED — assert -10.0 == 2.0 (overdue would fail instantly)
MS6 drop the UPPER clamp RED — "must never exceed cmd_timeout"
MS7 delete the whole ping-504 block RED — only the 🟡-3 test; the negative control stayed green
MS8 revert the wake error to a constant RED ×6, each naming the cause it lost
MS9 success shape loses ok RED ×2 — KeyError: 'ok'

A defect I introduced and caught

The two new threaded tests left unhandled daemon-thread exceptions, which pytest attributed to an unrelated later test (test_release_drops_ownership_without_dispatch) — the "a crashed sweep poisons the next run" class. Both now capture their exception and join. The suite is run with -W error::pytest.PytestUnhandledThreadExceptionWarning and is clean.

Counts (counted, not exit codes)

  • pytest 413 passed, 0 failed (was 407), under -W error::…ThreadExceptionWarning. Run under nix-shell -p python312Packages.pytest, and I checked the count is plausible rather than reading the exit code — a bare python3 -m pytest on this host would exit 0 printing No module named pytest.
  • node tests 454 / pass 454 / fail 0 / skipped 0; test timed out grep → 0.
  • Clean tree at the time of the run; git stash list unchanged at 9.

Live

Laptop .155 / personal / ext 0.7.0 / deployed server f5fbe82, tree clean. Healthy pingpong, ext 0.7.0. The gate itself is NOT live-verified — it is server.py, which needs home-manager switch + systemctl --user restart browser-bridge. Everything about it is covered by the threaded + unit tests above, including the wedged-while-busy case (via a monkeypatched EXEC_OP_BUDGET_S, so it runs in test time rather than 18 real seconds).

Noted on the #271 SKILL.md conflict — I have not touched #271 and will leave the resolution to you.

ZacxDev added a commit that referenced this pull request Aug 2, 2026
Hardens the CLI dispatch parser in test_surface_parity.py. It matched
`\bcmd_op\s+([A-Za-z]+)` on any line whose first non-space character was
not `#`, so any prose containing the phrase `cmd_op <word>` was harvested
as a wire op.

MEASURED on the merged tree of this PR + #278 (tip a0a0021): #278 added a
Python docstring inside a `python3 -c` block reading "The machine-readable
cause, from `cmd_op stderr` it already emits" — and the parser harvested a
phantom op named `stderr`. Merged-tree pytest went 1 failed / 424 passed,
with a diagnostic pointing at a wire op that does not exist. Invisible on
either branch alone. Fixed here as a CLASS, not by special-casing the word.

The rule now requires TWO independent conditions:

  1. the occurrence survives `mask_shell_noncode()` — a small shell lexer
     that blanks comments, single-/double-quoted strings (including
     MULTI-LINE ones), backquoted spans and heredoc bodies, preserving byte
     offsets. `$( … )` is deliberately NOT masked even inside double
     quotes: that is genuine command position.
  2. it sits in command position — start of statement, or after
     `;` `&&` `||` `|` `(` `$(` `then` `do` `else`.

New fixture tests/fixtures/cmd_op_parse_rig.sh pins 8 dispatch shapes that
MUST be harvested and 11 mention shapes that MUST NOT be.

RED-FIRST (the whole point): against the rig, the previous parser harvested
7 phantoms — phantombacktick, phantomdocstring, phantomdq, phantomheredoc,
phantommultilinedq, phantomquotedheredoc, phantomsq — while keeping all 8
real ones. Reverting the parser (mutation P1) reproduces exactly that.

Over-tightening controls, because a permanently-green no-op is the obvious
wrong fix:
  - the rig set must match EXACTLY (misses are named, not just leaks);
  - the real CLI must still yield all 19 ops == server.py's inventory;
  - `screenshot` is pinned by name — it is dispatched from
    `resp="$(cmd_op screenshot "$full")"`, a command substitution nested in
    double quotes. An earlier version of the masker blanked the `$(` and
    lost exactly that one op, 19 -> 18, while every other test stayed
    green. Caught by this control, not by review.
  - a new test pins that the CLI contains no LIVE backtick substitution,
    since backquoted spans are masked conservatively (113 backticks, all
    inside comments — MEASURED).

Mutation sweep, 10 mutations, all red, each naming its specific phantom or
missing op. Two SURVIVED the first pass and are the reason the rig grew two
more cases:
  - P5 (drop command-position anchoring) was green — every phantom was
    already being caught by masking. Now caught by an UNQUOTED bareword
    mention (`echo usage: cmd_op phantombareword`).
  - P6 (drop single-quote masking) was green for the WRONG reason: the
    `"""` in the docstring case toggled double-quote state, so a different
    defence was catching it. Now caught by a single-quoted `python3 -c`
    block containing no double quotes.
Also re-verified P7, the M14-style "new wire op with no CLI subcommand"
case, still goes red naming `sniff`.

Verified against the real #278 shape: injecting that exact docstring into a
throwaway COPY of `browser` (the real one is owned by #278 and untouched)
leaves the op set at 19 -> 19 and does not harvest `stderr`.

Counts (from reporter lines, not exit codes; pytest 9.0.2 confirmed
importable — a bare `python3 -m pytest` on this host exits 0 printing
"No module named pytest"):
  pytest 381 -> 385 passed, 0 failed
  node   460 tests, 459 pass, 1 fail — unchanged, still the deliberate
         AGENT SURFACE PARTITION finding awaiting the operator's ruling on
         context/emulate/ping. Not pre-empted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, wake causes

🔴-A A docstring inside a `python3 -c` block read "<dispatch-helper> stderr", and
   #277's surface-parity gate harvests wire ops with a regex that skips only lines
   whose first non-space char is `#`. A docstring line is not one, so it harvested a
   phantom wire op named `stderr` and reddened the MERGED tree (1 failed / 424
   passed) while both branches were green alone. Reworded, and a local test now runs
   the same harvest so the trap is closed on this side too rather than depending on
   the other PR — either fix alone closes it, either alone can regress.

🟡-B The submitter abandoning a command does NOT free the extension: its serial loop
   keeps executing for up to EXEC(18) + RESULT(10) = 28s, LONGER than the 20s
   cmd_timeout. Popping the `inflight` entry at submitter-exit therefore opened a
   window where the instance looked IDLE while provably still busy, and the next
   ping fast-failed at 2s against a healthy extension — in that window a0a0021 was
   worse than both main (20s flat) and the interim constant (10s flat). Now the
   entry is dropped only when the command never left the outbox (it will never run);
   when the extension already took it the entry SURVIVES the submitter, is released
   when the late result arrives, and otherwise expires at INFLIGHT_STALE_S
   (EXEC + RESULT) so one wedged command cannot make ping slow forever.

🟡-C `unknown_op` was the one wake failure still losing its cause: its branch uses
   neither the "failed in the browser:" prefix nor an echoed body, so both
   classifier arms had nothing to read. It is also the most likely `--wake` failure
   here (a stale loaded extension) and the PERMANENT one the exit-3 contract
   promises callers can distinguish. Added the arm + a parametrize case.

🟢-E The headline "busy AND wedged" test was INSENSITIVE to the `- age` term it
   advertises: EXEC was monkeypatched to 0.2 so the no-age budget (2.2s) sat under
   its own 5.0s bound, and deleting `- age` left it green. Recalibrated (EXEC 3.0 /
   GRACE 0.5 / age ~4s) so correct=0.3s vs no-age=3.5s with the bound between them,
   and cmd_timeout lowered below _req's 10s urlopen timeout so the `return timeout`
   mutant now dies on THIS test's assertion instead of a transport error.

🟢-D `fast_timeout` is clamped into [.., cmd_timeout] before use — both are
   operator-settable and nothing validates the relation, so fast=60/cmd=20 escaped
   the "never above cmd_timeout" claim through the lower clamp (measured: 60.0).

🟢-F/G/H Comment above the ping-504 branch still described a "default 10s SHORT
   deadline"; the inflight-release test claimed three exits but had two blocks
   describing the same case (it now covers success / abandoned-while-queued /
   abandoned-while-running / late-result-release); and the "a busy profile is never
   reported as dead" claims are now conditional — the derived budget is still capped
   at cmd_timeout, so >cmd_timeout of legitimate serial work can still time a ping
   out.

RESULT_BUDGET_S joins EXEC_OP_BUDGET_S with its own protocol.js drift guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Round 3 addressed (89a0bdd)

🔴-A — merged-tree phantom op

Reproduced independently before fixing (ran the same harvest locally: 'stderr' in opsTrue). Reworded so the dispatch helper is never followed by a bare word.

Also fixed on my side, as asked. A new test runs the same harvest against the CLI and asserts every harvested name is a real op, with an inline harness negative control (the harvester must find the phantom in a string that contains one, or its silence means nothing). Either fix alone closes this; either alone can regress. Verified: 'stderr' in opsFalse.

While writing the guard I tripped the identical trap twice more — my first two warning comments contained the offending phrase and the harvest stayed True. That is why the test exists rather than a comment.

🟡-B — the abandoned-command window

Reproduced exactly: 504 in 2.00 s while the extension was still executing.

Root cause as you described: the submitter giving up doesn't free the extension. Fixed structurally rather than by widening a deadline:

  • abandoned while still QUEUED → dropped at once (it will never run, so the instance really is that much less busy);
  • abandoned while RUNNING → the entry survives the submitter, because the work does;
  • released as soon as the late result arrives (deliver_result), so the instance doesn't stay "busy" for nothing;
  • otherwise expires at INFLIGHT_STALE_S = EXEC + RESULT, so one wedged command can't make ping slow forever.

RESULT_BUDGET_S gets its own protocol.js drift guard, same as EXEC_OP_BUDGET_S.

One clarification worth recording: the staleness window bounds memory, not the busy/wedged verdict. The budget already goes negative at EXEC+GRACE (20 s), so between 20 s and 28 s the ping is fast and the entry is retained. Conflating the two would have made ping slow for a further 8 s — the expiry test now measures all three points.

🟡-C — unknown_op

Added the returned (unknown_op) arm and extended the parametrize. Confirmed your reading: that branch uses neither the failed in the browser: prefix nor an echoed body, so both classifier arms had nothing to read.

🟢-E — you were right, and it was worse than "insensitive"

Both halves confirmed and fixed:

  • Old fixture: EXEC=0.2 → no-age budget 2.2 s < its own 5.0 s bound, so deleting - age left it green.
  • Recalibrated to EXEC=3.0 / GRACE=0.5 / age ≈ 4 s: correct 0.3 s vs no-age 3.5 s, bound at 1.5 between them. MU1 now fails at exactly 3.50 s with the test's own message.
  • cmd_timeout lowered from 25 s to 8 s (below _req's 10 s urlopen bound), so MU2 (return timeout) now fails at 8.00 s on this test's assertion instead of a transport TimeoutError.

One thing your note didn't cover that I hit: ping_timeout is resolved once in make_handler, so patching the env or PING_TIMEOUT_DEFAULT at request time is inert and the test would have silently measured the 2 s default. It is now passed explicitly to _serve.

🟢-D / 🟢-F / 🟢-G / 🟢-H

  • D fast_timeout clamped into [.., cmd_timeout] before use, so the claim holds unconditionally. Unit case 7 pins both the idle and the busy path (the lower clamp was the escape route).
  • F comment above the 504 branch rewritten — the deadline is conditional, and the default is 2 s.
  • G the release test now covers four genuinely distinct cases (success / abandoned-while-queued / abandoned-while-running / late-result-release) instead of claiming three and testing one twice.
  • H both "never reported as dead" claims are now conditional, naming the N ≥ 2 case and the cmd_timeout cap.

Red → green (base a0a0021)

test base HEAD kind
test_ping_still_waits_after_the_SUBMITTER_gave_up_on_a_running_command RED (504 @ 2.00 s) green regression (🟡-B)
test_inflight_release_distinguishes_queued_from_running_on_abandon RED green regression (🟡-B, 🟢-G)
test_a_kept_inflight_entry_expires_if_the_result_never_arrives RED green regression (🟡-B bound)
test_result_budget_matches_the_extension RED green drift guard
test_effective_timeout_gate_unit (cases 7) RED green regression (🟢-D)
test_a_failing_wake_never_swallows_the_primary_result[op_error-unknown_op-*] RED (wake_failed) green regression (🟡-C)
test_no_prose_line_looks_like_a_wire_op_dispatch RED (['stderr']) green regression (🔴-A)
test_ping_fast_fails_even_while_BUSY_... green green test-hardening, not a code fix — its evidence is MU1/MU2, not a base-red

8 red at a0a0021; no pre-existing test red.

Mutations — each red with THAT guard's own error

# mutation result
MU1 drop the - age term RED at 3.50 s with the test's own message — the mutant that used to survive
MU2 gate always returns cmd_timeout RED at 8.00 s on this test's assertion, not a transport error
MU3 always pop inflight on abandon RED ×2 — "told the extension is dead after 2.00s", "the entry must survive"
MU4 late result no longer releases RED — "must release the kept entry, not leave it to expire"
MU5 disable the staleness prune RED — "a stale entry must be pruned, not just ignored"
MU6 remove the 🟢-D clamp RED — "idle: fast_timeout must be capped at cmd_timeout"
MU7 remove the unknown_op arm RED ×2 — 'wake_failed' == 'unknown_op'
MU8 restore the phantom prose RED — "prose in the CLI reads as a wire-op dispatch: ['stderr']"

Counts

  • pytest 419 passed, 0 failed (was 413), under -W error::pytest.PytestUnhandledThreadExceptionWarning. Run under nix-shell -p python312Packages.pytest; count is plausible (+6 = 4 new tests + 2 new parametrize cases).
  • node tests 454 / pass 454 / fail 0 / skipped 0; test timed out grep → 0.
  • Clean tree at run time. SKILL.md untouched: 12,010 B — no bytes added, per your 39 B merged-tree note.

Live

Laptop .155 / personal / ext 0.7.0 / deployed server f5fbe82. Healthy pingpong; --help intact at 16,152 B; the phantom-op harvest verified clean against the real file. The gate and the abandon window are server.py, so not live-verified — they need a switch + systemctl --user restart browser-bridge.

Untouched: #271, and the SKILL.md merge resolution.

…ll awaiting

🟡-1 (round-4 audit). INFLIGHT_STALE_S is measured from ENQUEUE, but a queued
command's EXEC_OP_BUDGET_S does not start until the SERIAL extension dequeues it.
So age alone cannot mean "a healthy extension has certainly answered" — and it
fails precisely at N>=2, the case the `len(inst.inflight) * EXEC_OP_BUDGET_S` term
exists to model.

Measured with the injected clock, _effective_timeout_locked(inst, cmd_timeout, 2.0):

  N=3, cmd_timeout=60:  age 27s -> 29.0s ;  age 29s -> 2.0s   (should be 27.0s)
  N=2, cmd_timeout=20:  age 27s -> 11.0s ;  age 29s -> 2.0s   (should be  9.0s)

Scenario: an operator follows the advice THIS BRANCH added at
BROWSER_BRIDGE_CMD_TIMEOUT ("raise CMD_TIMEOUT if that is your workload"), sets
60s and submits 3 ops. At t=29s all three entries are pruned while their submitters
are still blocked, the instance reads IDLE, and `ping` fast-fails at 2s with ~25s
of legitimate work left. A regression versus a0a0021 in that window, and it
falsified the remediation the same commit documented.

Fix: prune requires BOTH age > INFLIGHT_STALE_S AND no live submitter
(`cid not in inst.waiters`). The memory bound is not weakened, it is split by
owner: a live-submitter entry is bounded by that submitter's own deadline (it
unwinds through the finally, which pops it); an abandoned entry has no one to bound
it, which is what INFLIGHT_STALE_S is for.

`waiters` was verified to be exactly the live-submitter set by reading every
mutation, not by assumption — and submit()'s finally now discards it too, so an
unexpected raise inside the wait loop cannot strand an entry into permanent
prune-exemption. A new test pins the property on both reachable exits.

🟢-2 The N=1 expiry test claimed "the staleness window bounds memory, not the
verdict". True at N=1 ONLY — it holds there because a lone entry's budget is
already negative at 20s, so pruning at 28s is verdict-neutral by construction. The
docstring now names that scope and points at the N>=2 test.

🟢-3 The abandon-late-result branch popped `inflight` but left `last_dispatch`
naming that cid, whose contract is "the command it never answered" and which is
surfaced by health/whoami. After that path it DID answer. Cleared, on the same
condition the normal path uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Round 4 addressed (f301d93)

🟡-1 — taken now, and you were right that it falsified its own advice

Reproduced first with the injected clock, before changing anything:

N=3, cmd_timeout=60:  age 27s -> 29.0s ;  age 29s -> 2.0s   (should be 27.0)
N=2, cmd_timeout=20:  age 27s -> 11.0s ;  age 29s -> 2.0s   (should be  9.0)

Your diagnosis holds exactly: INFLIGHT_STALE_S is measured from enqueue, but a queued command's execution budget doesn't start until the serial extension dequeues it — so age alone can't mean "certainly answered", and it breaks precisely at N≥2, the case the len(inst.inflight) * EXEC_OP_BUDGET_S term exists to model. The worst part is the one you named: the operator gets there by following the raise CMD_TIMEOUT advice this same branch added.

I verified waiters myself before relying on it rather than taking your word or the auditor's. Every mutation enumerated: added at enqueue (:1388), discarded on all three exits of the wait loop (:1404 supersede, :1408 timeout, :1424 result). That makes cid in waiters ⟺ a submitter is currently blocked.

One gap I found while checking, which I closed rather than relying on: an unexpected raise inside the wait loop (e.g. from _cond.wait) would exit through the finally without discarding, stranding a waiter — and under the new rule a stranded waiter means permanent prune-exemption, i.e. the memory bound silently disabled. submit()'s finally now discards waiters too (idempotent, no-op on every normal path), so the premise is structurally true rather than incidentally true. A new test pins the property on both externally reachable exits.

Adopted your one-liner. The memory bound is not weakened, it is split by owner — live-submitter entries are bounded by that submitter's own deadline (it unwinds through the finally, which pops the entry); abandoned entries have no one to bound them, which is exactly what INFLIGHT_STALE_S is for. Both directions are now pinned (MV1/MV2 below).

🟢-2 / 🟢-3

  • 🟢-2 Scope named. The N=1 test now says the "bounds memory, not the verdict" conclusion holds at N=1 only — and says why it is verdict-neutral there by construction (a lone entry's budget is already negative at 20 s) — and points at the N≥2 test. You're right that this is the same shape we keep hitting.
  • 🟢-3 last_dispatch cleared on the abandon-late-result branch, on the same condition the normal path uses, with an assertion added to the existing test.

Red → green (base 89a0bdd)

test base HEAD kind
test_stale_prune_never_evicts_an_entry_whose_submitter_is_STILL_WAITING RED (2.0 == 27.0) green regression (🟡-1)
test_inflight_release_distinguishes_queued_from_running_on_abandon RED (last_dispatch still names…) green regression (🟢-3)
test_waiters_is_exactly_the_live_submitter_set green green INVARIANT GUARD — pins the premise the prune now reads; not regression coverage
test_a_kept_inflight_entry_expires_if_the_result_never_arrives green green unchanged behaviour; docstring scoped (🟢-2)

Mutations

# mutation result
MV1 drop the c not in inst.waiters condition (restore the bug) RED — "three live submitters were pruned as stale, so the instance read idle while the extension was legitimately working"
MV2 drop the age condition, keep only waiters RED ×2 — the abandoned entry is pruned instantly, breaking both 🟡-B's test and the expiry bound. Proves both conditions necessary, not just the new one.
MV3 remove the last_dispatch clear on the abandon path RED — "last_dispatch still names a command that has now been answered"

Disclosed, not papered over: the finally's extra waiters.discard is a defensive net for a raise inside the wait loop that no test can reach from outside. It is an invariant guard with no mutation coverage, labelled as such in both the source and the test docstring. I'm not claiming it's covered.

Counts

  • pytest 421 passed, 0 failed (was 419; +2 new tests), under -W error::pytest.PytestUnhandledThreadExceptionWarning, via nix-shell -p python312Packages.pytest. Count plausible: +2.
  • node tests 454 / pass 454 / fail 0 / skipped 0 using the glob form — thanks for the harness note; I hit that exact false red (tests 1 / fail 1, MODULE_NOT_FOUND) early in this task and have been using the glob since.
  • Clean tree at run time. SKILL.md untouched: 12,010 B.

Noted, no action

🟢-4 — agreed on all three points, including that my copy is strictly stricter on same-line cases (a visible false alarm, not a silent hole) and that the newline-spanning \s case is covered by #277's rig and not mine. Recorded rather than chased; my guard's job is CLI prose, not policing #277's masker.

Live: healthy pingpong, ext 0.7.0. The gate and prune are server.py, so not live-verified — they need a switch + systemctl --user restart browser-bridge. #271 and the SKILL.md resolution untouched.

@ZacxDev
ZacxDev merged commit a0a5d73 into main Aug 2, 2026
@ZacxDev
ZacxDev deleted the fix/browser-cli-audit-2026-08-02 branch August 2, 2026 08:25
ZacxDev added a commit that referenced this pull request Aug 2, 2026
Hardens the CLI dispatch parser in test_surface_parity.py. It matched
`\bcmd_op\s+([A-Za-z]+)` on any line whose first non-space character was
not `#`, so any prose containing the phrase `cmd_op <word>` was harvested
as a wire op.

MEASURED on the merged tree of this PR + #278 (tip a0a0021): #278 added a
Python docstring inside a `python3 -c` block reading "The machine-readable
cause, from `cmd_op stderr` it already emits" — and the parser harvested a
phantom op named `stderr`. Merged-tree pytest went 1 failed / 424 passed,
with a diagnostic pointing at a wire op that does not exist. Invisible on
either branch alone. Fixed here as a CLASS, not by special-casing the word.

The rule now requires TWO independent conditions:

  1. the occurrence survives `mask_shell_noncode()` — a small shell lexer
     that blanks comments, single-/double-quoted strings (including
     MULTI-LINE ones), backquoted spans and heredoc bodies, preserving byte
     offsets. `$( … )` is deliberately NOT masked even inside double
     quotes: that is genuine command position.
  2. it sits in command position — start of statement, or after
     `;` `&&` `||` `|` `(` `$(` `then` `do` `else`.

New fixture tests/fixtures/cmd_op_parse_rig.sh pins 8 dispatch shapes that
MUST be harvested and 11 mention shapes that MUST NOT be.

RED-FIRST (the whole point): against the rig, the previous parser harvested
7 phantoms — phantombacktick, phantomdocstring, phantomdq, phantomheredoc,
phantommultilinedq, phantomquotedheredoc, phantomsq — while keeping all 8
real ones. Reverting the parser (mutation P1) reproduces exactly that.

Over-tightening controls, because a permanently-green no-op is the obvious
wrong fix:
  - the rig set must match EXACTLY (misses are named, not just leaks);
  - the real CLI must still yield all 19 ops == server.py's inventory;
  - `screenshot` is pinned by name — it is dispatched from
    `resp="$(cmd_op screenshot "$full")"`, a command substitution nested in
    double quotes. An earlier version of the masker blanked the `$(` and
    lost exactly that one op, 19 -> 18, while every other test stayed
    green. Caught by this control, not by review.
  - a new test pins that the CLI contains no LIVE backtick substitution,
    since backquoted spans are masked conservatively (113 backticks, all
    inside comments — MEASURED).

Mutation sweep, 10 mutations, all red, each naming its specific phantom or
missing op. Two SURVIVED the first pass and are the reason the rig grew two
more cases:
  - P5 (drop command-position anchoring) was green — every phantom was
    already being caught by masking. Now caught by an UNQUOTED bareword
    mention (`echo usage: cmd_op phantombareword`).
  - P6 (drop single-quote masking) was green for the WRONG reason: the
    `"""` in the docstring case toggled double-quote state, so a different
    defence was catching it. Now caught by a single-quoted `python3 -c`
    block containing no double quotes.
Also re-verified P7, the M14-style "new wire op with no CLI subcommand"
case, still goes red naming `sniff`.

Verified against the real #278 shape: injecting that exact docstring into a
throwaway COPY of `browser` (the real one is owned by #278 and untouched)
leaves the op set at 19 -> 19 and does not harvest `stderr`.

Counts (from reporter lines, not exit codes; pytest 9.0.2 confirmed
importable — a bare `python3 -m pytest` on this host exits 0 printing
"No module named pytest"):
  pytest 381 -> 385 passed, 0 failed
  node   460 tests, 459 pass, 1 fail — unchanged, still the deliberate
         AGENT SURFACE PARTITION finding awaiting the operator's ruling on
         context/emulate/ping. Not pre-empted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…ry (contains 1 DELIBERATE failure) (#277)

* test(browser-bridge): anchor the agent surface to the real op inventory

Closes the structural gap the 2026-08-02 surface audit called its most
important non-token finding (F9/V3, G1, G2).

The four-source agent parity test pinned browser.js's typed enum, the
agent-md capability table, the README contract and ALLOWED_OPS_DEFAULT to
EACH OTHER and to nothing upstream. Because all four omit `context`, `ping`
and `emulate` identically, the set was self-consistent, CI was green, and
the omission was invisible. Structurally the same defect as `context` being
dead on main (in protocol.js, absent from server.py's ALLOWED_OPS), one
layer further out - at the surface the agent actually touches.

tests/test_server.py::test_ping_op_set_mirrors_the_extension_protocol_js
already does this correctly for the wire layer: it PARSES protocol.js and
asserts set-equality. Same approach here, at two more layers.

browser_tool.test.mjs (+6 tests)
  - the existing four-source parity test now names WHICH source is missing
    WHICH op before the bare deepEqual (assertions added, none removed)
  - UPSTREAM ANCHOR: every agent-facing op name must resolve through
    OP_TO_SERVER to an op server.py actually allows
  - AGENT SURFACE PARTITION: every wire op must be REACHABLE by the agent
    or a DECLARED exclusion in REVIEWED_AGENT_EXCLUSIONS, each carrying a
    written reason AND a `source` snippet asserted to exist verbatim in
    browser_tool_impl.mjs - so an exclusion cannot be padded with an
    invented rationale, and cannot outlive the comment it points at
  - two HARNESS SELF-CHECK tests: plausible non-empty cardinality, and the
    parser failing loudly on an unreadable/wrong-shaped source

tests/test_surface_parity.py (new, +12 tests, IS in the flake gate)
  - CLI SUBCOMMANDS <-> op inventory. Not set-equality: 24 CLI names vs 18
    wire ops + 1 server op. Every name is classified wire-op / server-op /
    alias / client-only, the classification is asserted EXHAUSTIVE and a
    partition, and it is anchored to the CLI's real `cmd_op <op>` call
    sites so the table cannot claim a mapping the script does not make.
  - SKILL.md ops table <-> SUBCOMMANDS, both directions, plus
    server.py -> SKILL.md to close the triangle.
  - parser negative controls: missing file and wrong-shaped file must
    RAISE, never return an empty set (an empty set makes every parity
    assertion pass vacuously).

ONE TEST FAILS ON PURPOSE - that failure IS the finding:

  AGENT SURFACE PARTITION: UNDECLARED EXCLUSION(S): context, emulate, ping

Those three wire ops are unreachable by the autonomous agent (absent from
OP_TO_SERVER, so not even BROWSER_AGENT_ALLOWED_OPS re-enables them) and
carry no written rationale, unlike `open`/`close`/`tabs`/`activate`, which
all do. Whether each is a deliberate exclusion is an operator decision, not
one this test may make. Do NOT resolve it by tuning the test.

Counts (from the reporter lines, not exit codes):
  node   454 pass / 0 fail  ->  460 tests, 459 pass, 1 fail (the finding)
  pytest 369 passed         ->  381 passed, 0 failed

Mutation sweep: 25 mutations across delete / rename / add / alias-retarget /
unclassified-name / fabricated-rationale / harness-sabotage. All 25 went
red, each naming the specific op and source; verified red for its OWN
assertion, not an earlier one. Notably a fabricated exclusion rationale
does NOT silence the failure - it fails with "cited rationale is NOT
present in browser_tool_impl.mjs".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(browser-bridge): a `cmd_op` MENTION is not a dispatch

Hardens the CLI dispatch parser in test_surface_parity.py. It matched
`\bcmd_op\s+([A-Za-z]+)` on any line whose first non-space character was
not `#`, so any prose containing the phrase `cmd_op <word>` was harvested
as a wire op.

MEASURED on the merged tree of this PR + #278 (tip a0a0021): #278 added a
Python docstring inside a `python3 -c` block reading "The machine-readable
cause, from `cmd_op stderr` it already emits" — and the parser harvested a
phantom op named `stderr`. Merged-tree pytest went 1 failed / 424 passed,
with a diagnostic pointing at a wire op that does not exist. Invisible on
either branch alone. Fixed here as a CLASS, not by special-casing the word.

The rule now requires TWO independent conditions:

  1. the occurrence survives `mask_shell_noncode()` — a small shell lexer
     that blanks comments, single-/double-quoted strings (including
     MULTI-LINE ones), backquoted spans and heredoc bodies, preserving byte
     offsets. `$( … )` is deliberately NOT masked even inside double
     quotes: that is genuine command position.
  2. it sits in command position — start of statement, or after
     `;` `&&` `||` `|` `(` `$(` `then` `do` `else`.

New fixture tests/fixtures/cmd_op_parse_rig.sh pins 8 dispatch shapes that
MUST be harvested and 11 mention shapes that MUST NOT be.

RED-FIRST (the whole point): against the rig, the previous parser harvested
7 phantoms — phantombacktick, phantomdocstring, phantomdq, phantomheredoc,
phantommultilinedq, phantomquotedheredoc, phantomsq — while keeping all 8
real ones. Reverting the parser (mutation P1) reproduces exactly that.

Over-tightening controls, because a permanently-green no-op is the obvious
wrong fix:
  - the rig set must match EXACTLY (misses are named, not just leaks);
  - the real CLI must still yield all 19 ops == server.py's inventory;
  - `screenshot` is pinned by name — it is dispatched from
    `resp="$(cmd_op screenshot "$full")"`, a command substitution nested in
    double quotes. An earlier version of the masker blanked the `$(` and
    lost exactly that one op, 19 -> 18, while every other test stayed
    green. Caught by this control, not by review.
  - a new test pins that the CLI contains no LIVE backtick substitution,
    since backquoted spans are masked conservatively (113 backticks, all
    inside comments — MEASURED).

Mutation sweep, 10 mutations, all red, each naming its specific phantom or
missing op. Two SURVIVED the first pass and are the reason the rig grew two
more cases:
  - P5 (drop command-position anchoring) was green — every phantom was
    already being caught by masking. Now caught by an UNQUOTED bareword
    mention (`echo usage: cmd_op phantombareword`).
  - P6 (drop single-quote masking) was green for the WRONG reason: the
    `"""` in the docstring case toggled double-quote state, so a different
    defence was catching it. Now caught by a single-quoted `python3 -c`
    block containing no double quotes.
Also re-verified P7, the M14-style "new wire op with no CLI subcommand"
case, still goes red naming `sniff`.

Verified against the real #278 shape: injecting that exact docstring into a
throwaway COPY of `browser` (the real one is owned by #278 and untouched)
leaves the op set at 19 -> 19 and does not harvest `stderr`.

Counts (from reporter lines, not exit codes; pytest 9.0.2 confirmed
importable — a bare `python3 -m pytest` on this host exits 0 printing
"No module named pytest"):
  pytest 381 -> 385 passed, 0 failed
  node   460 tests, 459 pass, 1 fail — unchanged, still the deliberate
         AGENT SURFACE PARTITION finding awaiting the operator's ruling on
         context/emulate/ping. Not pre-empted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(browser-agent): make `context` reachable; DECLARE `ping` + `emulate` excluded

Resolves the deliberate failing test from #277 (AGENT SURFACE PARTITION), which
reported three wire ops that were neither agent-REACHABLE nor DECLARED
exclusions.

- `context` -> REACHABLE. Its absence was provably an oversight, not a
  decision: browser_tool_impl.mjs was last edited 2026-07-31 (#243, 1e2ad9e) and
  `context` only reached server.py's ALLOWED_OPS on 2026-08-01 (#263, 55ad035),
  so the agent-surface mapping has never been touched since the op existed. It
  is a cheap read of page state (url/domain/path/searchParams/title/tabId), no
  DOM content, strictly less powerful than the `text`/`html` the agent already
  has. Added to OP_TO_SERVER, ALLOWED_OPS_DEFAULT and the other three
  agent-facing sources the parity test holds in lockstep (browser.js enum,
  browser-agent.md capability table, README op contract), plus a field-pinned
  summarizeResult branch so a later server-side payload addition cannot silently
  widen what reaches the model.
- `ping` -> DECLARED EXCLUDED. Operator diagnostic for extension staleness; the
  model cannot act on the answer (cannot reload an extension or restart Brave)
  and it reads no page state.
- `emulate` -> DECLARED EXCLUDED. Mutates the tab and leaves STICKY per-tab
  state (device metrics, UA-CH, media overrides) that outlives the op until an
  explicit reset, so an agent that emulates and never resets hands back a
  silently altered tab.

Both exclusions carry the rationale comment in browser_tool_impl.mjs that
REVIEWED_AGENT_EXCLUSIONS asserts verbatim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…s (2026-08-02) (#286)

Covers the 2026-08-01 handoff's three open items taken through to
merged-deployed-verified: 11 devrc PRs, homelab-infra#274 + clawgate 0.7.82,
and issue #273 opened/probed/closed on measurement.

The four durable lessons, all measured:

- "Merged != deployed" fired twice in OPPOSITE directions. The kubeclaw chart
  merged to trunk and sat inert (go:embed behind a literal image pin, no Flux
  image automation). The workbench ran OLD code behind a green ship.sh, because
  an orphaned non-systemd process from the previous day held port 8788 while the
  unit crash-looped. Check the consumer is running your artifact.

- Twelve false-signal harnesses, and a rule already in RULES.md did not
  inoculate against its own class: `diff` unified output made `^>`/`^<` greps
  report "0 lines differ" for files differing by 1,445 bytes. cmp settled it.

- A count of DECLARATIONS is not a count of INSTANCES: two skipif decorators
  were reported as "2 skips" and actually gated 123 tests. 60x, and it was the
  difference between a nit and the session's most valuable fix.

- The merged-tree gate earned itself: a docstring in #278 containing the phrase
  `cmd_op stderr` made #277's parser harvest a phantom wire op. Red only on the
  merged tree, invisible on either branch alone.

Both hosts are at 81e2d76; main has since moved to 9254361 (other sessions), so
the next session should re-run ship.sh. A skills/docs update PR was dispatched
at end of session and should be checked for landing.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant