Skip to main content
cli
Released October 7, 2026
Full-page screenshots. browser screenshot --full-page captures the whole scrollable page rather than the viewport, and capture --screenshot saves a full-page PNG beside each capture as <stamp>.snap.png (removed by capture prune along with its capture), so a view set keeps a picture of every state it recorded.Resolve privacy and watch from every component whose selector matches an element, as SEP-0009 and SEP-0015 now state, instead of from whichever component names it. A broader component naming an element (a Field child naming every input in a form) no longer discards a narrower component’s block, a view component sharing a global’s name no longer drops the global’s privacy or watch, two declarations on one element resolve to the strictest, and block is absolute: an unmask reopens a mask, never a block.ComponentMatch.Watched lists every watched component matching a node, and ComponentMatch.Watch reports whether any does. New on match.Matcher: Privacy and Watched resolve every node of a tree, including nodes no component names. match.Withholds is exported, so a consumer withholding properties it extracted itself applies the matcher’s rule.Selectors match exactly what a browser matches. Every test in a compound now applies instead of only the last: a repeated attribute name ([class*="a"][class*="b"]), a repeated #id (#a#a matches, #a#b matches nothing), and a second :is()/:where() (:is(.a):is(.b)). The parser rejects a type selector or * after other tokens ([x]div), which CSS does not allow. Matching compares the values of the attributes HTML lists as case-insensitive (type, rel, target, lang and the rest) ASCII case-insensitively, so input[type=password] matches type="PASSWORD"; in an HTML document that list applies to every element, SVG and MathML included. Type names fold with ASCII rules only, and a ~= value that is empty or contains whitespace never matches.
cli
Released October 6, 2026
File-root definitions: (SEP-0019, draft): shared components that are $ref targets but never matched on their own. A view gets a definition only where it references one, scoped by the reference, so a structure whose selectors are only safe in context (a card’s img, h3, button) can be shared across views without becoming a global that claims every page. The loader, validate, lint, stats and the unknown-field check all know the key; a global wins a name clash (definition-shadowed-by-global), and a duplicate definition warns (merge-collision-definition).Also fixes $ref resolution for duplicate global names: the loader let the last same-name global (by source-file path) win, contradicting the spec’s first-seen rule (and the merge-collision-component warning’s own message). It now resolves to the first. Corpora with same-name globals that differ in selector may expand $ref differently; validate already warns on them.Snapshots and captures record the page URL. The header of a .snap now carries a url: line after route:, and viewset.URLOf reads it back, so an offline consumer can re-derive which view a capture belongs to (or match it against routes of its own) without the live page. Captures written earlier have no url: line; URLOf returns "" for them.
cli
Released October 6, 2026
Match attribute selectors against the camelCase SVG and MathML attribute names captures keep, so [viewBox="0 0 24 24"] matches offline as it does in a browser.Skills: teach the fields added in 0.34.0.sightmap-authoring now covers privacy, watch and tags on components, URL properties on views and requests, and file-root environments and origins. It also corrects the message guidance: an uncaught exception arrives with level exception, so level: ERROR does not match it. sightmap-browser explains the view tags: line in a snapshot, and why a component under privacy: block or mask renders without its content properties.
cli
Released October 5, 2026
Add privacy to component entries (SEP-0009).privacy: block | mask | unmask declares whether a capture consumer may retain the matched element’s content. It applies to the element and its subtree, and the nearest enclosing declaration wins, so the common shape is a mask on a form with an unmask on the one field inside it that is safe to keep.Resolution deliberately differs from tags, which union across every applicable definition. A union is right for classification, where more labels are additive; it is wrong for a directive, where two applicable values are a contradiction that has to be decided rather than merged.It governs extracted properties as well as captured content, at the node each value is read from rather than at the component that declared it. Extraction is tree-closed, so a PATH.prop reaching a blocked descendant is withheld even when the declaring component is unrestricted. exists: survives mask because presence is structure, as do the four interactive-state attributes SEP-0013 defines (checked, selected, disabled, expanded) — a closed set carrying state rather than a value, without which a masked form could report no control state at all.mask also covers attribute values in the captured recording, not only text and input values. A consumer must withhold every data-* attribute, value/title/alt/placeholder/aria-label, and any attribute the corpus names in an attr= extract anywhere — naming an attribute in an extract is itself the author declaring it carries content, which keeps the rule from going stale. Presentational attributes (class, style, id) may be retained because replay needs them to render, and the SEP states plainly that user data in one of those survives a mask and should use block instead. A withheld property is absent, indistinguishable from one that did not resolve.Omission declares nothing: a component with no privacy leaves the consumer’s own default untouched, so a corpus can adopt the field one component at a time. A consumer may withhold more than the corpus asks, but never less.The loader carries the authored value onto ComponentDef.Privacy verbatim and does not inherit it to children, matching tags/source/memory. Resolution is the consumer’s job, over the flattened chain.Add watch to component entries (SEP-0015).watch: true asks a capture consumer to report that a component became visible to the user, rather than reporting it only when someone interacts with it. The surrounding lifecycle (rendered, removed) is optional: becoming visible is the only moment that answers the question the field exists for, and the rest is largely churn, since an element can render far off-screen, re-render on every state change, and be removed by a route transition. Repeat reports of one appearance should be collapsed, and a visibility report is passive — never an interaction. The components whose appearance is the signal (an empty state, an error, a promotion) are usually the ones nobody clicks, so an interaction-shaped record contains no trace of them.watch applies to the component it is declared on and never to its children, the opposite of SEP-0009 privacy. Privacy is a restriction, where covering the subtree is the safe default; watch generates records, where covering a subtree silently would multiply them.A consumer’s interactivity heuristic must not suppress a watched component. Without that rule the field does nothing for exactly the non-interactive components that motivate it.The loader carries the authored value onto ComponentDef.Watch. watch: false is identical to omitting the field.Add named environments and origins (SEP-0014).A file-root environments array defines deploy targets. A web environment maps surface names (app, api) to the scheme://host[:port] URL each surface has there; a native one (platform: ios | android) is identified by app_id and an optional build_type, and borrows a web environment’s origins through backend. A file-root origins map holds shared origins, hosts that are the same in every environment such as a tracking pixel or a vendor endpoint, and an environment’s own entry of the same name overrides it. Origin URLs may be patterns, with wildcards only in the leftmost host label (https://deploy-preview-*--acme.netlify.app, https://**.acme.com) or the port (http://localhost:*), so preview deploys and local dev servers belong to an environment without being enumerated.Views and requests reference both by name through environments: [...] and origins: [...]. Absent or empty means unconstrained. A view-scoped request’s environments are intersected with its view’s; its origins are never inherited. The fields are declarative only and take no part in route matching, so adding a list to an existing entity never changes what traffic it matches.Both registries are project-wide, first file by source path wins. The Go SDK loads the new fields (defaulting platform to web), exposes Corpus.EnvironmentByName, Corpus.ResolveOrigin, and RequestEnvironments, and validates them with new diagnostics: environment-invalid, origin-invalid, environment-backend-invalid, environment-ref-unresolved, origin-ref-unresolved (errors), and environment-name-collision, origin-name-collision, origin-environment-gap, origin-host-shared, environment-duplicate, environments-empty, origins-empty (warnings).One extract object for every property (SEP-0017).Component, request and message properties now read with extract: { from, path, pattern, join }, drawing from from one namespace: dom.text, dom.raw_text, dom.attr, dom.state, component, component.exists, req.body/rsp.body/req.headers/rsp.headers, and stack. Components gain pattern, and a path segment written Name[] reads every match instead of the first, collapsed into one value by join. A bracket with content in a path segment is reserved for future path predicates. dom.state reads interactive state separately from the markup attribute of the same name.The string forms (extract: text, attr=NAME, PATH.prop, exists:PATH, and request/message source/field/pattern) still load, lowered to the object, and validate warns extract-legacy-form with the replacement. They will be removed in a later release. validate also warns extract-privacy-withheld on a read its component’s own privacy withholds.Go API: ComponentPropertyDef, RequestPropertyDef and MessagePropertyDef each carry one Extract value in place of their per-entity fields.The exported corpus JSON (sightmap export) changes shape to match: component extract becomes an object, and request and message properties carry extract in place of source/field/pattern. This SDK still decodes the old shape, but an older reader cannot decode the new one, so upgrade readers before writers.Carry a node’s interactive state for dom.state (SEP-0013).The probe now captures each control’s current checked, selected, disabled and expanded state from native properties and ARIA, overlaid by the accessibility tree, as "true"/"false" ("mixed" for an indeterminate checkbox). extract: { from: dom.state, path: checked } reads it, including where no accessibility tree is available. It is kept apart from the element’s attributes, so dom.attr and selector matching still see the markup.Add tags to messages, ratify tags on signals, and close the view.tags gap (SEP-0016).tags now reaches every matchable entity. SEP-0004 deferred messages because console and exception events “aren’t a matchable entity in the spec today”; SEP-0006 then created the messages entity, matched by level and a message regex, so that premise no longer holds and no separate mechanism is needed.Message tags resolve as a union across every matching entry, which matters more here than anywhere else: message identity is a refusal rather than a winner, since SEP-0006 requires a consumer to surface an ambiguity when a record matches several entries. The classification survives that ambiguity even though the name does not.signal.tags already shipped via SEP-0007 with a schema description citing a SEP that never mentions signals. It is now documented, and its resolution defined: a signal’s effective tags are its own unioned with the resolved tags of the entity its ref names.Also closes the long-standing view.tags divergence. It was in the JSON Schema but absent from ViewDef and the Go loader’s allowlist, so a corpus carrying a view tag validated under ajv and was rejected by Go. New Corpus.TagsForURL resolves view tags as a union across every matching view, and Corpus.TagsForSignal does the same for signals.Resolve a console record’s tags as a union (SEP-0016).MessageMatch now carries each matched message’s tags, and Corpus.TagsForRecord returns the union across every message a record matches, deduplicated and sorted. The union holds even when the record matches several messages and its identity is ambiguous.Show a page’s view tags in observe (SEP-0004).The view header now prints a tags: line: the union of tags across every view whose route matches the page, so a broad tagged view is not shadowed by the narrower view that wins identity.Honor component privacy when resolving properties (SEP-0009).The matcher resolves each node’s effective privacy (nearest enclosing declaration wins, unmask overrides) and withholds any property value read from a block or mask node, judged at the node the value is read from, so a from: component read cannot surface a value out of a blocked descendant. Under mask, component.exists, dom.state, and dom.attr reads of the four interactive-state attributes still resolve. ComponentMatch now carries the effective Privacy and the component’s own Watch, and validate reports an unknown privacy value as invalid-privacy.Remove the dependencies field from views and components (SEP-0001, Rejected).dependencies asked a curator to hand-maintain a dependency graph that static import analysis derives more accurately and for free, and match --path, the reverse-lookup consumer it existed to serve, was never built. No SDK ever read the field.It is gone from $defs.view and $defs.component in the schema, from the loader’s unknown-field allowlist, from the normative spec, and from conformance fixtures 008-dependencies-binding and 108-fmt-dependencies-canonical.Breaking for any corpus that carries the field. Because the schema sets additionalProperties: false, a view or component with dependencies: is now rejected as schema-invalid rather than silently accepted. Delete the key; nothing consumed it.canonical-format.md keeps its sort-and-deduplicate rule for unordered string arrays. dependencies was its only instance, but SEP-0014 relies on the rule for view- and request-level reference lists.Body paths accept backslash escapes, and sightmap.SplitFieldPath is exported.A request or message property’s extract.path into a body is dot-separated. \. now addresses a literal dot inside a JSON key and \\ a literal backslash, so a key named a.b is reachable. A path with no backslash resolves exactly as before.This is a behavior change for a path that already contained a backslash. a\b used to address the key a\b and now fails to parse; a\.b used to mean the key a\ then b, and now means the single key a.b. validate reports a malformed path as request-property-path-invalid rather than letting the property resolve to nothing.SplitFieldPath exposes the same splitting for tools that compile a corpus into another schema.Add URL properties on views and requests (SEP-0008).A :name segment in a view or request route now binds that segment’s percent-decoded value as a property, needing no declaration. An optional properties[] array reads with the SEP-0017 extract object, from: url.query or from: url.path, for naming a query-string value or renaming a bound segment.One grammar covers both entities. Both carry a route, both are matched against a URL, and the value an author wants out of that URL is the same kind of thing in each case. A request property reads url.query/url.path alongside its body and header sources, so one request may carry URL-shaped and payload-shaped properties side by side.This changes what :param produces, never what it matches: route matching still normalizes :param to *, so every existing route matches exactly the URL set it matched before.A declaration is also a retention request. A consumer that scrubs captured URLs should retain the parts its corpus names, since a scrubbed parameter makes the property permanently unresolvable and omission is otherwise silent. Naming a part is not a privacy grant: a consumer must not treat it as authorization to retain a value its own policy would withhold.Removes transform from the proposal, which SEP-0010 deleted from every property type.Extension: address components by their full path, not their leaf name.Component names are unique only within a parent — that scoping is the whole point of children:. The overlay treated them as globally unique in three places, and on any real page that silently produced wrong answers:
  • activeComponents() deduplicated the corpus by name, so a production sign-in map of 104 components collapsed to 54. Clicking a text input resolved two levels deep instead of six, because the surviving Form was a different form.
  • Parent scoping in resolveElement() accepted an unrelated namesake’s match as a component’s parent.
  • PATH.prop extraction resolved each path segment by name across the whole corpus, so a submit button’s Label.label found a text field’s label, matched nothing inside the button, and dropped the property.
All three now key on a component’s address (its parentChain plus its name). resolveElement visits components ancestor-first and requires a component’s own parent to have matched, and each returned match carries its address.A file-root global is still dropped when a matching view declares a component of the same name at any depth, as Corpus.ComponentsForURL does, so a view-scoped $ref expansion subsumes its global instead of matching alongside it.Speed up match.Matcher.Match and match.FindAllMatches on large trees and corpora.The matcher tried every query at every node and allocated a map sized to the whole query set per node, so cost grew with nodes × queries. Queries are now indexed by a requirement of their first selector part (id, attr="value", class, attribute name, or tag), so a node only tries queries it could start. Per-node maps and per-child ancestor-chain copies are gone, and class attribute selectors ([class*="..."] and friends) no longer join the class list on every check. Matching against a 503-query corpus over a 10,000-node tree drops from 290 ms and 466 MB to 6.7 ms and 0.14 MB. Results and their order are unchanged.Adds Element.Attr and Element.HasAttr, which resolve an attribute the way selector matching does (id and class fall back to their dedicated fields).Matcher.Conflicts now deduplicates claims by component definition rather than by name, and sightmap.Conflict carries the claiming Defs alongside Names.A component name is unique only within its parent, so two different definitions can share one. Deduplicating by name silently discarded every conflict between them — on a production sign-in corpus, five nodes were each claimed by two definitions and none were reported. Snapshots looked clean while first-match-wins quietly dropped one claimant on every one of them.Names alone cannot identify a claimant in that case, so Conflict.Defs is index-aligned with Names, and the [Conflicts] report disambiguates repeated names by selector. A definition offering several alternative selectors still counts once, and so does a global together with its $ref expansions, which share the new ComponentDef.Origin.Don’t surface a blocked node’s accessibility value in the rendered tree.The matcher withholds a property read from a block or mask node (SEP-0009), but the renderer then substituted the node’s accessibility value as value, reintroducing the content the matcher had just withheld: a masked input rendered its own value. The fallback is now withheld under block and mask; declared properties the matcher did surface, such as a dom.state read under mask, still render.SEP-0014: a consumer may fall back to a view’s url: host, but only for a corpus that declares no environments and no origins, so a corpus that predates both keeps compiling as it did. Once a corpus declares either, every host comes from origins, and url: still never defines an environment or origin. A page host is a web concept, so a view that runs only in native environments needs none.
cli
Released September 25, 2026
browser: enable CDP focus emulation (Emulation.setFocusEmulationEnabled) on each tab so document.hasFocus() is always true regardless of the real OS window focus. Some page widgets gate behavior on page focus — e.g. react-aria date pickers open only on focus while the page holds OS focus — which an agent-, side-panel-, or headless-driven page never has, making them impossible to actuate in-page. Matches Playwright/Puppeteer, which enable the same emulation by default. Best-effort: a transitional target or very old Chrome that rejects the call simply falls back to the prior focus-dependent behavior.Fix sightmap browser start --port 0 / --cdp-port 0 (documented as “auto-allocate”): port 0 now asks the OS for a free port and records the port actually bound in .sightmap/.session. Previously 0 resolved to 0, the daemon bound an OS-chosen port but wrote serverPort: 0, CDP landed on port 1, and start --detach exited 0 while every client command then refused the session with “no running session”. start --detach also no longer reports a session ready until its file records a reachable server port.Overlay: resolve components when hover lands on a wrapper above the component (e.g. a pointer-events:none control).The overlay extension resolved the hovered element bottom-up with closest(). When a control has pointer-events:none and a styling wrapper sits ABOVE the component container (as JetBlue’s checkout form fields do — a .first-name div wraps jb-form-field-container > input), the hit-test lands on that wrapper, which is an ancestor of the component subtree, so closest() walks up and finds nothing — the field appeared uncovered even though the CLI resolver (top-down) matched it. On a resolution miss the overlay now descends into the hovered element to the deepest active-component element whose box contains the pointer, and resolves from there.Fix the extension overlay rendering nothing on hover for any corpus without a components.yaml.The content script used state.globals.length as its “corpus is loaded” signal. But globals is the optional file-root component list and is omitempty on the wire, so a corpus whose components are all view-scoped — the common case, and what sightmap init produces — serves no globals key at all. The gate was therefore permanently false:
  • Hover was dead. onMouseMove returned before resolving, so #__sightmap-overlay injected fine but never got any children: no highlight boxes, no tooltip, no console error. Clicks kept working and logging correct component paths, because the click path gates on state.version — which made the failure look like a hover/render bug rather than a readiness-gate bug.
  • The corpus was re-fetched on every poll. pollVersion took the same gate, so it never reached the cheap /sightmap/version path and re-downloaded the whole corpus every 4s, logging [sightmap] loaded N components each time.
Both paths now share a single corpusLoaded() helper keyed on state.version, which fetchSightmap only records once a load returned a non-empty component set — so the “server answered before its corpus finished loading” retry behavior is unchanged.The extension manifest version is bumped so an already-installed ~/.sightmap/extension is re-extracted on the next browser start (ensureExtension only reinstalls on a version change).Snapshot tree hygiene: exclude non-content nodes from the extracted component tree (snapshot --tree-out, capture, --json).The tree builder now prunes two classes of node — together with their whole subtrees — before they reach output, so a captured *.snap.tree.json is app content only and is safe to use as an authoring/golden fixture without post-processing:
  1. The CLI’s own injected overlay (#__sightmap-overlay, #__sightmap-tooltip). It was visible and non-ignored, so it leaked into the tree JSON and enumerated as bogus candidates.
  2. Document-metadata and non-rendered tags: <head>, <script>, <style>, <meta>, <link>, <title>, <noscript>, <template>, <base>.
cli
Released September 11, 2026
Add the raw_text extract mode (SEP-0013, first half).extract: raw_text returns a node’s own literal text — the concatenation of its direct text-node children, whitespace-normalized — never its accessibility name. It is pinned to textContent-style data, not the layout-dependent innerText, and it excludes both descendant element text (no <style>/<script> bleed, no subtree concatenation) and CSS pseudo content (::before/::after).It is the deterministic escape for when the accessible name welds in extra text: a heading whose AX name appends a CSS ::after badge resolves text to “Main Most popular” but raw_text to “Main”, so an exact property match (SubFare[tier="Main"]) works. text stays the default (and the right choice when the accessible name is the value); an empty raw_text omits the property, like the other extract forms.(The other half of SEP-0013 — pinning text to a faithful, cross-consumer accessible-name computation, and carrying interactive-state attributes — is deferred.)Add the SEP-0007 Component/View state-signal subset. A corpus-root signals: block names a component (present) or a view (route-active) as a named boolean predicate ({name, ref, tags?}) — the dependency-free core of SEP-0007. Resolve one with Corpus.ResolveSignal / SignalByName; validation reports signal-ref-unresolved and signal-ref-ambiguous. Request/message refs and the temporal/window machinery are intentionally out of scope.Offline matcher: support :not(:has(...)) and a combinator inside :not(...), for parity with the live DOM.The offline component-tree matcher (used by snapshot, coverage, validate, sel-check, and the overlay) now honors two standard-CSS relational selectors it previously mishandled:
  • :not(:has(...)) and :is(...:has(...)) — a relational pseudo nested inside :not()/:is() is now evaluated with the subject’s subtree instead of being silently dropped. This unblocks the leaf-scoping pattern X:not(:has(X)) (e.g. jb-form-field-container:not(:has(jb-form-field-container))), which previously matched zero nodes offline while resolving live.
  • A descendant/child combinator inside :not() (e.g. button:not(nav button)) now parses and is evaluated against the subject’s ancestor chain. Such selectors previously threw a parse error, so a corpus using valid CSS failed to validate even though it resolved live.
:not() now accepts a complex-selector list, matching CSS Selectors Level 4. Sibling combinators (+, ~) remain unsupported and are still rejected loudly at parse time.Note: SelectorPart.Not changes from *SelectorPart to []ParsedSelector to represent the complex-selector-list argument.Reframe the sightmap-authoring skill’s “Property extraction principles” around purpose: a property earns its place only as a discriminator (makes an otherwise-ambiguous instance addressable) or as signal (a value a downstream event/agent needs). A property that does neither — most commonly a text/label that just restates a node’s accessible name, or a text on a nameless container that dumps its whole subtree — is noise. Rule 1 now requires a link/button to be identifiable (by its accessible name, or a useful property) rather than mandating a property (which invited restating-the-name busywork); Rule 2’s per-instance discriminator is preserved, with an explicit carve-out for responsive duplicates (narrow the selector, don’t invent a discriminator). Validated against the JetBlue corpus (drove ~90% of property decisions cleanly; 21 noise properties removed with zero tool breakage).Live shadow-piercing query (sel-probe, click/fill, property extraction) no longer under-reports on pages with a form.__smDeepQueryAll walked the DOM via instance properties (node.children, el.matches, el.shadowRoot). On an HTMLFormElement, a control whose name/id is children makes the form’s named getter return that control instead of the element HTMLCollection ([LegacyOverrideBuiltIns]), so for (const c of node.children) threw “children is not iterable” on that form. The blanket catch then returned a truncated result — silently 0 matches for any element after the form in document order (observed on jetblue.com: [data-fs-element="submit"], a.rounded-button, [data-fs-element="points-toggle"], and others reported 0 live while querySelectorAll and the offline matcher saw them).The traversal now goes through cached Node.prototype/Element.prototype accessors, which bypass any named-property shadowing, so form-heavy pages resolve correctly. Affects every live lookup: sel-probe, component-query click/fill/hover, and live property extraction.browser mcp no longer points at Chrome flags that do not exist. The “no WebMCP tools here” error used to suggest --enable-blink-features=ModelContext,ModelContextTesting and --enable-features=DevToolsWebMCPSupport; Chrome silently ignores those names, so following them left document.modelContext undefined on Google Chrome and made the native path look unshipped. The message and the browser mcp docs now name the real switch, --enable-features=WebMCPTesting (the same feature as chrome://flags/#enable-webmcp-testing). Fixes #413.
cli
Released September 7, 2026
Fix browser wait-for --component 'X' timing out whenever the query matched more than one node on the page. The wait polled with the strict single-node resolver (the one click/fill/hover need), so a “several match” result was reported as an ambiguity error and mistaken for “not present yet” — even though snapshot listed every instance and wait-for --selector with the same multi-match CSS returned immediately. wait-for --component now treats presence of ≥1 match as satisfied (multiple matches are proof the component is on the page, which is exactly what the caller is waiting for), via a new compquery.Present presence predicate. The interaction commands are unchanged: they still resolve to a single target through the strict compquery.Resolve. Narrowed queries keep working — a property predicate (X[status="Published"]) or an occurrence index (X#0) still matches, and X#N waits until occurrence N exists.
cli
Released September 3, 2026
Fix browser mcp call failing against native Chrome WebMCP. executeTool on a native document.modelContext requires the arguments as a JSON string (and returns the result as a JSON string), whereas a JS polyfill takes and returns objects. mcp call passed arguments as an object unconditionally, so it worked against a polyfill but failed on native with “Failed to parse input arguments”. It now detects the native surface (the built-in executeTool reads as [native code]) and serializes the arguments accordingly; the native JSON-string result was already normalized.
cli
Released September 3, 2026
Add browser mcp list and browser mcp call for the WebMCP tools a page exposes via document.modelContext. mcp list enumerates the tools (name, description, input schema; --json for full schemas) and distinguishes native, polyfilled, and absent WebMCP — failing loudly with the Chrome flags that enable native WebMCP when there is none. mcp call <tool> resolves a tool by name and invokes it via executeTool; arguments are supplied as a JSON object (--args) and/or repeatable --param key=value pairs. It unwraps the standard CallToolResult envelope (rendering the tool’s text/structured content and any guidance as itself rather than a stringified blob), exits non-zero when the tool reports isError so it is scriptable, and reports not-found or thrown errors distinctly. One tool runs at a single point in time; there is no cross-navigation tool response.Fix browser start silently running sessionless when it couldn’t persist the session file. start now creates the corpus dir if absent (so the session lives at the per-corpus .sightmap/.session rather than a shared $TMPDIR fallback) and fails loudly if it still can’t write it, instead of continuing without a session. Client commands that find no session file now warn before falling back to the default CDP port — where a session may belong to a different corpus or agent — rather than silently attaching to a foreign tab. Applies to both the launched and --attach start paths.Fix browser inject --persist not re-injecting across navigations. The persisted script was registered with Page.addScriptToEvaluateOnNewDocument on the daemon’s collector connection, but that connection never enabled the Page domain — so the command returned an identifier (and inject --list showed the entry) while the script was never actually evaluated on new documents. Only the initial one-shot run fired. The collector now enables Page before registering, so a persisted script runs at document start on every subsequent navigation and new tab, as documented.sightmap lint now auto-reconciles the multi-instance-no-property rule against captured snapshots by default. The static heuristic guesses from selector shape alone and false-positived on container-ish selectors that in fact match a single node; when captures exist under .sightmap/snapshots/, lint now uses their real match counts (a count of 1 suppresses the warning) without needing an explicit --all-snapshots. With no captures present it runs the static heuristics unchanged, and --snapshot/--all-snapshots still control the set explicitly. A missing snapshots/ directory is no longer an error.
cli
Released September 2, 2026
Add browser inject for persistent script injection. Unlike eval (which runs once in the current document), inject --persist registers a script with the running session daemon, which re-applies it at the start of every new document in every tab via CDP Page.addScriptToEvaluateOnNewDocument — so it survives navigations and new tabs for the life of the session. --file loads the source from disk, and --list / --remove ID manage the persisted set. Useful for polyfills, overlays, and debugging/experimentation bundles that must outlive a multi-page flow.Docs: sync the authoring and browser agent skills to recent offline-extraction and eval changes. The authoring skill now states that every attribute (not just class/id) matches offline and that text falls back to a node’s rendered innerText when it has no accessible name; the browser skill notes that browser eval awaits a returned promise.
cli
Released September 2, 2026
The offline element model now captures every attribute the live DOM carries (minus injected sightmap ids and framework-internal _-scoping attributes like Angular’s _nghost/_ngcontent), not just the curated subset that landed in the generated selector string. Offline attribute selectors ([attr=…], [attr*=…], …) and extract: attr=NAME now match the live DOM for non-standard attributes — e.g. value on a role="option" <li> — closing a source of live/offline divergence.Offline text extraction now falls back to a node’s rendered text when it has no accessible name, so extract: text on role-less elements (custom elements, bare <span>s) resolves the visible value instead of silently empty. Component text is captured from innerText — rendered, transform- and visibility-aware, excluding <style>/<script> and non-rendered nodes — and normalized to a single clean shape (whitespace runs collapsed, ends trimmed) at the build boundary, so snapshot/coverage/bounds and library consumers all see the same value the runtime does. Adds a text field to the component-node schema.browser eval now awaits a returned Promise and yields its settled value instead of an opaque {}, so async page/tool code (e.g. an awaited executeTool()) can be driven directly. A per-eval timeout bounds a never-settling promise. This is a no-op for synchronous expressions.
cli
Released August 29, 2026
Component properties now resolve offline over the component tree (SEP-0010). Matcher.Match populates each ComponentMatch.Properties from the matched tree with no live DOM, so a downstream consumer holding a serialized component tree gets property values directly. The extract grammar is the four tree-closed forms — text, attr=NAME, PATH.prop, exists:PATH — and transform is removed from component properties. sightmap validate now checks component properties[]: duplicate names within a component and unrecognized/removed extract modes are errors.Retire the live-DOM component-property extraction pass and remove transform from request and message properties (SEP-0010). Component property values now come solely from the matcher’s offline, tree-closed resolution — the CDP/JS extraction (observe.ExtractProperties and its properties.js) is gone, and snapshot/bounds/query read the values the matcher already resolved. Request and message properties[] keep source/field/pattern; fold any post-processing into pattern (RE2 + capture groups). ApplyTransform is removed.Port the DevTools extension’s component-property extraction to the SEP-0010 tree-closed model. The embedded overlay (resolver.js and its inlined content.js copy) now resolves the four extract forms — text, attr=NAME, PATH.prop, exists:PATH — over the matched component tree, replacing the removed DOM-shaped modes (inner_text/text_only/inner_html/raw CSS sub-selector) and transforms. Descendant paths resolve a child component’s own extracted property (text is the element’s DOM text content, the extension’s implementation-defined accessible text).Update the sightmap-authoring skill for SEP-0010: component properties[] now document the four tree-closed extract forms (text, attr=NAME, PATH.prop, exists:PATH) and the promote-a-sub-element-to-a-child-component pattern. Removed the DOM-shaped modes (inner_text/text_only/inner_html/raw CSS sub-selectors) and all transform guidance (including the request-properties transform reference).
cli
Released August 26, 2026
sightmap browser start --detach runs the daemon in the background (in its own session) and returns once it is serving, so scripts and agents no longer hang on the foreground daemon. Unlike nohup start &, the detached daemon survives the launching shell (it setsids into its own session). browser start also now prints that it is a foreground daemon holding the shell, and browser status probes the sightmap HTTP server in addition to Chrome’s CDP — reporting ⚠ degraded when CDP is up but the server has been reaped, instead of a misleading running.browser bounds now accepts the full component-query grammar — property predicates and descendant chains — the same engine as click/fill/hover/wait-for. Previously it matched component names only, so bounds 'Card[title="X"]' or bounds 'Row Star' returned “matched no component”. Multi-match is preserved (a query returns every matching component’s box), --substring still does name-only fuzzy matching, and --all is unchanged. Also documented the existing wait-for --component and --view flags in browser --help.Authoring + browser skills: document the browser daemon lifecycle. browser start is a long-running foreground daemon that holds the shell; scripts and agents should use browser start --detach (which returns once serving and survives the shell) rather than nohup start &. Also note headless auto-detection and the --no-sandbox hint on sandboxed hosts, and that browser status can report a ⚠ degraded daemon.browser start now comes up out of the box on headless and sandboxed Linux hosts. With no display ($DISPLAY/$WAYLAND_DISPLAY unset) it defaults to headless instead of dying with “Missing X server or $DISPLAY”, and when Chrome’s launch fails because the host restricts its sandbox (unprivileged user namespaces clamped by AppArmor or a container) the error points straight at the fix (--chrome-flag=--no-sandbox) rather than leaving you to decode Chrome’s stderr.
cli
Released August 25, 2026
Authoring skill: document the component model. Add “Component hierarchy (children:)” and “Cross-view references ($ref)” sections covering nested components, parent-scoped selectors, the depth budget, and $ref attestation. The browser skill now spells out that component queries use a descendant combinator (no > child combinator) and that a multi-match is an ambiguity error, not a missing component.Authoring skill: fix the “Further reading” pointers. They referenced go/README.md (not shipped with the installed skill) and docs/reference.md (which does not exist). The skill is self-contained — coverage model, outer loop, tool surface, lint rules, quality checklist, and the component model are all in it — and now points to the published docs for the normative spec instead of dead repo-relative paths.Authoring skill: fill three small content gaps. Document the memory: shape (a list of plain strings, attachable at the file root or on a view/component), add a route-matching note (* matches one path segment, ** matches zero or more, and the URL is matched decoded so %2F counts as a separator), and reconcile the class-selector guidance (stable hand-authored class names are fine — the volatility risk is generated/hashed classes).
cli
Released August 24, 2026
sightmap multi-coverage no longer manufactures phantom “global candidate” promotions from stale capture directories. It grouped columns purely by directory name under snapshots/, so a leftover or renamed dir (e.g. an old snapshots/views/ kept alongside the current snapshots/home/ for the same page) became a second column and made that page’s own components look like they “appear in 2+ views” — advising the author to wrongly globalize view-scoped components.A capture dir is now treated as a real view only when it matches a view in the current corpus (by SnapBasename). Non-current dirs are still shown in the matrix (marked *) for context but are excluded from the cross-view global-candidate analysis, and a warning names them so the author can delete the stale dir or re-capture under the current name. The sightmap-authoring skill documents the new behavior.Two authoring-clarity fixes for the offline inventory and the capture gate.sightmap stats now attributes corpus-root global components and requests to their own (global) · (all views) row instead of showing 0 against every view. A corpus whose coverage lives entirely in globals (e.g. a single-view app mapped with global components) previously rendered a per-view table that read as empty, with the confusing footer “per-view rows sum to 0”. The globals are shown once, on a leading row, and the footer explains them — globals apply to every view, so folding them into one view’s row would misattribute coverage. The --json contract is unchanged.sightmap capture’s novelty-gate message no longer reads like it is refusing a first baseline. The first capture of a view always writes (an empty set can’t be redundant); reaching the gate means a baseline already exists and the new capture adds nothing, so the message now says so plainly (“<view> already has N capture(s); this one adds no new component or interactive slot — not saved”). The sightmap-authoring skill is updated to match and to reinforce the first-capture-always-writes guarantee.
cli
Released August 24, 2026
browser install now fetches the correct architecture on linux-arm64. It previously mapped every Linux host to the linux64 (x86-64) Chrome for Testing build regardless of CPU, so arm64 machines silently got an x64 Chrome under ~/.sightmap/browsers/chrome-<ver>/chrome-linux64/. The platform is now resolved per-arch (linux-arm64 on arm64, linux64 on x64). Because Google does not currently ship an arm64 build to the Stable channel, resolution falls back through Beta → Dev → Canary for platforms Stable does not carry, printing a one-line warning when it uses a non-Stable channel. Every mainstream platform continues to install from Stable unchanged.
cli
Released August 21, 2026
Authoring efficacy on hook-poor DOMs (Salesforce Lightning, Angular, framework-generated markup):
  • Generalized selector-candidate generation. gap and suggest no longer dead-end when a DOM has no data-testid/data-component. Candidate generation now ranks custom-element tags, design-system classes, ids, other stable data-*, and name/href/aria hooks (data-attributes remain the top-ranked input, not an override), dropping only clearly machine-generated tokens. gap also emits a container hook on the hook-poor path.
  • New explain command. Node-first inspection: pick nodes by selector, --id, or --grep (role/name) — live or offline via --snap — and dump each node’s facts, ranked selector candidates, coverage tier + owning component, and ancestor hooks. Shadow-transparent (matches the offline matcher), so authors no longer hand-read *.snap.tree.json.
  • Honest coverage. snapshot --coverage and gap now warn when a clean “0 orphaned” pass is carried entirely by global (chrome) components — i.e. the current view modeled nothing and the pass reflects a global backstop rather than a real view.
  • Authoring skill: a second mandatory property rule requires a per-instance discriminator on any component whose selector matches more than one instance, so repeated cards/rows/tabs stay individually addressable; plus explain documentation.
Fix the browser overlay extension rendering 0 components on pages whose corpus the sightmap server serves correctly. The content script proxied its /sightmap fetch through the background service worker, and on a cold/just-woken MV3 worker that round-trip intermittently delivered an empty corpus at page load; the empty result then stuck for the life of the page because the per-instance version never changes (so pollVersion never refetched) and an open side panel keeps the worker alive. sightmap snapshot was unaffected (it reads the corpus in-process over CDP). The content script now fetches the local server directly on http-origin pages — host_permissions already grants http://localhost:* and the server sends Access-Control-Allow-Origin: * — using the background proxy only as an https mixed-content fallback, and it treats an empty corpus as “not loaded yet” and keeps retrying instead of caching it.Fix snapshot/capture rendering an empty component tree when the page root (or any ancestor) is reported invisible. render.Filter dropped an entire subtree at the first IsVisible=false node, but a zero-size or visibility:hidden ancestor — notably the document <html> root, which reports height 0 / ignored — can have fully visible descendants, so the whole annotated tree vanished even though coverage still counted the visible nodes. Invisible nodes are now made transparent (the node itself is omitted, visible descendants survive); a genuinely hidden display:none subtree still renders nothing, keeping render in lockstep with coverage.sightmap stats now reports a Messages count. The offline inventory listed views, components, requests, properties, and memory but silently omitted the SEP-0006 messages: entity, so a corpus’s console/exception matchers were invisible in the totals (and in --json). Adds messages to the totals table and the --json contract.
cli
Released August 18, 2026
browser start --attach host:port attaches the daemon (devtools server + console/network collector) to an already-running Chrome’s CDP endpoint instead of launching and owning its own. It is a deliberately degraded mode: no owned profile or extension guarantees, capture is complete only from attach onward (pre-attach network/console history can’t be recovered — Network.enable replays nothing), and browser stop detaches rather than killing the caller-owned browser. The collector and devtools query surface are unchanged — the collector only ever needed a CDP address — so live console/exception and request matching work identically once attached.Also fixes a latent shutdown-order bug in the collector surfaced by attach mode: Collector.Stop cancelled the per-tab drain contexts after wg.Wait(), which deadlocked whenever the CDP connections were still healthy at stop time (the norm when a consumer stops the collector without tearing down the browser). Stop now cancels the drains first and refuses new tab attachments during teardown.And fixes extension-server-port injection under attach: findExtensionSWWS took the first extension service worker it found, so in a real browser with several extensions installed (the attach case) it injected the port hint into the wrong one and the sightmap overlay never learned the server port — falling back to probing 7891–7900 and latching onto whatever sightmap server answered first (often a different session). It now identifies the sightmap extension by its manifest name before injecting, so the correct overlay gets the hint. An owned launch never hit this: its isolated profile contains only the sightmap extension.The browser start devtools now surface extracted property values, not just matched-def names. The collector eagerly retains request/response bodies for XHR/Fetch traffic (request bodies inline from requestWillBeSent; response bodies fetched on Network.loadingFinished, off the event loop), so a buffered Request record is complete. sightmap network list|get annotate via RequestsForRecord — resolving each matched request’s properties[] against the live headers/body — and sightmap console list|get surface a matched message’s stack properties[]. Extracted values render as a trailing {name=value, …} token on list lines and a Properties: block in get detail, e.g. GET /api/checkout/pay → 200 OK (Fetch) {outcome=declined} — the “200 OK but the body says declined” case, now visible.Make selectors behave consistently across shadow DOM. The captured component tree already flattens shadow roots (so offline matching, sel-probe’s offline count, and coverage pierce shadow), but every live-DOM operation re-found nodes with document.querySelector, which cannot cross shadow boundaries — so property extraction, interaction (click/fill/hover/value/scroll/wait-for), bounds, sel-probe’s live count, and suggest/discover were silently shadow-blind and disagreed with the corpus. A shared shadow-piercing resolver (browser.DeepQueryJS) now backs all of them, so live operations reach shadow-DOM content the same way matching does (property extraction on shadow-DOM leaves, interaction with shadow-DOM controls, and discovery of shadow-DOM links now work). spec/v1/schema.md formalizes the selector/tree model and its shadow-DOM semantics — a deliberate divergence from live document.querySelector, which does not cross shadow roots.Expand the authoring and browser skills to cover the runtime spec additions.
  • sightmap-authoring: new “Requests and messages” section documenting requests: (route/method), request properties: extraction (SEP-0005 source/field/pattern/transform), and messages: (SEP-0006 level/message + source: stack stack-addressing properties); a new “Runtime activity” step in the per-page loop plus a runtime line in the “Done when” checklist so the loop routes authors to these entities; and view-route-generality guidance (a catch-all route that matches every page is a smell — carve specific routes).
  • sightmap-browser: reframe the console/network tooling as the runtime view of the corpus — each record leads with a [Match]/[--] slot and trails extracted {name=value} properties — with the observe-only “reproduce the traffic” note and --url-substring / SPA wait-for caveats.
  • Fix stale command references: sel-probe -- 'selector' (the bare sel-probe 'selector' form fails) and gap --include-hidden (the documented --visible flag does not exist).
The reference network/console collector now populates the faithful-record fields added for SEP-0005/0006 runtime matching. Observed Request records carry request/response headers (from the CDP requestWillBeSent/responseReceived events — cheap, no extra round-trip) and an observed DurationMs; observed exception Message records carry the Stack (from exceptionThrown stackTrace.callFrames, throwing frame first, with 0-based line/column preserved). Bodies remain fetched on demand. This is what lets RequestsForRecord extract body/header properties and MessagesForRecord resolve stack properties against traffic the reference tooling captured.Make network get reliable and header-aware. The devtools body endpoint now serves the response/request body eagerly retained on the record (captured at loadingFinished) instead of always re-fetching it from Chrome via getResponseBody, which failed once the browser had evicted the body (“response body no longer available”). And network get now renders the captured request/response headers (they were already on the record from the collector but never surfaced). This closes the HTTP response header/body residual split out of the network-collector gaps.
cli
Released August 13, 2026
match.Matcher gains a chain-matching entry point for consumers that classify a stream of individual observed elements rather than a full component tree. Each observed element carries only its own root→leaf ancestor chain, so there was no first-class API for it — a consumer had to reimplement the spec’s component identity/tag resolution by hand (and route-blind). MatchChain(chain, pageURL) runs the shared NFA matcher over a single-branch spine built from the chain and returns depth-annotated ChainMatch values; NamesForChain applies the nearest-enclosing identity rule and TagsForChain the tag-union rule (deduped, sorted), so the spec’s resolution lives once in the shared library. Matching is route-aware, so view-scoped components apply on the chain exactly as in a full-tree match. Purely additive packaging over already-exported pieces — no new matching semantics.
cli
Released August 13, 2026
messages: can now match and extract on exception stack traces (a SEP-0006 follow-on). An observed Message gains an optional Stack []Frame (throwing frame first; Frame{Function, File, Line, Column}), empty for a plain console record. A MessageDef gains a stack-addressing properties[] mirroring SEP-0005’s source/field/pattern/transform: the one source is stack, field names a frame and attribute (top.file, top.function, 1.line, where top aliases frame 0), an optional RE2 pattern refines the resolved value, and transform post-processes it. Corpus.MessagesForRecord now folds the extracted values into each MessageMatch.Properties, bringing it to parity with ComponentMatch/RequestMatch; unresolved values are omitted silently, so a plain console record (no stack) simply extracts nothing. The reference CLI validates the new declarations (message-property-invalid-name / -source-invalid / -no-field / -pattern-invalid).Observed Request records can now carry the payload a RequestDef’s properties[] addresses, and a new matcher resolves them — the runtime half of SEP-0005. Request gains optional (omitempty) DurationMs, ReqHeaders / RspHeaders ([]Header), and ReqBody / RspBody (*Body) fields, so a producer holding a full capture can populate them while a lazy producer leaves them nil and the wire stays lean. Corpus.RequestsForRecord(rec) []RequestMatch does route+method identity matching (via RequestsForURL) and then resolves each matched def’s properties[] against the record — source → field (JSON dot-path with numeric array indexing for a *.body; case-insensitive header lookup for a *.headers) → optional RE2 pattern (capture group 1 else the whole match, or a scan of the raw source when field is absent) → optional transform. Unresolved values are omitted silently per the spec, so an incomplete record degrades to fewer properties rather than erroring.
cli
Released August 12, 2026
Add sightmap export [dir]: load a .sightmap/ corpus and emit the canonical Corpus wire — the exact json.Marshal(sightmap.Corpus) shape a library consumer reads (selectors[] arrays, components nested under each view, plus requests and messages) — to stdout, a file (-o FILE), or an HTTP endpoint (--url, POSTed as application/json with no auth headers). The .sightmap/ directory is auto-detected by walking up from [dir] or the cwd, and TLS verification is skipped for local hosts (localhost, 127.0.0.1, .test) or under an explicit --insecure. A companion sightmap push URL [FILE] POSTs a corpus JSON (from a file or stdin) through the same transport.This replaces the hand-rolled Python collector (collect_and_upload_sightmap.py) that shipped a second, lossy serializer — it flattened views into compound selectors and dropped routes, requests, and messages. Routing the upload through the Go loader makes it the single source of truth and shares the exact sightmap.Corpus type with the server-side reader, so the two ends cannot drift.sightmap serve and the browser start daemon now serve the canonical Corpus wire — the same shape library consumers see (selectors[] arrays, components nested under each view) — inside a thin {site, version, corpus} envelope, replacing the bespoke pre-compiled shape. The bundled browser extension consumes it directly; cache-busting via /sightmap/version is unchanged.
go
Released August 12, 2026
Restructure the Go library for downstream consumers. All corpus vocabulary, observed runtime records, and match-result types now live in a single self-contained sightmap package, with the matching engines consolidated behind one match.Matcher. Types follow a consistent naming model — …Def for a spec, bare structs for observed records, …Match for results — and extracted values share a typed PropertyValue. This is a breaking change to the Go import surface for library consumers (there were none before this release).Add runtime matching for console/exception messages. Corpus.MessagesForRecord classifies an observed console record against messages: definitions (case-insensitive level equality + an RE2 message regex, precompiled at load), returning every match so an ambiguous record is surfaced rather than silently resolved. The sightmap console and sightmap network devtools listings now annotate each captured record with the corpus definitions that classify it — messages via MessagesForRecord, requests via route+method identity — leading each line with the match.
cli
Released August 10, 2026
Add a top-level messages: entity (SEP-0006): named console-output and exception patterns, matched by level and a message regex. This gives console activity what requests: gives network activity, a named entity the rest of the corpus can reference by name.An uncaught exception arrives as level: exception, not level: error. The reference capture emits log, debug, info, warn, error, and exception, so level: ERROR matches console.error and does not match an exception. SEP-0006 still needs no kind: discriminator, because the origin is carried as a level value, but a corpus that wants exceptions has to name them.New validation:
  • message-regex-invalid (error) compiles message at validation time, matching how component selectors are already checked. The corpus no longer stores a pattern nobody has proven is a pattern. The dialect is pinned to RE2 (Go regexp / the re2 npm package for JS): a linear-time syntax with no backreferences or lookaround, so authoring-time validation and runtime matching agree across SDKs.
  • merge-collision-message (warning) reports a duplicated name. This one is load-bearing for SEP-0007: ref: resolution counts distinct entity kinds, so two messages sharing a name collapse to one kind and the ambiguity check never fires.
  • message-conflict (warning) reports two entries that can match the same record, where that overlap is statically decidable: same level, identical or absent message.
  • message-level-unknown (lint) catches a level outside the emitted vocabulary. The realistic trap is WARNING, CDP’s own spelling, which the capture normalizes to warn.
  • field-type-invalid (error) now covers message fields, so message: 404 and level: 500 are rejected in Go as ajv already rejected them.
This resolves SEP-0006’s open question on ambiguous-match diagnostics, which the SEP delegated to its implementation.Matching is not implemented: the SDK parses and validates these declarations but does not evaluate them against console records, even though the capture layer already collects both console output and exceptions.Add properties: to request definitions (SEP-0005): named values a consumer extracts from a live request/response pair, so a 200 OK whose body says a payment was declined can be reasoned about.
  • source names the root (a closed enum: req.body/rsp.body/req.headers/rsp.headers).
  • field selects a value within source: an object-key dot-path for a body source, a header name for a headers source (required there).
  • pattern is an RE2 regex (Go regexp / the re2 npm package for JS; no backreferences or lookaround) that refines what field resolved, or scans the raw source text when field is absent. field and pattern compose (anyOf) instead of being mutually exclusive.
  • transform shares the component-property vocabulary (unchanged; cleanup tracked separately).
New validation, closing a gap where the Go SDK accepted corpora the JSON Schema rejected:
  • request-property-invalid-name, request-property-no-extractor, request-property-source-invalid, request-property-headers-require-field, and request-property-pattern-invalid (errors) enforce in Go what only ajv enforced before.
  • request-property-shadows-reserved (warning) fires when a property is named status, method, or duration, which shadows the request’s HTTP identity and makes it unreachable from a signal filter.
  • field-type-invalid (error) rejects an unquoted non-string scalar in a schema-string field. yaml.v3 decodes any scalar into a Go string by taking the raw lexeme, so source: 200 used to load as "200" while ajv rejected it.
Extraction itself is not implemented: the SDK parses and validates these declarations but resolves no source/field/pattern and applies no transform. spec/v1/schema.md marks the evaluation requirements as such, and the 018-request-properties conformance fixture is now executed by the Go test suite.
cli
Released August 7, 2026
Add sightmap atlas: find searches the community atlas at sightmap.org/atlas, list browses it, and add installs one corpus into .sightmap/.find matches slug, name, domains, categories, and description, ranked so an exact domain match comes first, because an agent about to automate a site starts from a URL and has no way to guess a slug. Each hit prints its own sightmap atlas add command, and a query that matches nothing exits 0.add fetches one .tar.gz and never reads the index, so an index outage cannot stop an install. It refuses a non-empty target before touching the network; there is no --force. --index and --source point the verbs at a mirror or a private corpus store, under the same HTTPS-only transport policy and the same archive caps.New public API: sightmap.Totals, the five corpus-wide counts split out of sightmap.Stats, which now embeds it. Field access and the sightmap stats --json output are unchanged — the JSON is byte-identical — but a composite literal must now write Stats{Totals: Totals{Views: 1}}. The split exists because a published catalog entry carries these five numbers as its stats object and puts the per-view rows in a sibling field, so atlas.Entry.Stats is a sightmap.Totals: one definition of what the counts mean, and no PerView field that no valid catalog could ever fill. As a side effect sightmap atlas find --json now prints a count that is zero rather than omitting it, and reports properties and memory, which it previously discarded.Add corpus statistics, in the library and on the CLI.New public API: sightmap.Stats, sightmap.ViewStats, and (*Corpus).Stats(), so any consumer of a loaded corpus — the atlas index generator, Subtext — gets the counts without shelling out to the CLI. Components counts distinct component names corpus-wide (a global reused by three views is one component), while Properties and Memory are summed over distinct component definitions, so a $ref-expanded copy counts once but two views that each define a different component under the same local name both count. Stats.IsEmpty reports a corpus with nothing in it — memory alone is not nothing.New sightmap stats verb over that API: the totals plus a per-view component/request table, and --json for a stable machine-readable form (views / components / requests / properties / memory / per_view) suitable for CI consumers. stats refuses a corpus that sightmap validate rejects, since the loader drops the definitions it cannot resolve and the counts would silently under-report; in --json mode the failure is itself JSON (an error key plus diagnostics), so a consumer always has something to parse.
gocli
Released August 5, 2026
Breaking: rename the rule-side corpus types to the Def convention — match.SightmapComponent → match.ComponentDef and match.SightmapMatch → match.ComponentMatch. A ComponentDef describes what to match; a ComponentMatch is the result of matching one. No behaviour change; callers update to the new names.Add sightmap init: scaffold a schema-correct .sightmap/ corpus (a commented components.yaml and views/example.yaml carrying version: 1 and the top-level views: wrapper) so the first files an author sees are already valid instead of written from memory. Existing files are never overwritten, and the scaffolded corpus passes sightmap validate as-is.Breaking: split the matching engine out of Corpus. Corpus is now pure, serializable data; build a Matcher with NewMatcher(corpus) to match a live component tree. Corpus.MatchTree and Corpus.Components move to Matcher.MatchTree / Matcher.Components (the per-URL compiled-query cache now lives on the Matcher). Read-only corpus queries (ComponentsForURL, ViewForURL, RequestsForURL, GlobalComponentNames) stay on Corpus.Model requests: in the corpus and make it serializable to a stable wire form:
  • Global and view-scoped API request definitions (RequestDef / Payload / Field) are now parsed into the corpus — previously requests: was known to the schema but silently dropped.
  • New Corpus.RequestsForURL(url, method) returns every request whose route glob (and optional method) matches the observed request — “all matches apply”, per the spec. Reuses the existing route matcher (:param, trailing-slash, **).
  • Corpus, View, and RequestDef now carry JSON tags, so a corpus serializes to a lean wire form (memory / globals / views / requests); authoring-only View fields (url / access / snapshots / sourceFile / stability) are excluded. The flattened list is emitted in a stable pre-order (lexical file order, then declaration-order depth-first), locked by a test, so the wire form is reproducible.
First-run papercuts fixed:
  • Subcommand --help/-h now exits 0 and no longer prints flag: help requested — a help request is a success, not an error (#117).
  • browser status on a session file that exists but doesn’t parse (e.g. hand-written or corrupted, so no valid port) now reports it as an unrecognized format with the expected shape, instead of a misleading “server and CDP were assigned the same port (0)” collision hint. The collision hint is also gated on a real server port (#118).
  • validate no longer warns about unknown fields in tooling files it owns (survey.yaml), as it already skips config.yaml (#119).
Make browser start work — and fail legibly — on Linux and in root containers (the standard agent/CI environment):
  • FindChrome now checks the sightmap-managed Chrome for Testing install (~/.sightmap/browsers/) on Linux and Windows, not just macOS, so the documented browser install → browser start flow works. The “no Chrome found” errors now point at browser install.
  • browser start adds --no-sandbox automatically when running as root (Chrome refuses to start otherwise), and accepts --chrome-flag (repeatable) and --chrome-binary to override the launch.
  • On a startup timeout, browser start now reports the resolved binary, the full argument list, and the tail of Chrome’s stderr — instead of a bare “timed out waiting for CDP” that hid the real cause.
Three first-run papercuts:
  • validate now errors on an unsupported version: value (e.g. version: 2) — the spec requires version: 1. This is the companion to the missing-version: warning.
  • browser install --help prints usage and exits 0 instead of ignoring its arguments and starting the 184 MB Chrome-for-Testing download (the subcommand now parses its flags).
  • search names the near-miss when a pattern matches a view or request name (search covers component fields only), instead of a bare “no matches”.
Route matching: ** is now a proper globstar. As a whole path segment it matches zero or more segments, so /admin/** matches /admin itself (as the spec always stated) as well as /admin/users and deeper, and /a/**/b matches /a/b. A ** glued into a segment (e.g. /foo**) is treated as a regular single-segment * that does not cross a /. This fixes the previous behaviour where /admin/** failed to match /admin and /messages** failed to match /messages.Remove the authoring skill’s instruction to run sightmap browser register --addr localhost:PORT — that subcommand does not exist, so agents following it dead-ended. Attaching to an externally-launched browser remains a possible future feature (tracked upstream); until it lands the skill no longer documents it.report and capture errors now teach the views: file structure instead of naming a field in isolation:
  • report distinguishes “no views defined at all” from “views exist but none has a url:”, and both print a minimal views: example (with route: and url: and their roles) — previously it always said “no views with URLs found / Add a url: field”, which contradicted capture’s route-only advice.
  • capture’s “no view matches” error shows the same views: example and names route: explicitly.
The sightmap-authoring skill’s Phase 1a now shows a complete view-file example (the top-level views: list with version:/route:/url:), instead of only telling authors to “create a view file with route:” — which invited putting view fields at the file root (silently making it a globals file).Teach the corpus schema at the point authors get it wrong, instead of failing silently or generically:
  • A view field at the file root (e.g. a top-level route:/name:) now warns that view fields belong under a top-level views: list, with a short example, instead of a bare “unknown field”.
  • A view-shaped file that sets url: and components: but no views: now warns that it defines no views and its components are treated as global (previously it validated clean and silently became a globals file).
  • A missing version: now warns (the spec requires version: 1 in every corpus file).
  • validate now warns when the whole corpus has global components but no views — it can never match a view, so capture, report, and per-view coverage are unavailable.
All are advisory warnings (exit 0); correct corpora stay warning-free.
go
Released July 31, 2026
Go library: components can now carry source: (relative path to the implementing file — already schema-recognized, previously dropped by the loader) and tags: (authored classification labels, e.g. defect). Neither is inherited by children, matching memory/properties/stability’s existing convention. Tags also flows through ApplySightmap’s match result alongside Memory. New Corpus.AllComponents() returns every component in the corpus (globals plus every view’s), deduped by first-seen name — the flat, whole-corpus list a consumer building an upload payload or a lint/coverage report wants, replacing an equivalent hand-rolled loop in cmd_lint.go.
go
Released July 30, 2026
The release workflow now pushes a go/vX.Y.Z tag alongside the existing vX.Y.Z tag. This module lives in the repo’s go/ subdirectory rather than at the root, and Go’s module versioning requires a nested module’s tags to be prefixed with that subdirectory — so go get github.com/sightmap/sightmap/go@vX.Y.Z has never actually resolved to a tagged release, only to @latest’s branch-tip pseudo-version. The bare tag is unchanged and still drives everything else (goreleaser, npm publishing, the release-already-tagged check).
go
Released July 30, 2026
Go library: Corpus.Memory now carries file-level memory entries (the loader previously dropped them). Lower the module’s go directive from 1.25.2 to 1.23, its actual dependency floor, so consumers aren’t forced onto a newer toolchain than the code requires.
cli
Released July 30, 2026
Verify the changesets release automation end-to-end: no functional change.
cli
Released July 28, 2026
Offline selector matching now matches the live DOM for id, class, and SVG. Three gaps are closed so a selector sel-probe verifies live behaves the same way snapshot/coverage/capture see it offline:
  • Attribute selectors on id ([id^="issue_"], [id$=…], …) now match. id lives in a dedicated node field, so the matcher resolves id/class attribute selectors to those fields — not only to captured attrs.
  • placeholder is captured, so input[placeholder="…"] matches offline.
  • SVG classes are captured. On SVG elements className is an SVGAnimatedString, which broke the old extraction (it threw and dropped the whole selector); probe.js now uses classList, so svg.lucide, [class*="lucide"], and :has(svg.lucide-x) match offline.
The authoring skill and docs are corrected accordingly: attribute selectors on class and id are no longer described as “don’t work offline” — they work, for HTML and SVG alike.Better handling of asynchronous SPA navigation, without baking implicit waits into click.
  • navigate now reports client-side redirects. It previously only saw server-side (HTTP) redirects, which are reflected by the load event; a client-side redirect that fires during hydration (an auth guard bouncing /login → /, or / → /workspace) went unreported, so the caller was told it was somewhere it wasn’t. navigate now waits briefly after load for a follow-up navigation and prints (redirected to FINAL) for those too.
  • wait-for gains --view and --component. These are the explicit, corpus-aware step boundaries to use after an action that should navigate (the act-then-wait split Playwright and Selenium use): --view <Name> waits until the current URL resolves to a named sightmap view, and --component '<Query>' waits until a component query — including property filters like WorkItemRow[key="FALCON-7"] — matches a node. Both auto-retry until they hold or time out loudly. --url, --selector, and --load remain the raw equivalents.
click deliberately does not wait for or guess about resulting navigation — it acts, reports, and keeps its loud covered/off-screen refusals. Adds browser.AwaitNavigation (waits for Page.frameNavigated / Page.navigatedWithinDocument, settling chained redirects) behind navigate.
cli
Released July 27, 2026
Coverage and the annotated tree now agree on what counts as visible. probe.js set each node’s isVisible from that element’s own computed style, which misses ancestor-driven hiding: a control inside a closed opacity:0 overlay (a dismissed dropdown or context menu) has computed opacity:1 of its own, so it reported visible even though it is painted nowhere. Coverage counted these while the renderer dropped the hidden container’s subtree, so on real apps a large share of a page’s interactive nodes (all the closed menus) silently inflated the count and tanked the score.probe.js now computes isVisible with the browser’s own Element.checkVisibility, which accounts for ancestors hidden by display:none, visibility:hidden, opacity:0, or content-visibility — keeping the layout/rendering judgment in the real browser instead of re-deriving CSS semantics. snapshot/coverage “(visible only)” now excludes descendants of hidden containers; --include-hidden still counts every interactive node. The renderer also drops invisible subtrees regardless of interactivity so it and coverage read one signal.
cli
Released July 27, 2026
snapshot now surfaces runtime match conflicts in a [Conflicts] section — the ambiguities that only exist against a live page, complementing the static corpus-conflict warnings validate emits. Two triggers:
  • A single DOM node matched by more than one distinct component name. Matching is first-match-wins, so only one applied and the others were silently dropped — which is also why a correct-looking component can report 0 matches. The section names the node, the competing components, and which one won.
  • Two or more views matching the current URL at equal specificity, where declaration order alone decided the winner.
Adds match.FindConflicts and Corpus.TiedViews; both are computed during observe.Page. (The view-tie half is provisional pending the route-specificity decision in the functional-decomposition proposal.)validate now surfaces two classes of invalid corpus the loader previously dropped silently (so they shipped with a green check):
  • Missing required fields — a component with no selector (or no name), and a view with no name, are now errors (missing-selector / missing-name / missing-route) instead of being quietly discarded during load.
  • Unresolved $ref — a $ref naming a component that no file defines is now a ref-unresolved error (matching the spec’s MUST), instead of the reference being silently skipped.
Both exit non-zero. The spec’s diagnostic-code table documents the new error codes alongside the corpus-conflict warnings.validate now warns on unknown fields. The typed loader silently ignored any YAML key it didn’t recognize, so a typo like memroy: — or a half-baked field — vanished without a trace. validate now walks the raw YAML and emits an unknown-field warning (not an error) for any key the spec doesn’t define at its position, at any nesting depth. It warns rather than rejects, so authors can stash experimental fields (e.g. macros:) during development; recognized fields — including the reserved tooling fields access and snapshots — are never flagged, and .sightmap/config.yaml is excluded. This completes strict validate alongside the earlier required-field and $ref checks.Fix report and make the view url: field first-class. url: is now read per view (with a file-level url: as a default for views that omit their own), instead of only at the file level. Previously per-view url: was silently dropped, so report — which needs a representative URL per view — errored with no views with URLs found even when every view declared one. url: is also now part of the published schema (SEP-accepted alongside properties), so a corpus that declares it validates instead of failing additionalProperties: false.
cli
Released July 27, 2026
validate now distinguishes errors (which fail validation, exit 1) from warnings (advisory, exit 0), and warns on silent corpus conflicts that a fallback rule was resolving without telling you:
  • merge-collision-view — two or more views share a name.
  • route-conflict — two or more views share the same (normalized) route; only the first-declared applies to that URL (this is the “same-route hijack” that can silently drop a view’s components).
  • merge-collision-component — two or more root-level global components share a name with different selectors.
Findings now carry a stable code and severity. Scoped component name reuse (the same child under multiple parents) is correctly not flagged, since that is intentional. The spec’s diagnostic-code table documents the new corpus codes.Make snapshot loud about corpus-match state instead of silently omitting it. When a corpus is loaded but no view’s route matches the URL (e.g. an auth redirect to a login page), the output now shows a [No view matched] <url> notice instead of a headerless tree that looks identical to a normal snapshot — so an agent knows the page is off the map. And the [Coverage] summary is now printed whenever a corpus is applied, including for a matched view that has no components yet (every interactive node reads as an orphan); previously that case printed only the view header, leaving the documented snapshot --coverage bootstrap with nothing to iterate on.
cli
Released July 26, 2026
Make browser click and fill fail loudly instead of silently no-op’ing. click now scrolls the target to the center of the viewport and verifies its center is the top-most element there before dispatching — it errors when the target can’t be positioned in the viewport (previously an off-screen target below the fold was clicked at a coordinate that hit nothing and still exited 0) or is covered by another element (an open overlay/modal). The success confirmation reports the real post-scroll coordinates. fill now reads the value back after typing and errors when a non-empty value was typed but the field is still empty — the signature of a React-controlled input where plain typing doesn’t stick — telling you to retry with --clear.sel-probe now cross-checks the offline matcher. It queries the live DOM as before, but also runs the same selector through the offline matcher that snapshot/coverage/capture use, prints that count, and emits a ⚠ offline/live divergence warning (with a hint) when the two disagree. This closes the false-confidence trap where a selector “verified” against the live DOM is silently dead in the corpus — e.g. attribute selectors on id ([id^="…"]) match live but never offline, because id is a dedicated node field rather than a matchable attribute.
cli
Released July 26, 2026
Make browser-use feedback loud: the interaction commands (click, fill, hover, keypress, scroll, drag, wait-for, dialog, tabs resize) now echo a short confirmation on success instead of exiting silently — e.g. clicked CartNavButton @ (674,30) — so an agent driving the browser can see what happened. Several raw CDP/Go errors are now rewritten into actionable messages: a wait-for timeout reads timed out after 800ms waiting for selector "..." (not context deadline exceeded), resolving a dialog when none is open says so plainly, and fetching an evicted network response body explains that bodies are only retained briefly. snapshot now prints a note when no .sightmap corpus is found at the target directory instead of silently rendering an un-annotated tree.
cli
Released July 26, 2026
Stop green-lighting broken corpora and blank pages:
  • snapshot now exits non-zero when the corpus is present but fails to parse (a missing corpus stays non-fatal — the tree still renders), and when the observed page has 0 interactive nodes (blank or still loading). It still renders whatever it observed first. inspect and suggest now warn on a bad corpus instead of silently ignoring it.
  • Coverage no longer marks a page with 0 interactive nodes as a pass. The coverage line renders a distinct ∅ mark (instead of ✓), the coverage command counts empty captures as failures, and capture refuses to persist a blank/loading page as a view’s baseline (override with --force).
cli
Released July 26, 2026
Make corpus loading robust against two malformed inputs that previously failed badly:
  • A circular $ref chain (A → B → A, or a component that references itself) sent every corpus-loading command into infinite recursion and hung the process. Loading now detects the cycle, stops expanding, and validate reports it as a ref-circular error instead of hanging.
  • splitSelectors only balanced parentheses, so a comma inside an attribute selector or quoted string ([data-x="a,b"]) was wrongly treated as a selector-list separator and split into two dead alternatives. Splitting is now aware of [] brackets and quoted strings (with backslash escapes), matching how CSS is actually written.
Fix view route matching so it behaves as the spec documents. Trailing slashes are now normalized off both the route pattern and the URL path before matching, so a slash-free route like /*/projects matches the /acme/projects/ paths that Django, Rails, and many React-Router apps emit — previously such URLs matched no view at all, silently dropping the [View:] header, view memory, and view-scoped components. Express-style :param segments in view routes now match a single path segment (and score between a literal and * for specificity). And Corpus.ViewForURL now returns the most specific matching view — using declaration order only to break equal-specificity ties — instead of the first match in corpus order, closing a divergence between the exported library and the spec.
cli
Released July 26, 2026
Isolate concurrent browser sessions so agents working in different projects no longer cross-talk. The session file is now keyed to --sightmap-dir (it lives at <sightmap-dir>/.session) instead of a single shared $TMPDIR file, so a second browser start only ever reuses the Chrome for its own corpus rather than piggybacking on another agent’s browser. Free-port probing now checks the IPv4 loopback (where Chrome’s CDP and the sightmap server bind), and the sightmap server binds 127.0.0.1, so two daemons started on the default ports slide to non-overlapping ports instead of one daemon’s server colliding with another’s CDP port. Every session-aware browser command (including console/network, status/stop, tabs, and the low-level interactions) now accepts --sightmap-dir to select which session it talks to.Fix the Sightmap overlay getting stuck in a reload loop that flooded the captured console. The extension’s version poll compared the raw /sightmap/version JSON text against the parsed version string, so it never matched and re-fetched every few seconds. The poll now parses the response and compares the version field. Also drop the post-install chrome.runtime.reload() step: a fresh browser start always relaunches Chrome with --load-extension, which loads the new unpacked extension directly, and the hot-reload could leave the overlay’s content script uninjected until the next full restart.
cli
Released July 25, 2026
CLI teardown: reorganize the tooling around reusable library subsystems and add browser devtools.
  • Separate snapshot (observe: annotated tree + coverage) from capture (persist into a view’s set); extract the observe, coverage, viewset, and authoring packages and merge the internal Session into Corpus.
  • Make the browser start daemon the single session owner: retire --launch and the browser launch subcommand (live commands attach to a started session).
  • Add console and network devtools commands, backed by a session-lifetime collector in the daemon that buffers console messages (with uncaught exceptions folded in) and network requests, with lazy response bodies.
  • Add screenshot clipping to browser screenshot (--component / --selector / --expand-pct).