From tandem
Per-site navigation memory for tandem's shared browser. Profiles that describe a site's skeleton (routes), its durable locators, navigation recipes, and gotchas, so you navigate KNOWING instead of re-deriving the DOM each time. Use it when you start operating a site (check whether a profile already exists), when you finish a recon worth saving, or when a saved locator fails (re-record). ASSISTED recon: you draft, the human confirms, it's saved.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tandem:mapThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Persists *how a specific site is navigated* in a per-host profile. Avoids re-deriving the
Persists how a specific site is navigated in a per-host profile. Avoids re-deriving the
structure (huge snapshots, ephemeral refs) every session. Complements the
"Real navigation — patterns" section of the tandem skill: that is CROSS-site technique (infinite
scroll, cookie banners…); this is knowledge OF-a-site (the routes of THAT site).
Full design and rationale: tandem/docs/01-navigation-memory.md.
scripts/map.sh)Profiles in ~/.claude/tandem/sites/<host>.md (OUTSIDE git; override TANDEM_DATA_DIR).
The SOURCE OF TRUTH are the .md files; the index is DERIVED from their frontmatter, not kept separately.
map.sh list ............ table of profiles (host · updated · file)map.sh show <host> ..... dumps the profile (accepts a full URL: normalizes to host)map.sh path <host> ..... the file path (whether it exists or not) — for reading/writingmap.sh index ........... derived JSON index (to stdout + caches sites/index.json)Invoke it by its path within the plugin (scripts/map.sh); there's no bin on PATH for map.
PostToolUse hook on browser_navigate injects the host's profile the first time you
navigate to it in the session (scripts/hook-inject-profile.mjs). If it didn't arrive (or you want
to re-read it): map.sh show <host>. If there's no profile and the work isn't trivial → offer the
human to do a recon and save it.fingerprint.mjs check <host> <route> (structural signal, small JSON), NOT with a
targeted browser_snapshot (it weighs ~18× the whole profile). match → trust that route's sel:
values WITHOUT a snapshot (see "Frugal flow"). drift → point 4. No fingerprint yet → a one-off
targeted snapshot, and capture the fingerprint for next time.browser_* tools (targeted snapshot, browser_evaluate for data,
navigating routes). Don't dump raw DOM into the profile: abstract it.[verified YYYY-MM-DD] (you lived it now) or [hypothesis] (inferred, untested).$(map.sh path <host>). Set created/updated in the frontmatter.id if one exists; if not, role+name
from the accessibility tree. Never position/index nor eNN refs (they're ephemeral).sel: (opt-in). Besides the readable primary, a locator can carry a sel:
= a Playwright selector the agent passes as target to act WITHOUT a snapshot. Generate the
syntax with scripts/selector.mjs (NOT by hand — the quote/regex escaping is easy to mistype):
selector.mjs <role> <name> [--regex] [--anchor]. Two classes:
sel:, e.g. role=button[name="New ticket"].
For long/fragile names use --regex (substring) instead of the exact literal.sel: with a slot the recipe fills in
use, e.g. role=row[name=/^{id}/]. A common prefix (^TCK-) matches N rows → the action
is strict and throws; that's why it's parameterized to the concrete id.sel: (forcing positional CSS lies).sel: is ONLY the selector, no inline comments (recipe.mjs/selector.mjs would
include them in the value). Notes go in primary:/corroborates:.<table> that the tree flattens to
generic (no role=row/cell) and duplicate the list for responsive. Verify LIVE before
anchoring by role=row; if there's none, anchor by stable class + text= + >> visible=true. The
selector.mjs helper (v1) only covers role=…[name] — these cases are written by hand for now.Instead of trusting how old the [verified] tag is, compare the structural SKELETON
of the page against the one you saved. If it changed → that route's locators are suspect,
time to re-check/re-record (the date doesn't tell you that: a page untouched for 6 months is still valid;
one that changed yesterday is stale even if the tag is from yesterday).
Engine: scripts/fingerprint.mjs {capture|check} <host> <route-key> (JSON signals via stdin).
Sidecar sites/<host>.fingerprints.json (machine, separate from the readable profile). check
returns {status: new|match|drift, added[], removed[]} and rc 1 if there's drift; the diff tells you
EXACTLY which locator changed.
Canonical evaluate — captures only STRUCTURE (roles, landmarks, buttons, inputs, headings,
nav-links), not table data, and normalizes digits to # so counts/dates/ids don't count
as drift. Do NOT type it by hand: the SINGLE SOURCE is scripts/page-signals.mjs (versioned and
with tests). Get the exact JS and pass it to browser_evaluate verbatim:
node scripts/page-signals.mjs print # emits the self-contained () => [...] blob
Why a file and not the snippet here: changing the extraction invalidates ALL the stored fingerprints (false drift everywhere). With the single source, the algorithm can't drift without its tests catching it (
tests/page-signals.test.mjs); the blob is serialized from those same functions.
Flow: browser_evaluate with the output of page-signals.mjs print → pipe the array to
fingerprint.mjs capture|check <host> <route>.
drift → re-record-on-drift (re-derive, update the profile) and
recapture the fingerprint. match → trust the profile. new → not captured yet.Gotcha [verified 2026-06-22]: BEFORE capture/check confirm that location.pathname is the
expected route. If the session expired and you bounced to /login (or another wall), do NOT capture —
you'd save the login skeleton under the wrong route-key. It's an auth wall → the human clears it, then you repeat.
sel: without re-snapshotting)With a profile loaded, do NOT snapshot by default to act: the tree snapshot weighs ~18× the profile entire (measured: 105 KB vs 5.6 KB). The snapshot goes from default mode to honest FALLBACK.
sel: values,
fingerprint.mjs check <host> <route>. match → the sel: values are good, act by them. drift/new
→ re-derive with a targeted snapshot (and capture/recapture the fingerprint).sel:. browser_click/type/evaluate with target=<sel>. Zero snapshot.browser_wait_for on the sel before the click; otherwise the element
may not exist yet and the savings go into retries.sel: fails (doesn't resolve, or throws on ambiguity) → treat it as drift: targeted
snapshot, re-derive, update the sel: (assisted re-record, reflex 4), recapture the
fingerprint. A wrong cached sel: is worse than a slow snapshot.Fallback to snapshot = no profile, drift, or reading new unmapped data.
A recipe is a NAMED and PARAMETERIZED sequence of steps over the profile's sel: values. Flat-YAML
format under ## Recipes, compiled by scripts/recipe.mjs:
open-ticket-by-id(id):
- type: ticket-search <- {id}
- click: ticket-row <- {id}
- wait-url: /tickets/
- return: url
name(params):. Each step = action: locator [<- {param}|"literal"]. The {param}
fills the sel: TEMPLATE (the slot must be named the same as the param). v1 actions:
navigate, type, click, wait-url, return/extract. Do NOT type the Playwright syntax by
hand: recipe.mjs compiles it. When saving a recipe in recon: recipe.mjs validate <host>.recipe.mjs COMPILES, the agent EXECUTES):--step (default, observable): recipe.mjs compile <host> <recipe> <args> --step → steps
{action,target,value} → run them with browser_type/click/wait_for. The human sees each step.
Precede it with the fingerprint check gate (same as for a standalone sel:).--fast (frugal, 1 call): recipe.mjs compile … --fast → a single Playwright function. Mandatory
dry-run: show the code to the human; with their OK, run it with browser_run_code_unsafe. That
tool is RCE-equivalent → only recipes from the co-curated store, never model or page text.extracts a sensitive datum (IBAN/national ID) → that datum goes to an ACTION,
not to the context/log. The dry-run makes it visible before running.<host>.md)---
site: <host>
created: YYYY-MM-DD
updated: YYYY-MM-DD
auth: { wall: "<which wall>", cleared_by: human|claude|none }
---
# <host> — navigation profile
## Routes (skeleton) — URL pattern → what it's for
## Locators (multi-anchor) — name: sel (optional, executable) + primary + corroborates + | verified DATE
## Recipes — executable (flat-YAML): name(params) + steps `action: locator <- {param}`
## Gotchas — induced rules, each one [verified DATE] or [hypothesis]
Format example: see docs/01-navigation-memory.md.
npx claudepluginhub bgmacris/quimera-ai --plugin tandemCreates, edits, and verifies skills using a test-driven development approach with pressure scenarios and subagents.