mahbot 0.7.3

An autonomous agentic engineering system that manages software development through role separation, subagents, and deterministic diagnostics.
Chrome browser automation: navigate web pages, interact with elements, extract content, and capture screenshots for visual inspection. Returns an accessibility-tree snapshot for AI consumption.

## Required parameter
`tab` — a logical session name. Missing or empty uses a per-run session whose tabs the product closes itself when your run ends — retried, unprompted, until the browser says they are gone, and the same for the leftovers of runs that ended without their own cleanup. A run keeps at most five tabs: when a new one appears and the run already holds five, the tab you have addressed least recently is closed — a close the browser does not confirm is retried at the next new tab, so the bound is not hard. Addressing a tab the limit closed is an error naming it; `open` a URL under that name to get the tab back.

Reporting never happens once per attempt: a close that could not reach the browser is simply retried, and each thing the product can state — a tab it cannot close, or a browser side whose answers it cannot decide from — is stated at most once.

What it states is reported on the product's Issues view rather than left as a cleanup for you or the owner — a tab the extension no longer holds as its own (e.g. one created before the extension was updated or re-installed), and, as a single fact about the host rather than a run's leftover, a browser side that is disabled, too old for the ownership door, or answering in a shape the product cannot decide from. A group renamed in the browser is left alone rather than guessed at.

Sessions are mapped to a private per-run namespace derived from the run's identity, so a name here never addresses another run's session — and a run resumed under that identity re-attaches the names it was working with. Use an explicit different name (e.g. "docs") only to keep multiple pages open simultaneously. Same tab = serialized operations on that page.

## Workflow
### 1. Navigate: `open <url>`
Opens a page and automatically returns a compact accessibility snapshot. Read this snapshot carefully — it shows the page structure, interactive elements, and element refs (like `@e1`, `@e2`) that you can use in subsequent commands.

### 2. Understand the page: `snapshot`
Re-scan the page to get fresh element refs. Pass `compact: false` to include empty structural elements, `interactive_only: true` to see only buttons/links/inputs, or `depth: N` to limit tree depth (useful for deeply nested pages).

### 3. Interact: `find`, `click`, `fill`, `type`, `press`
Use `find` with locators to click, fill, hover, or check elements. Use `click` with a CSS selector or ref (`@e1`) for simple clicks. Always take a fresh `snapshot` after navigation before using refs — they become stale on any DOM change.

Text input — never set `.value` via eval; use the native actions (they route through React/Vue/Angular setters and rich-editor APIs like CodeMirror/Monaco/ProseMirror):
* `fill { selector: "input#email", text: "user@example.com" }` — clear the field and write the text; the written value is read back and verified, so success means the field really holds it. The default choice, including multiline/large text.
* `type { selector: "#search", text: "hello", key_events: true }` — type character-by-character WITHOUT clearing (appends to existing content). `key_events: true` sends real per-character keyDown/keyUp for autocomplete/combobox fields. Embedded newlines press Enter (which may submit a form) — use `fill` for multiline text. A ⚠ warning in the output means the page rewrote or filtered the typed text.
* `press { key: "Enter", selector: "textarea[name=q]" }` — press a key at the focused element; `selector` tries to focus the element first (use it when focus may have moved), but focus moves only if it is focusable (input, textarea, button, ...) — for a non-focusable selector (e.g. `body`) focus does not move and the key lands wherever focus currently is. `data.target` shows where the key actually landed (may differ from `selector`, may be absent). Press Enter after filling to submit forms. A ⚠ warning means the key provably had no effect — no key listeners for a listener-dependent key (Enter on a bare input, Escape, arrows in text fields; keys with browser defaults like Enter on a button are never probed), an Enter that landed where nothing is focused, or the key going to an iframe boundary — click the target instead.

### 4. Extract content: `get_text`, `get_innertext`, `eval`, or `snapshot`
* `get_text { selector: "body" }` — uses DOM `textContent`, so returns ALL descendant text including content inside `<script>` and `<style>` elements. For visible text only (no script/style), use `get_innertext` or `eval` with `document.querySelector('body').innerText`.
* `get_innertext { selector: "body" }` — uses `innerText()`, returns only visible rendered text. No `<script>`, `<style>`, or hidden content.
* `get_text` or `get_innertext` with a specific selector — extract text from one element.
* `snapshot` — shows page structure (accessibility tree), not full text content. It is naturally compact; use `get_text` for detailed content extraction.
* `eval { js: "..." }` — run arbitrary JavaScript to inspect or extract data. Returns the result serialized as a string. See `## JS eval notes` below.
* `snapshot { compact: false, depth: 5 }` — more detailed accessibility tree.

### 5. Visually inspect the rendered page: `screenshot`
Capture a screenshot of the current tab and inject it into your conversation as a native image, so you can see exactly how the page is rendered. The screenshot is for your own analysis — you do not need to echo it or reference its path. Use it after `open`/`find`/`click` when visual layout, styling, or rendered state matters more than the accessibility tree.

### 6. Wait for a condition: `wait`
* `wait { selector: "#results" }` — wait until a CSS selector matches something.
* `wait { url: "pattern" }` — wait until the URL matches a pattern.
* `wait { text: "Loaded" }` — wait until this text appears in the page.
Give exactly ONE target. A wait declares ~10 s to chrome-use (`expect` declares ~20 s) — a timeout is reported as an error, never an infinite block.

### 7. Assert a condition: `expect`
A bounded PASS/FAIL assertion — prefer it over open-and-eyeball when you need a definite answer:
* `{ condition: "visible", selector: "#main" }` — also "hidden", "present".
* `{ condition: "count", selector: ".card", op: "==", count: 5 }` — op: == != > < >= <=.
* `{ condition: "text", selector: "h1", predicate: "contains", expected: "Hello" }` — predicate: equals | contains | matches.
* `{ condition: "value", selector: "input", expected: "..." }` — input value.
* `{ condition: "attr", selector: "a", name: "href", predicate: "contains", expected: "/docs" }`.
* `{ condition: "url", predicate: "contains", expected: "dashboard" }`.
PASS means the condition held within the deadline; FAIL reports the observed `actual` and whether it timed out. A FAIL is information, not a retry prompt.

### 8. Structured extraction: `extract`
`extract { schema: {...}, limit: N }` — schema-driven row extraction. The schema has an optional `"rows"` CSS selector key for row lists and a required `"fields"` object. Each field maps to a CSS selector string, or to an object for non-text getters: `{"url": {"sel": "a.link", "get": "@href"}}`. Getter is `"text"` (default), `"html"`, `"value"`, or `"@<attribute>"` — the `"@"` prefix is REQUIRED for attributes (get `"@href"`, not `"href"`); inside an extract schema `"@"` always introduces an attribute, unrelated to the `@e1` element refs other actions use. Unknown getters are rejected with a corrective error rather than silently returning text. Example: `{"rows": ".card", "fields": {"title": ".title", "price": ".price"}}`. An empty region is reported honestly (0 rows) instead of returning phantom rows; a failing rows-selector count (e.g. invalid CSS) is an error. `limit` trims the returned rows while `total` keeps the honest full count.

## JS eval notes
* `const`/`let` declarations are scoped to individual `page.evaluate()` calls — they do NOT cause redeclaration errors across separate `eval` calls.
* `var` declarations and `window.*` assignments DO persist across calls (standard JavaScript behavior).
* For multi-step extraction, use `window.__tmp` namespace or wrap in an IIFE: `(() => { const x = ...; return x; })()`.

## Large content handling
* For files over ~10KB, prefer the `read` tool (local files) or `shell` with `curl` (remote files) — they have no truncation.
* If using chrome for large content, use `eval` with chunked extraction: `document.body.innerText.slice(0, 10000)`.
* `get_text` on raw.githubusercontent.com or CDN URLs returns full content but wrapped in page HTML — prefer `read` or `curl` for raw file content.

## `value` vs `text` (critical distinction)
* `value` = the locator search target (CSS selector, button label text, role name). This is what the tool searches for to *find* the element.
* `text` = text to *fill* into the element. Only used with `action: "fill"`.
* **Do NOT swap them** — they serve different purposes.

## Locator types for `find` (ranked by reliability)

### CSS selector locators (`first`, `last`, `nth`) — most reliable
`value` is a CSS selector. Always matches the DOM directly. For `nth` you must also pass `index` (zero-based).
* Use `"input"`, `"textarea"`, `"form input"` for text inputs — role-based textbox locators are unreliable.
* **Important**: CSS selectors must match the actual DOM attributes. If a link's `href` is `connection/index.html` (relative), use `a[href$='connection/index.html']` — not an absolute path.
* Tag names and attribute values are case-sensitive.

### `by: "text"` — second most reliable
Matches visible DOM text content (case-sensitive substring). Use `exact: true` for exact (non-substring) match.
* Does NOT match `aria-label`, `alt`, `placeholder`, or `title` attributes.
* Only matches text visible in the DOM, not hidden/spanned text.
* **Look at the snapshot output to see what visible text is available** before using this locator.

### `by: "role"` — use with caution
Matches by ARIA role (button, link, heading, etc.). Pass `name` to filter by computed accessible name.
* The `name` filter can fail even when the snapshot shows a matching element. When it fails, fall back to `by: "text"` or `by: "first"` with CSS.
* Avoid for textboxes/inputs — use `by: "first"` with CSS instead.

### `by: "placeholder"` — matches HTML placeholder attribute exactly
The snapshot shows the *accessible name* (from `aria-label`), not the `placeholder` attribute. These are often different. Use `eval` with `document.querySelector('selector').getAttribute('placeholder')` to find the real placeholder value.

### `by: "label"`, `by: "testid"`, `by: "alt"`, `by: "title"` — special-purpose
* `label` — matches `<label for='...'>` elements only (not `aria-label`).
* `testid` — matches `data-testid` attribute.
* `alt` — matches HTML `alt` attribute.
* `title` — matches HTML `title` attribute (exact match).

## Valid `find` actions
`click` (click element), `fill` (clears then types, uses `text` param), `hover` (hover over element), `check` (check checkbox/radio button), `text` (get element text content — does NOT use the `text` param). (`focus`, `type`, and `uncheck` are not supported by the runtime.)

## Keyboard shortcuts
* `press { key: "Enter" }` — submit forms after filling inputs.
* `press { key: "Escape" }` — close modals, dialogs, or search overlays.
* `press { key: "/" }` — open search on sites with keyboard-triggered search overlays (documentation, help centers, etc.).
* `press { key: "Tab" }` — move focus to the next focusable element.
* `press { key: "Control+a" }` — select all text in a focused input.
* `press { key: "ArrowDown" }` / `"ArrowUp"` — navigate lists/dropdowns.

## Debugging when selectors fail
When `find` or `click` can't locate an element, use `eval` to inspect the DOM:
* `eval { js: "document.querySelector('selector')?.outerHTML" }` — see the actual element type, attributes, and text.
* `eval { js: "document.querySelectorAll('selector').length" }` — check how many elements match; maybe the wrong one is being targeted.
* `eval { js: "document.querySelector('a')?.href" }` — check actual link URLs (often relative, not absolute as you might expect).
* `eval { js: "document.querySelector('input')?.getAttribute('placeholder')" }` — read the actual placeholder attribute value.

## Limitations
* Refs (`@e1`, `@e2`, …) from a snapshot become stale after any navigation or DOM change — take a fresh snapshot before using them.
* `open` returns a compact snapshot automatically; use a separate `snapshot` call for more detail or different options.
* Elements outside the viewport may not be interactable. If interaction fails, try a `find` with a CSS selector first, or use `eval` to check position.
* Elements with `tabindex="-1"` may not be clickable via role-based locators but can still be found with `by: "first"` CSS selector.
* The snapshot's accessible names may differ from HTML attributes (`aria-label` vs `placeholder`, etc.) — use `eval` to inspect actual attributes.

## Bounds, leftovers and the real browser

* Every call runs to a clock chrome-use itself works to, and the tool's own kill rides above it (plus chrome-use's relay-recovery window and 2 s slack) — chrome-use always gives up first, so its own reason, never a synthetic timeout, is what you normally see. `wait` (~10 s) and `expect` (~20 s) are the verbs that declare a deadline chrome-use honours in full; every other action, `open`'s navigation included, runs to the clock the product declares to chrome-use — the tool's own client tolerance (45 s) less the product's 2 s margin — because a declaration AT that tolerance makes the tool run out of tolerance instead of answering, which then reads as a wedged session (the session is stopped and its open tabs are lost). A timeout is information: with a healthy daemon it usually means a slow page, so retry once before changing approach.
* `mahbot chrome`, the browser command you can run through the shell, works to the same clocks: its `wait`/`expect` default to 8 s, and its `--timeout` is refused (rc 3) at or above chrome-use's 45 s tolerance on the verbs that forward a deadline (`wait`, `expect`, `open` — for `open` the declared deadline is what remains of that flag when the `--expect` wait starts, so a value just under the tolerance still declares less). On the verbs chrome-use takes no per-call deadline for (`count`, `eval`, `extract`, `click`, `fill`, `type`, `press`) the declared clock is the smallest accepted `--timeout` and changes nothing; a value above it raises only mahbot's own kill — the bound `timeout_ms` reports — and anything below it is refused as a usage error rather than silently ignored. Its `--session` names are validated rather than reshaped: a name takes letters, digits, '-' or '_' only (chrome-use's own alphabet — a '.' is refused, because chrome-use will not stop a session whose name has one), and anything else is refused (rc 3) — unlike this tool's `tab`, which is sanitized into a per-run session.
* A leftover note reports chrome-use's own helper still holding the call's output channel. A FINISHED call carries it — its exit status and output are the result — and a FAILING call carries it too: the failure is the failure, and the leftover is a separate process chrome-use deliberately leaves running. The helper is named, left RUNNING, and stopped with the `session stop` that addresses it; no further output from it is collected — mahbot holds at most the 16 most recent such channels, releasing the oldest first, so a helper that writes after its channel is released can end on the broken pipe. The helper is chrome-use's own and also stops itself on chrome-use's idle timeout.
* The tool only drives the owner's real, logged-in browser: before an action it establishes that a real browser is reachable through the relay, and recovers the connection if it is not. A call that ran in a browser chrome-use launched itself is a plain FAILURE, never a quiet success — retry once the connection is back, and read it as an environment fault, not as page state.
* A session that stopped answering is recovered automatically on every path (this tool, the `mahbot chrome` CLI, the watchdog) — you do not stop and recreate a wedged session yourself.

## chrome-use troubleshooting

When the `chrome` tool fails, its error names the classified cause plus what auto-recovery does on its own. The chrome-use binary lives where its own installer puts it — on macOS and Linux the system-wide programs directory when that directory holds a copy or can be written, otherwise the owner's own per-user programs directory; on Windows the helper's own per-user programs directory under `%LOCALAPPDATA%`, with no system-wide step — and mahbot replaces whatever sits there with the newest release on every start of the product. A copy in a directory mahbot cannot write is the copy the agents use, and mahbot records that it could not be brought up to date rather than working around it; failures are non-fatal and retried on the next start. There is no install tool, and the product always runs its own copy itself rather than the one a shell's search path happens to resolve — diagnose and point at fixes, never claim you reinstalled the CLI.

- **NotInstalled** — the chrome-use extension or native host is missing: enable the extension at `chrome://extensions`. A missing CLI is installed again on the next start.
- **HostBroken** — the native host launcher is broken: run `chrome-use doctor` on the copy the product installed, spelling its full path out when the bare name does not resolve on this shell, or check the logs for the last install error.
- **ExtensionDisabled** — the extension is disabled: enable it at `chrome://extensions`.
- **ExtensionAbsent** — the extension is not installed in Chrome: install it from the Chrome Web Store (listing `chrome-use`, ID `knfcmbamhjmaonkfnjhldjedeobeafmk`; `ab-connect` is its internal codename, seen in runtime messages — same product, don't search the Store for "ab-connect").
- **RelayDown** — Chrome is running but the extension is not republishing: check the extension is enabled and installed in Chrome's DEFAULT profile (a non-default profile won't connect).
- **ChromeNotRunning** — mahbot auto-launches Chrome; if that attempt fails, the user starts Chrome manually.
- **UnreachableTab** — the extension lost its debugger attach to the tab this session was driving and never re-attaches `about:` tabs, so this session cannot drive that tab again. The tab belongs to your session and is the product's to close (at run end, and when a wedged session is recovered) — never close it by hand; move on with a different `tab` name if you need a working page.
- **DaemonWedge** — the chrome daemon is down/unresponsive: auto-recovery restarts it, no user action needed.
- **CLI missing or broken** — the binary is installed on every start of the product, so a broken install only self-heals on the next start: check the logs for the install error and let it retry, or offer the user manual control via the installed chrome-use CLI.

A Chrome-side problem (extension disabled/absent) is not something a daemon restart fixes — the extension itself has to be enabled at `chrome://extensions`, which is outside this tool's reach; the product reports it, and it is never repaired from here either way (closing tabs is not yours to do). Right after an update the extension version may briefly skew from the CLI's; if the relay stays down just after an update, advise reloading the extension at `chrome://extensions`. The connection is Chrome native messaging — no debug port, no "Allow remote debugging?" popup.