Changelog

What changed, pack by pack.

Every release of every Heavy Artillery Skills pack, newest first. Your account on the shop always has the current version of each pack you own, ready to download.

Major
Skills moved out of the pack
Minor
A new capability, or a changed default
Patch
Fixes and documentation

Showing all 47 releases.

  1. 2 releases

    Campaign Tracker Pack 1.0.0

    First release

    The first release: one skill, campaign-tracker, that sets up campaign tracking for a client site the same way every time and proves it live.

    5 changes in detail
    • The Worker (worker/): short links from the sheet's published CSV, checked by exact hostname so a link can never point off the client's own hosts; the browser snippet at /t/ct.js (at most 4.5 KB gzipped); /t/lead and /t/checkout, same-origin only and rate limited; signed SureCart and generic purchase webhooks, stored once per order and per event; the Meta Conversions API for Lead and Purchase; read endpoints for the sheet, behind one read-only key (CT_API_KEY); and the writes, behind a second key that only the owner keeps (CT_ADMIN_KEY): /t/api/forget for deletion requests, and the test lead and test visit verify sends. Neither key does the other's job, so the key a sheet holds cannot delete a record or send a test event; health reports whether the admin key is set and warns when it equals the read key. Leads are stored as a salted hash, never an email or a name.
    • Ad visits, off by default: with ad_visits on, the Worker sends Meta a server-side PageView for each page view of a tab that arrived from a Meta ad (it requires the fbclid itself), so the client can build an audience of ad visitors who did not convert without a pixel. D1 keeps only daily counters, never an IP, user agent or click id.
    • The sheet (sheet/): Link Builder, Short Links, the Paid Template for Meta ads, Results, Weekly and Monthly, reading every number through the Worker, so it never holds a vendor token.
    • The scripts: intake.mjs, provision.mjs, verify.mjs and audit.mjs run the steps for each client from one registry file; forget.mjs handles a deletion request, a dry run unless --confirm; sheet-bundle.mjs writes the sheet's code as three files to paste into a blank sheet; and worker-check.mjs, snippet-check.mjs and sheet-check.mjs test the Worker, the snippet and the sheet's code in Node. provision.mjs gen-secret CT_ADMIN_KEY --out <file> writes the admin key only to that file; verify.mjs proves the read key cannot write and the admin key cannot read, and --final fails while the admin key is unset or equal to the read key; audit.mjs flags the same. Selftests: intake 23, provision 27, verify 26, audit 9, forget 12, sheet-bundle 9, worker-check 381 (on Node 22.13 or newer), snippet-check 60 and sheet-check 149 cases.
    • ci/campaign-tracker-skills.mjs runs every selftest with the case count the skill states, requires each script to refuse a run with no arguments, and breaks one guard at a time in a copy, 28 in all (among them the short-link host check, email normalization, the secret and signature compares, the origin check, the purchase, event and lead dedupe, the ad-visit click-id requirement, the internal-address pattern, the _ct strip, the forget route's key check and its dry run, and the admin key: the admin guard accepting the read key, and the forget route or forget.mjs taking it), to prove the selftest notices.

    Measured before this release (2026-10-18 and 2026-10-19): the Worker end to end on a store and the site that sells into it, then a second site, including a SureCart test-mode order joined to its checkout, and the sheet in two real Google Sheets. Not yet measured: a live-mode SureCart order, the generic webhook from a real checkout, the sheet's daily trigger, and each form tool on a real install. The skill grades each of these and says how to check it.

    All-Access Bundle 2.5.0

    Minor

    A new pack joins the bundle: the Campaign Tracker Pack 1.0.0, one skill, campaign-tracker, that sets up campaign tracking for a client site the same way every time and proves it live. The bundle takes the same move, a new minor version. The other five packs' versions are unchanged; their READMEs now count six packs.

    Pack versions inside
    PluginVersion
    spotter-round2.2.0
    agent-setup1.3.0
    email-pack1.1.4
    wordpress-pack2.2.0
    app-stack1.2.2
    campaign-tracker1.0.0
  2. 4 releases

    Email Pack 1.1.4

    Patch

    Two layout fixes in the Supabase template generator, each now refused by its build, and a corrected button example in html-email. Measured 2026-10-01 on a production project built from this generator.

    Upgrading: copy the new supabase-email-templates/scripts/build-email-templates.mjs over yours, carry your marked blocks across (BRAND, F, C, D, LOCKUP, TILES, LABELS, TEMPLATES), build, and push. Every template's content changes, so the dry run shows each one differ.

    3 changes in detail
    • No gap under the logo. The header band's lockup link was an inline-block, so it sat on its cell's text line and kept 4px under the logo at every width. It is a block now, and the build refuses any link that holds only an image and is not display:block.
    • A long address fits a phone. An address set apart in the copy was held to one line, so a 34-character address made the templates that show one wider than a 320px screen, and a 40-character one wider than 375px. It now breaks only where the line leaves no other choice. The build refuses any element holding a variable other than {{ .Token }} that cannot wrap. The {{ .Email }} placeholder is 12 characters, so only a render with real-length values shows this; the skill now says so.
    • html-email §2a rule 4: the button example drew its link as display:inline-block, which keeps the line height under a link that holds only an image: every button 8 to 9px taller than drawn on desktop widths, all of it under the label. The example is now display:block, with how to check for it: render above the narrow breakpoint, and clear a blocked image's alt text before measuring.

    WordPress Pack 2.2.0

    Minor

    A new skill and two SureCart additions: a new minor version. No existing script or check changed behavior.

    4 changes in detail
    • wordpress-login-screens, new: WordPress's sign-in screens in the site's design. login-theme.php and login-theme.css restyle wp-login.php (log in, lost and reset password, logged out) on any theme, with the site's own logo, or its name set as text when it has none, so the WordPress mark never shows. fortress-theme.css does the same for the Fortress security plugin's two-factor, recovery, magic-link and error pages, which the theme cannot reach, through Fortress's one hook: a top-level theme_css_file in its config. The skill covers the config placement (inside a module block, it fails the config test), the reload without which nothing changes, and the ?v= that must be bumped in the config on every edit of the file, or a CDN keeps serving the old copy (measured: a cache hit served bytes older than the file). check-fortress-theme.mjs proves the live Fortress page links your file and serves your current bytes; its --selftest runs 12 cases. Measured on WordPress 7.1 and Fortress's live magic-link page at 320 to 1280px: no overflow, every text contrast pair at or above 4.5:1, no stranded words.
    • SureCart's customer dashboard in a dark site's palette (surecart-dashboard-dark.php + surecart-dashboard-dark.css): the dashboard is SureCart's own page template, not the theme's, and on a dark site its headings came out white on white. The pair darkens it by page, as the instant checkout already was, including its login form. Measured over a live dashboard: 0 contrast failures across 104 text runs, no overflow. The SureCart reference gains §1.10 and §1.11.
    • surecart-required-asterisk.css, new, for any theme: a required checkbox's red asterisk (the checkout's terms box) dropped onto a line of its own at some widths. One rule keeps it on the label's last word: 125 of 561 widths wrong before, 0 after. Reference §6.2.
    • ci/login-screens-skills.mjs, new, holds the new skill to its claims (21 checks: the selftest count, copies of the checker with each verdict removed, the PHP against stubbed WordPress, the shared tokens). ci/wordpress-skills.mjs adds the dashboard and asterisk files (53 checks, was 50).

    All-Access Bundle 2.4.0

    Minor

    The WordPress Pack takes a minor version: a new skill, wordpress-login-screens, puts wp-login.php and the Fortress security plugin's two-factor pages on brand, and SureCart's customer dashboard gets a dark-site treatment. The bundle takes the same move, a new minor version. Nothing else changed.

    Pack versions inside
    PluginVersion
    spotter-round2.2.0
    agent-setup1.3.0
    email-pack1.1.4
    wordpress-pack2.2.0
    app-stack1.2.2

    All-Access Bundle 2.3.1

    Patch

    The Email Pack takes a patch: the Supabase template generator leaves no gap under the logo and wraps a long address on a phone, each now refused by its build, and html-email's button example draws its link as a block. The bundle takes the same move, a patch. Nothing else changed.

    Pack versions inside
    PluginVersion
    spotter-round2.2.0
    agent-setup1.3.0
    email-pack1.1.4
    wordpress-pack2.1.2
    app-stack1.2.2
  3. 9 releases

    Spotter Round 2.2.0

    Minor

    served.mjs checks a staging site behind Cloudflare Access, writes a result file, and says when it could not look. A pack that gains a capability takes a new minor version.

    6 changes in detail

    post-deploy-proof

    • A URL that gets no answer now exits 3, printed as UNREACHED: DNS, the connection, TLS, or nothing within --timeout (20 seconds by default, new). Before, the run crashed with a stack trace and exit 1, which read as a finding. It is never reported as ORIGIN.
    • A manifest with no checks now exits 3 ("the manifest holds no checks, so nothing was measured"). Before, it printed "0 URL(s) serving what you expect" and exited 0.
    • The command line is strict. An unknown flag, a flag without its value, a flag given twice (--access may repeat), --manifest with --url, --equals or --contains with a manifest, and --selftest beside anything else are each exit 2 with one line, before any request. Before, --acess example.com was ignored and the run passed.
    • --access <host> (new, repeatable), for a staging site behind Cloudflare Access: the service token's pair, from CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET in your environment, goes to that host only, over https, and is never printed or written. BLOCKED when Access does not take the token.
    • --json <file> (new) writes one result file, check-result@1, whole or not at all, mode 600.
    • served.mjs --selftest: 28 cases, was 16. ci/hosting-skills.mjs breaks three more copies of the script that the selftest must catch.

    Agent Setup Pack 1.3.0

    Minor

    A new recommended global rule keeps long agent sessions cheap: compact early, at a break. A pack that gains a capability takes a new minor version.

    4 changes in detail

    universal-agent-setup

    • New habit block, context ("Long sessions: compact early, at a break"). Every turn re-sends the whole conversation, and a cache makes that cheaper, not free. The block says to compact at about 40% of the model's context window, never below 150K: after a compaction every request still carries a base of typically 40–60K tokens, and compacting pays only when it leaves about 100K of room above that. It also says to compact at natural breaks and say what to keep, to compact or clear before a long idle because the cache expires, to keep a long task's state in files, and to send verbose work to subagents.
    • Opt in with ./install.sh --global <file> --blocks context (or habit, or all), or paste the block from agents-md/global-snippet.md. The default core selection is unchanged, and --check reports nothing for files that carry none of it.
    • Part 1c of the skill sets the point in each tool, from each tool's own documentation and source, read 2026-09-28: Claude Code's /autocompact (saved as autoCompactWindow), --autocompact for one launch and CLAUDE_CODE_AUTO_COMPACT_WINDOW (a plain integer: 400k reads as 400 and clamps to the 100K minimum); Codex's model_auto_compact_token_limit in ~/.codex/config.toml (unset, 90% of the window); Gemini CLI's model.compressionThreshold in ~/.gemini/settings.json (a fraction, default 0.5). It shows how to measure your own base right after a compaction in each tool, and when to raise the point above the table.
    • ci/agents-skills.mjs checks that agents-md/README.md names every global rule block in its own group, and that a copy of the table missing one fails: 48 checks (was 46).

    Agent Setup Pack 1.2.0

    Minor

    secret.sh set-batch takes several prepared keys in one sitting. A pack that gains a capability takes a new minor version.

    4 changes in detail

    secret-handoff

    • secret.sh set-batch --list <file> (new): one line per key, name<TAB>shape<TAB>prompt, all checked before anything is asked. Each value is read hidden, as set reads it, with [i/n] <prompt> above; echo stays off from the first prompt to the end, so nothing typed between prompts is drawn.
    • A key already set is kept unless --replace. A refused value stops the batch with exit 1: values saved before it stay, and the same line run again skips them.
    • Without a terminal it exits 2 and prints the line to hand to a person. --stdin takes exactly one line per entry, for automation that already holds the values; everything is read before anything is saved.
    • set and set-batch share one check-and-save path, so every existing check covers both. The selftest grows from 42 to 50 cases; ci/secrets-skills.mjs adds two hollow copies and a pseudo-terminal run of set-batch.

    Email Pack 1.1.3

    Patch

    Documentation only: no guard, script or check changed, and the built templates are byte-identical to 1.1.2's.

    2 changes in detail
    • html-email: "Two dark modes, never one." The Gmail app and Apple Mail darken the same message differently (measured from one delivered message on iOS: the card arrived a neutral gray in Gmail and a blue-black in Apple Mail, with the locked band identical in both), and no markup can make them match. The skill says why each obvious fix fails, and that copy a buyer or reader sees must say an email reads right in light and dark mode in the clients tested, never that it looks identical in every client.
    • Links colored by the client: the Gmail app links a bare domain in its own blue, and Apple Mail's detected links can take the dark block's link color; the skill gives the reset to repeat inside the dark block, and says to write your own domain as a styled link to keep your color.

    WordPress Pack 2.1.2

    Patch

    A SureCart fix for any theme: a new snippet and its notes. No existing skill text, script or check changed behavior.

    4 changes in detail
    • surecart-checkout-veil.css, new: on a phone, a SureCart checkout was a few pixels wider than the screen while it loaded, so the page could be dragged sideways. SureCart's own loading veil sits 5px outside the checkout on every side; this one rule keeps it inside. It works with any theme, not only Bricks: paste it into the theme's stylesheet or Additional CSS. Measured on SureCart 4.9.0: 92 of 300 samples too wide at 375px before, 0 of 300 at 320, 375 and 414px after.
    • The SureCart reference gains §6, "SureCart's own components, on any theme", with this fix written up as symptom, cause, rule and measurement, and how to catch it (an overflow check run after the page settles misses it, because the veil is gone by then).
    • The skill's router and scripts table point to the snippet, and "SureCart on any theme" now loads the skill.
    • ci/wordpress-skills.mjs holds the snippet to four checks, each proved able to fail by a planted break (50 checks, was 46).

    App Stack Pack 1.2.2

    Patch

    Documentation only: no skill script or check changed.

    1 change in detail
    • surecontact-integration, gotcha 27: a form submission is stored with its HTML tags stripped (measured through the public submit API): bold and links typed into a field arrive as plain words, so no merge tag can carry live markup into an email. That is what makes it safe to echo a submitted name back in a confirmation email. The gotcha also names the limits: a bare web address survives as text and a mail client may link it, and only the submit API was measured.

    All-Access Bundle 2.3.0

    Minor

    The Agent Setup Pack takes a minor version: a new recommended global rule, context, keeps long agent sessions cheap (compact early, at a break), and universal-agent-setup gives the setting for each tool. The bundle takes the same move, a new minor version. Nothing else changed.

    Pack versions inside
    PluginVersion
    spotter-round2.2.0
    agent-setup1.3.0
    email-pack1.1.3
    wordpress-pack2.1.2
    app-stack1.2.2

    All-Access Bundle 2.2.1

    Patch

    Three packs take a patch. The WordPress Pack adds a SureCart snippet for any theme that keeps the checkout's loading veil inside a phone's screen; the Email Pack's and the App Stack's changes are documentation (two dark modes in the email skill, and SureContact gotcha 27). Each pack's changelog has the detail.

    Pack versions inside
    PluginVersion
    spotter-round2.2.0
    agent-setup1.2.0
    email-pack1.1.3
    wordpress-pack2.1.2
    app-stack1.2.2

    All-Access Bundle 2.2.0

    Minor

    Spotter Round 2.2.0: served.mjs checks a staging site behind Cloudflare Access, writes a result file, and exits 3 when it could not look. Agent Setup 1.2.0: secret.sh set-batch takes several prepared keys in one sitting. The bundle takes the larger of the two moves, a new minor version.

    Pack versions inside
    PluginVersion
    spotter-round2.2.0
    agent-setup1.2.0
    email-pack1.1.2
    wordpress-pack2.1.1
    app-stack1.2.1
  4. 3 releases

    Email Pack 1.1.2

    Patch

    Maintenance only: no skill, script or check in this pack changed. The pack's copy of ci/verify.sh, the harness every pack ships, now matches the other packs' and the All-Access Bundle's byte for byte. The only difference was two checks of the Agent Setup Pack's peer detector, which run only when that pack's multi-agent-workflow skill is installed alongside this one.

    App Stack Pack 1.2.1

    Patch

    Maintenance only: no skill, script or check in this pack changed. The pack's copy of ci/verify.sh, the harness every pack ships, now matches the other packs' and the All-Access Bundle's byte for byte. The only difference was two checks of the Agent Setup Pack's peer detector, which run only when that pack's multi-agent-workflow skill is installed alongside this one.

    All-Access Bundle 2.1.4

    Patch

    The App Stack Pack and the Email Pack take a maintenance patch: their ci/verify.sh now matches every other pack's, so each folder under plugins/ is again byte for byte the zip that pack sells as. No skill, script or check changed. This README now also says that programs, which add a maintained starter and support, are sold separately.

    Pack versions inside
    PluginVersion
    spotter-round2.1.1
    agent-setup1.1.1
    email-pack1.1.2
    wordpress-pack2.1.1
    app-stack1.2.1
  5. 12 releases

    Spotter Round 2.1.1

    Patch
    3 changes in detail

    build-ab-harness

    • lh.mjs summarize no longer stops halfway on a slow server. Lighthouse 12's server-response insight holds its rows as a checklist object, not a list, and reading it as a list threw TypeError: row.items.slice is not a function after the reports were written, so a run that measured everything printed no summary (seen 2026-09-26 on a live store). The checklist rows now print, as ok or no beside each.
    • One run over several hosts keeps each host's reports. Report files were named by path alone, so / on two sites was one file and the second measurement silently replaced the first. When a run spans hosts, each name carries its host; a single-host run keeps the names it had. compare keeps two hosts in one label apart the same way, while an A/B's two ports are still matched by path.
    • lh.mjs --selftest (new): 11 cases, from report shapes planted in a temporary folder, with no browser or network. ci/browser-skills.mjs runs it even without Playwright, and breaks four copies of the script, one per behavior above, that the selftest must catch (38 checks, was 33).

    Spotter Round 2.1.0

    Minor

    The layout detector measures what it used to skip, two measuring scripts join, and the browser checks stop hanging on pages that never go quiet. A pack that gains a capability takes a new minor version. The free tier, verification-protocol alone, takes this version too.

    19 changes in detail

    layout-proof

    • orphans.mjs measures whole blocks, walking inline children, so a heading set one span per word is measured. It used to skip any element without text of its own.
    • Two-word blocks and clamped lines are reported apart, not counted: a two-word title has no better arrangement, and a word hidden under a line clamp is the clamp cutting. Three-word headings still count.
    • Hidden text is counted by reason (the hidden attribute, display none, visibility hidden, screen-reader only, zero size), so the report says how much of the page it could not see.
    • The second pass turns pretty off at every width, not only the last, never touches balance, and proves a zero: it reads the page's stylesheets first, media queries included, and a pretty rule with nothing switched makes the run "not evidence".
    • A control runs inside the audited page in both passes, beside the twin pair, and a failed control exits 3 ("not evidence"), apart from 1 ("findings").
    • An error page is refused (HTTP 400 and up), where it used to be audited and called clean. The page is measured once it has loaded and the network has had up to 8 seconds to go quiet. A --selector that matches nothing, --step 0 (which looped forever), backwards --widths and unknown options are refused; --json works. The selftest runs 33 cases (was 8).
    • Guidance corrected where it was wrong or dated:
      • At display size, use balance first and let a stranded word stand; glue only short final pairs in body copy (a glued pair made a 409px block in a 268px box).
      • balance never adds a line: 0 of 2,328 heading-and-width pairs in each of Chromium, Firefox and WebKit (measured 2026-09-25). The claim that it can turn two lines into three is gone.
      • Safari 26 applies pretty, so a WebKit pass hides orphans too (engine table measured through Playwright 1.63: Chromium 153 and WebKit 26.6 apply both, Firefox 155 only balance).
      • The heading that stranded at 46 widths had three words, not two, and three words has a fix.
      • Step the viewport, not a container: fluid type moves with the viewport.
      • Check the longest single word against the narrowest column; test that a font has U+2011 before using it.

    visual-fidelity-gate

    • New element-size.mjs: the size each image is actually rendered at, per viewport, for writing a true sizes; with --baseline, every image box must match the live site's (25 cases).
    • The browser scripts no longer hang on a busy page. shots.mjs, engines.mjs and cls-scroll.mjs wait for load, then up to 8 seconds for the network, instead of network idle, which never arrives with an autoplaying video or a bot-check widget (observed on one site, 2026-09-19: three pages timed out at 45 seconds and dropped out of the run). An HTTP error page is a failed load.
    • engines.mjs reports a failed route and keeps going (it crashed at the first failure), takes --routes, and still fails the run. shots.mjs exits 1 on a failed load or capture instead of skipping it silently.
    • Scrolling from code turns smooth scrolling off: with it on, the scroll-through read 39 to 262 in-between positions (measured).
    • Usage examples a find-and-replace had broken are fixed: --name-attr data-name, and .hero{--src:var(--src-fb)!important}.
    • New methods: the <picture> fix needs both rules; an alpha-channel mask encoded as AVIF with a WebP fallback; a sizes value smaller than its slot changes the layout; the forced-fallback and zero-pending gate rows.

    build-ab-harness

    • New bytes.mjs, a byte census of what a phone and a desktop download, by category, including the CSS mask and background-image weight Lighthouse has no audit for (22 cases); and references/lighthouse-12-audits.md.
    • New methods: which routes to measure and what to record before changing anything; the harness can create the defect it measures (a server sending no-store put a 0.0145 shift on a page that measured 0.0000 in production); build in the mode a repository commits; preload the LCP image with type= and decide its priority live.
    • ab.sh --help prints its usage, and a broken sentence in its header is fixed.

    verification-protocol

    • Count React's expression boundaries before trusting the lost-space search, with the {" "} fix; scroll each page before reading the console; the contrast probe's other blind spots, and a before-and-after comparison of failures; the two-pass orphan sweep and its control; the word-list method for spelling, JavaScript chunks included; the artboard cause (stored 1/64px short, measured 2026-09-10) and exporting at 2x. It no longer says layout-proof does the centered-row check, which it does not.

    Across the pack

    • The two _lib.mjs copies are held byte-identical by the release gate, and no longer carry a real site's folder name and route.
    • ci/browser-skills.mjs runs the three browser selftests and 30 deliberately broken copies.

    Agent Setup Pack 1.1.1

    Patch
    2 changes in detail

    multi-agent-workflow

    • The session-start report is headed with what the detector found. In the main checkout of a repository with a linked worktree, it rightly reported the checkout as shared, but under the header PEER AI SESSIONS DETECTED when it had found no session at all, so every session started there opened with it (seen 2026-09-26). That report is now headed SHARED MAIN CHECKOUT and says no live peer session was found; the peer header appears only when a session was. Exit codes are unchanged.
    • ci/verify.sh asserts both headers against the planted fixtures (37 assertions, was 35); the 1.1.0 detector fails the new one.

    Agent Setup Pack 1.1.0

    Minor

    secret.sh installs as the secret command, and the peer detector's hooks name their events. A pack that gains a capability takes a new minor version.

    10 changes in detail

    secret-handoff

    • secret.sh answers as secret on your PATH: secret <name> fetches a value, and --list (-l), --path (-p) and --check are the short forms of list, path and check. A word is read as a name only when it stands alone, so a mistyped command followed by a name (chek demo.token) is refused, not fetched. The skill has a section on installing it under that name.
    • A store reached through a linked folder works. The permission check read the link's own mode (755 on macOS) and refused the store (measured 2026-09-25); it now reads the folder the link points to.
    • list and check skip a file not named like a secret, such as a README.md, and say how many they skipped, instead of refusing the store.
    • Pending rotations: when the store holds a ROTATE-ME.md task list, list and check end with pending rotations: N open — see <path>, where N counts the lines that begin - [ ]. The file is counted, never printed, and never read as a secret.
    • Changed: list and check print their report on stdout, so it pipes (secret --list | grep stripe). Errors, prompts and warnings stay on stderr, and get still prints nothing but the value. If a script of yours read the report from stderr, switch it.
    • The selftest runs 42 cases (was 34).

    multi-agent-workflow

    • The hooks name their events: hooks/hooks.json runs peers.sh --hook --session-start and peers.sh --hook --commit-check. If you pasted the hook settings from the skill, update those two lines. With no event named, hook mode now exits 0 silently; before, it fell through to the commit check and could ask about a command that was not a commit.
    • At session start without jq, the detector says cannot check and names jq, instead of printing nothing, which read as "no peers".
    • AGENT_GIT_GUARD names the commit guard when it does not sit beside peers.sh, and the skill says why the three scripts belong in one folder. The cannot check hint prints a path that runs.
    • New practices: check staged changes by name and by content; note the files already dirty at session start and never revert, stage or commit them; move work between worktrees with its untracked files; link node_modules into a worktree only after comparing lockfiles (a .gitignore line node_modules/, with the slash, does not ignore a link of that name; measured, git 2.50); hand over tightly coupled work instead of splitting it; the two narrow exceptions to "never in a shared checkout", each after proof; and two more blind spots (the guards fire on commits, not on reset --hard, stash or add -A; a session started before the hooks were installed is unprotected).

    Email Pack 1.1.1

    Patch

    Documentation only: no guard or check changed, and the built templates are byte-identical to 1.1.0's.

    2 changes in detail
    • supabase-email-templates: config push is safe only when config.toml mirrors every Auth setting of the hosted project. The generator's phone block now names its reasons: 20px gutters, a 23px heading so a three-word heading holds one line, 32px codes, and label and value rows stacked.
    • US spelling in three script comments.

    WordPress Pack 2.1.1

    Patch

    Documentation only: no skill, script or check changed. This changelog now describes the AI section builder the skill covers without naming it.

    WordPress Pack 2.1.0

    Minor

    The page review and the font check no longer hang on a busy page, and the orphan pass keeps balance. Two defaults changed, so a run can report more than it did: a new minor version.

    4 changes in detail
    • page-review.mjs and font-check.mjs wait for the page to settle: load, then up to 8 seconds for the network to go quiet, continuing either way. Strict network idle never arrives on a page with an autoplaying video or a bot-check widget, so both failed after 45 seconds with "could not load" (the same wait timed out three pages of one site on 2026-09-19). --wait still demands strict idle, and a timeout names --wait and --timeout. Changed default: --wait settle.
    • The orphan pass turns off only pretty. It turned off balance too, which models a browser almost nobody runs and re-reports every balance fix; a run that loses a balanced element is now reported as not evidence.
    • Three-word headings are checked. The minimum was four words, and a three-word heading stranded a word at 46 widths on one site (measured 2026-08-29), which has a fix. Two-word splits are listed apart and counted only with --min-words 2. Changed default: --min-words 3.
    • The selftests run 42 cases for the page review (was 36) and 31 for the font check (was 27).

    App Stack Pack 1.2.0

    Minor

    The _redirects guard the Cloudflare skill described now ships, and the Stripe skill's mode line no longer misreads an account's name. A pack that gains a capability takes a new minor version.

    5 changes in detail
    • cloudflare-pages: scripts/redirects-limits.mjs, the build guard §2 told you to write. It fails a _redirects file with more than 2,000 static or 100 dynamic rules, an exact path after a wildcard, or a rule over 1,000 characters; prints static <n>/2000 and warns at 90%; and notes a rule whose source names a host, because Pages ignores those lines. Exit 0 pass, 1 finding, 2 usage, 3 environment; --selftest runs 23 cases. The skill now opens with a table of its scripts.
    • cloudflare-pages: the exact build errors, so an agent can match them: a lockfile missing @emnapi/runtime, @img/sharp-* or @tailwindcss/oxide-* (observed 2026-08-06), and Playwright's Executable doesn't exist at /opt/buildhome/… or ERR_PACKAGE_PATH_NOT_EXPORTED (observed 2026-08-28). For a page whose animations tween from a start state, set reducedMotion: 'reduce' on the Playwright context.
    • stripe-on-supabase: the mode line read "live" from an account's name. §2 took the CLI's mode from a search over the whole active line, so a test-mode account named "Delivery Co" read as live (measured 2026-09-25). It now cuts the line after the account id. billing-check.mjs has a new rule, stripe-cli-mode, that finds the old line in your own scripts; its selftest runs 66 cases.
    • stripe-on-supabase §2 adds a two-read account guard: the CLI's active account and the key's account must match (a mismatch is the stale-config failure, observed 2026-08-29), then the account and mode you expected. §8: branding through the API needs a broad live key, so drag the two files into the Dashboard instead; one accent passed at 5.24:1 after darkening the same hue (measured 2026-08-30); and turn on Contact information in Checkout, with the refund policy if you state a refund window.
    • supabase-passkeys §5: cases 2 to 7 need TOTP on in the local config, enroll_enabled and verify_enabled under [auth.mfa.totp] (observed 2026-09-25).

    All-Access Bundle 2.1.3

    Patch

    Spotter Round takes a patch: the A/B harness's Lighthouse reader handles every report shape, and one run over several hosts keeps each host's reports. Nothing else changed.

    Pack versions inside
    PluginVersion
    spotter-round2.1.1
    agent-setup1.1.1
    email-pack1.1.1
    wordpress-pack2.1.1
    app-stack1.2.0

    All-Access Bundle 2.1.2

    Patch

    The WordPress Pack takes a documentation-only patch: its changelog wording. Nothing else changed.

    Pack versions inside
    PluginVersion
    spotter-round2.1.0
    agent-setup1.1.1
    email-pack1.1.1
    wordpress-pack2.1.1
    app-stack1.2.0

    All-Access Bundle 2.1.1

    Patch

    The Agent Setup Pack takes a patch: the peer detector's session-start report no longer says PEER AI SESSIONS DETECTED in a shared main checkout where it found no session; it says SHARED MAIN CHECKOUT. Nothing else changed.

    Pack versions inside
    PluginVersion
    spotter-round2.1.0
    agent-setup1.1.1
    email-pack1.1.1
    wordpress-pack2.1.0
    app-stack1.2.0

    All-Access Bundle 2.1.0

    Minor

    Every pack moves, none by more than a minor version, so the bundle takes a minor version. Spotter Round's layout detector measures whole blocks and proves its zeros, two measuring scripts join (element-size.mjs, bytes.mjs), and the browser checks stop hanging on pages that never go quiet. The Agent Setup Pack's secret.sh installs as secret, and its peer-detector hooks name their events. The App Stack Pack ships the _redirects guard, and its Stripe skill no longer reads "live" from an account's name. The WordPress Pack's page review settles instead of hanging, and its orphan pass keeps balance. The Email Pack's changes are documentation only.

    Pack versions inside
    PluginVersion
    spotter-round2.1.0
    agent-setup1.1.0
    email-pack1.1.1
    wordpress-pack2.1.0
    app-stack1.2.0
  6. 12 releases

    Spotter Round 2.0.1

    Patch

    Documentation only: no skill, script or check changed.

    2 changes in detail
    • The README's list of the other packs says where each skill is now: SureContact and OttoKit are in the App Stack Pack, because neither is limited to WordPress, and the WordPress Pack is Bricks 2.4 and later, Automatic.css and SureCart.
    • The free tier, which takes this pack's version, no longer says in its README that Spotter Round checks every pages.dev copy of your site stays out of search. That check is cloudflare-pages, which moved to the App Stack Pack before 2.0.0 shipped.

    Spotter Round 2.0.0

    Major

    Spotter Round is the verification pack now: one job, done thoroughly. Up to 1.3.0 it also carried the agent-setup, email, CRM and automation skills; each is now its own pack, sold separately, and the All-Access Bundle carries every pack. A buyer of 1.x gets the bundle.

    14 changes in detail

    What moved, and where (skill names unchanged)

    • universal-agent-setup, multi-agent-workflow, the hooks, agents-md/ and docs/how-it-works.md: the Agent Setup Pack.
    • html-email, supabase-email-templates: the Email Pack.
    • surecontact-integration, ottokit-integration: the WordPress Pack.
    • A plain-skills install of 1.x keeps its copies of these in your skills directory until you remove them; 2.0.0's installer does not touch them.

    spotter-start (was start-here)

    • Renamed so it cannot collide with another vendor's start-here in a shared skills directory. The 1.x installer moved a same-named skill aside as start-here.replaced.<time>: if you had one, restore it from there.
    • Routes the core's checks, plus the new deploy skill, post-deploy-proof. The other packs' skills are listed under "if installed", with the instruction to say a skill is absent rather than improvise its checks. That includes cloudflare-pages, prepared for this release and shipped instead in the App Stack Pack, because only a Cloudflare Pages site can use it: every skill left here works on any host.

    post-deploy-proof (new)

    A build-time check proves what you generate; the public gets what the edge serves. A site's routing gate asserted 42 invariants about its generated robots.txt and passed while crawlers were served a four-hour-old cached copy (observed once).

    • scripts/served.mjs fetches every URL twice, plain and cache-busted, so a stale edge copy and a deploy that never landed get different verdicts: they need opposite fixes. It checks exact bodies, text, contains and absent, status, redirect targets and headers, on any host, from flags or a manifest. --selftest stands up a local server with planted defects and runs 16 cases.

    verification-protocol

    • Its three pointers to layout-proof and build-ab-harness say "if installed", because the free tier carries this file alone. The free and paid copies are still one file.
    • Its introduction no longer says item 9 (generated images at delivery size) has a runnable check: none ships yet. It is on the roadmap as the share-preview check.

    build-ab-harness

    • ab.sh looks for the peer detector where an install would put it, then on PATH, and names what it is missing when there is none: the detector ships in the Agent Setup Pack now.
    • It gets Lighthouse through npx --yes lighthouse@12, which nothing said: the first run downloads Lighthouse 12 from the npm registry, and each later run asks the registry for a newer 12.x, unless LH_BIN points at a copy you installed or the project you run it from already has Lighthouse 12 in its node_modules (measured with npm 11). The skill now says so, and so do the README's list of what the scripts touch and the router, which had said every request goes to a URL you name.

    Packaging and CI

    • Every pack is built from one flat skills/ tree by release/build-pack.sh <slug>, from its manifest in packs/<slug>/; the bundle by release/build-bundle.sh; all of them twice, byte-compared, by release/build-all.sh.
    • ci/verify.sh knows which pack it is checking. It asserts the pack's skills against the manifest first, then reports each step as ran, skipped (a prerequisite is missing, which blocks a release), or not applicable to this pack, and ends with the count of each.
    • New checks: ci/pack-closure.mjs (no pack leans on a skill it does not ship), ci/hosting-skills.mjs (both deploy skills: selftests, bad command lines, mutations their selftests must catch), a step that fails any --selftest no check runs, and layout-proof's own control, which never ran in CI before. ci/pack-controls.sh plants a defect for each packaging check and requires it to fail.

    Agent Setup Pack 1.0.1

    Patch

    Documentation only: no skill, script, hook or check changed.

    1 change in detail
    • The README's list of the other packs says where each skill is now: SureContact and OttoKit are in the App Stack Pack, because neither is limited to WordPress, and the WordPress Pack is Bricks 2.4 and later, Automatic.css and SureCart.

    Agent Setup Pack 1.0.0

    First release

    The agent-setup skills from Spotter Round 1.0.0–1.3.0, as their own pack, with a new check of what each tool loads, a way to make Claude Code read AGENTS.md, and a new skill for handing an agent a secret.

    11 changes in detail

    secret-handoff (new)

    An agent that needs an API key used to get it pasted into the chat, or handed one command that both prompts for it and saves it. Pasted into bash 3.2 as a block, that command saved nothing and ran the value pasted next as a command, into the shell history (measured 2026-09-24).

    • Two steps, one line each, run unedited: prepare creates the private folder and file (700 and 600) and prints the exact second line; set asks with typing hidden, refuses before asking when step 1 was not run, refuses an empty value and a multi-line paste, and confirms only the length and whether an expected shape matched. It reads byte by byte, because a plain hidden read hangs on macOS from 1024 bytes and cuts the value at 4095 on Linux (measured).
    • How the agent uses a value without reading it (only inside the one command that needs it, never with tracing or verbose flags), what the approach does not protect against, and what to do once a value has leaked. import takes a multi-line key file; check audits the whole store's permissions.
    • scripts/secret.sh: prepare, set, get, check, list, path, import, rm; --selftest runs 34 cases in a temporary store, on macOS bash 3.2 and Linux bash 5.

    universal-agent-setup

    • Claude Code reads a repository's AGENTS.md from version 2.1.277 (2026-09-18), but only where no CLAUDE.md exists in the working folder or above it. Where both exist it reads CLAUDE.md alone, unless that file imports @AGENTS.md or the "Project instructions" setting includes both. The skill now says so, with sources, and agents-md/README.md lists Claude Code among the tools that read AGENTS.md, with that caveat, and Gemini CLI with its own (it reads GEMINI.md unless context.fileName names AGENTS.md).
    • New config-doctor.sh --loads <repo> [--cwd <dir>] [--tools …] lists the instruction files Claude Code, Codex and Gemini CLI each load under their documented defaults. It names any CLAUDE.md that hides AGENTS.md from Claude Code, exits 0 when every tool reads the same rules, 1 on a divergence and 2 when it cannot check, reads files only, and lists the settings it cannot see.
    • New write-agents-md.sh --claude-import (and ./install.sh --agents-md <repo> --claude-import) puts @AGENTS.md first in an existing CLAUDE.md, once, changing nothing else and never creating one. The writer now exits 2 on a target it skipped, and config-doctor.sh says cannot check outside a configuration repository instead of running there.
    • Refreshing a stanza already in AGENTS.md without python3, write-agents-md.sh printed a bash error, then update, and exited 0 with the old stanza still in place, and ./install.sh --agents-md passed that on as success. It now checks for python3 before writing anything, as --claude-import does, and refuses in one line with exit 3.
    • ci/agents-skills.mjs: the new modes against 18 fixture repositories and 7 import cases, bad command lines, a stanza refresh with no python3 on the PATH, and five copies of the scripts with a guard removed that the checks must catch.
    • The skills-first global rule names the Spotter Round router by its new name, spotter-start. Its recorded hash changed with its text, so re-running ./install.sh --global updates it in place.

    multi-agent-workflow

    • The peer detector merged sessions on the last dash-separated part of their ids, across tools, so a Gemini CLI session whose id ended like a Codex session's was never reported. Planted together, the shipped Codex and Gemini fixtures (both …-0001) reported only the Codex one, and a peer in your own worktree read as one in a sibling: exit 11, not 10. Sessions are keyed by tool as well as id now, and a report changes only where two tools' sessions share an id. No control had planted the two together: ci/agents-skills.mjs now plants the Gemini fixture alone, both together, and a Claude session beside its own heartbeat, with two copies of the detector that must fail them, and check 4 in examples/negative-control-peers/README.md plants both by hand.

    Before the split, as part of Spotter Round

    • 1.0.0 (2026-09-13). multi-agent-workflow: one worktree per agent; a tool-neutral peer detector that says cannot check instead of staying silent when it cannot look; a session baseline; and a git pre-commit floor that refuses a commit in a shared main checkout whatever tool sent it, including in an agent's auto-approve mode. universal-agent-setup: a managed AGENTS.md stanza for the tools that read one; recommended global rules as opt-in blocks, which never change your own text or overwrite a rule you edited; and a doctor that keeps agent configuration in a git repository and reports drift.

    Email Pack 1.1.0

    Minor

    What the Gmail app's dark theme did to real email on 2026-09-25, read pixel by pixel from screenshots, and one generator for every sender.

    Upgrading from 1.0.0: run the label drawer once. Button labels are images now, so the Supabase generator refuses to build until yours are drawn. Copy html-email/scripts/label-images.mjs beside the generator (with the new band-lock-guard.mjs), build, run the command the refusal prints with your label face added, set LABELS.map and LABELS.dir, and build again. The drawer needs sharp as well as Playwright: npm i -D playwright sharp && npx playwright install chromium. An email of your own with a live label in a locked cell is now refused by band-lock-guard.mjs, and the same command draws its labels. The pack README has the steps.

    9 changes in detail
    • html-email §2a, rewritten from the measurements. An unlocked ground keeps its hue and chroma and has its lightness flipped (L*out ≈ 100.5 − 0.879 × L*in, fitted over three pairs); a ground carried by a background tile holds; text is recolored against the ground Gmail imagines (the flip of the declared color) until it reaches about 4.5:1, so no live text keeps its contrast on a locked cell. Withdrawn: "a brass label on locked ink survived untouched" (2026-09-15, judged by eye). A #C8A84B label on a locked #0E1219 button arrived #826B15, 3.6:1 against the real ink.
    • Button labels are images: 3×, flat on the button cell's own color, named for their content (Gmail's image proxy caches by URL), alt text styled like the label. The worked palette's card is now the cool #F4F6FA (the warm #FAFAF7 arrived as an olive #242422); code blocks and chips follow the theme instead of staying dark (an unlocked ink block arrived light); a callout's accent edge is its own narrow locked cell; content tables become lists (they squeezed on a phone).
    • band-lock-guard.mjs checks every locked cell, not only the band: live text, an image with no alt or an unstyled one, a colored border. It names the innermost cell holding the words and prints how many locked cells it examined. Selftest 37 cases (was 29).
    • polarity-sweep.mjs --gmail-sim applies the measured model. The first version flipped HSL lightness and passed that label at 6.46:1; over 31 built emails with live labels it read 6,066 values and reported none under. Now it fails them, at 3.72:1. New: --predict answers the model for a palette; --static also refuses an unlocked dark ground holding text inside a light card; the selftest counts its browser cases and says how many it could not run (21 cases with a browser, 16 without).
    • label-images.mjs, new: draws every button label in one command. Your face (a css2 URL, a font-file URL or a local font file) and colors, at 3× (it refuses under 2×), flat on the button's color, each file named for its sha256, plus a JSON map in the shape the generator reads. It refuses to write anything if Chrome reports a single glyph drawn by a fallback font, and the same inputs write the same bytes. --check proves a map against its folder. Selftest 31 cases (24 with no browser).
    • supabase-email-templates: the reference generator's buttons carry label images (LABELS: a map from the drawer, or images), proven like the lockup when you give it the folder (flat, drawn on the band color, a clean 2× or more, named for its sha256). It refuses a button with no image, an image no template uses, and the shipped placeholder names once BRAND.site is yours, and each refusal prints the drawer's command with your colors, folders and labels filled in. The new band lock refused all twelve of its buttons before this change; the sweep's new Gmail pass failed all twelve at 3.73:1, and passes the new templates (1,074 readings, 0 under).
    • Corrected numbers. Brass on the card is 2.12:1 on #F4F6FA and 2.19:1 on #FAFAF7 (§4 said 2.07:1); the darkened brass that loses its class hook reads 3.39:1 on the dark card (it said 2.5:1).
    • Three guides in html-email/references/, graded documented, measured or observed, with the code they sketch marked not exercised: Postmark (templates and layouts over the API, Mustachio, validate, streams, what tracking and the inliner change, DKIM and Return-Path, a push read back by hash), WordPress core mail (wp_mail as HTML, the filter for every core notification, a plain-text part, the templates as a generated plugin, SMTP), and a hosted store's customer emails (its frame, its inliner and its dark rules, measured from one delivered message).
    • The pack's own checks: ci/email-skills.mjs runs the sweep's and the drawer's browser cases, the sweep over the built Supabase templates, and the upgrade end to end (draw, map, build on a site of its own) when PERF_PROJECT names a project with Playwright and sharp (a SKIP otherwise, which a release refuses), and breaks a copy of each new check to prove its selftest notices. 29 checks, was 11.

    Email Pack 1.0.0

    First release

    The email skills from Spotter Round 1.0.0–1.3.0, as their own pack. Nothing about how they work changed in the move.

    6 changes in detail
    • html-email's description and its transport table no longer assume a CRM skill is installed: they name the Supabase templates as the transport in this pack, and point to the CRM skill "if installed". The CRM skill (surecontact-integration) is in the WordPress Pack, with its own byte-identical copy of the hosted-image guard.
    • band-lock-guard.mjs spells "color" the US way in its two error messages and five selftest labels, as everywhere else a user reads.

    Before the split, as part of Spotter Round

    • 1.2.1 (2026-09-16). A lock protects a background, never a border. A button's 1px edge arrived red in the Gmail iOS app over a locked fill that held; the edge is now the padding of an outer locked cell, and band-lock-guard.mjs refuses a colored border on any locked cell. The Supabase reference generator shipped the defect in twelve of its thirteen templates, every one with a button (the reauthentication email has none); the new guard caught it. Corrected 2026-09-25: this entry said all thirteen.
    • 1.2.0 (2026-09-15). Deploy the images, then probe-send, then go live. hosted-asset-guard.mjs makes one real request per image URL and refuses a relative, data:, cid: or plain-http: reference, a non-200, an empty image, and a 200 that is not an image.
    • 1.1.0 (2026-09-14). Email that survives the Gmail app, which inverts colors element by element and never touches an image: the header band locked with a hosted tile, no live text in a locked cell, the footer on the card. band-lock-guard.mjs and polarity-sweep.mjs (both color schemes, a Gmail-style pass, and a browserless --static check).
    • 1.0.0 (2026-09-13). html-email: dark mode that survives Apple Mail, a band and lockup that read as one surface either way, contrast checked on the built output. supabase-email-templates: every Supabase Auth email from one generator, pushed with a partial update and read back field by field.

    WordPress Pack 2.0.0

    Major

    This pack is WordPress itself now. SureContact and OttoKit are not limited to WordPress: both work with any stack, so their skills moved to the App Stack Pack (1.1.0), and the WordPress Pack keeps wordpress-bricks-stack. A pack that loses skills takes a new major version.

    3 changes in detail
    • surecontact-integration and ottokit-integration left this pack, unchanged, with their scripts, their checks and SureContact's copy of hosted-asset-guard.mjs. They are in the App Stack Pack. ottokit-integration as it shipped here says a server id proves the workspace. It does not: every OttoKit server on the account shows the workspace selected in the web app. The App Stack Pack 1.1.0 corrects the skill and its call helper.
    • wordpress-bricks-stack does what it did. Its description says Bricks 2.4 and later, and no longer names the CRM skill: a description cannot say "if installed", and that skill is not in this pack. Its pointers to the CRM and automation skills say "if installed" and name the App Stack Pack.
    • ci/verify.sh runs one harness here, ci/wordpress-skills.mjs (42 checks), and the pack ships no copies.

    WordPress Pack 1.0.0

    First release

    The WordPress stack as its own pack: a new skill for Bricks Builder, Automatic.css and SureCart, and the CRM and automation skills from Spotter Round.

    9 changes in detail

    wordpress-bricks-stack (new)

    • Rules and scripts for a WordPress site built with Bricks Builder 2.4, Automatic.css 4, an AI section builder and SureCart, changed by an agent through an MCP server: who can write what, staging templates behind conditions (a published header or footer with no conditions goes live on every page), a backup before every write that keeps no revisions, Automatic.css 4 palettes that need their derived keys, and SureCart's light panels on a dark site.
    • scripts/page-review.mjs reviews a rendered page, including a draft seen only through a logged-in preview: contrast, overflow, clipped and off-screen content, stranded words, the heading outline and console errors (--selftest, 36 cases). scripts/font-check.mjs reports the font the browser actually draws for an element (--selftest, 27 cases). Neither selftest opens a socket.
    • Three PHP snippets (a draft preview, the Automatic.css palette derivation, SureCart's instant checkout in a dark palette), each refusing to run outside WordPress or with its settings unset. The preview destroys its session token even when the fetch fails, and returns no token, cookie or nonce.
    • Dark-checkout stylesheets whose header claims are checks: every selector scoped to html or body, exactly one hard-coded color, no !important.
    • ci/wordpress-skills.mjs runs 42 checks, including a copy of each script with a guard removed that its checks must catch. Where php or playwright is missing it says SKIP, never ok.

    surecontact-integration, from Spotter Round 1.0.0–1.3.0

    • It now ships its own copy of scripts/hosted-asset-guard.mjs, byte-identical to the one in the Email Pack and kept that way by the release build, so the probe-send step works without that pack.
    • Its pointers to the email craft skill say "if installed".
    • Before the split: forms through the submit API, per-workspace keys, merge tags, drips sent byte-exact (1.0.0); probe before you write (1.1.0); deploy the images before the probe (1.2.0); form views, incoming webhooks, workflows chained by tags, click triggers, versioning a live workflow and gotchas 17–26 (1.3.0).

    ottokit-integration, from Spotter Round 1.3.0

    • One server per workspace, proven before every call; definitions kept as JSON in the repository and round-tripped through export and import; each step's traps; gotchas 1–18. ottokit-definition.mjs validates, packs, decodes and diffs with no network, and ottokit-call.mjs keeps only your workspace's run history.

    App Stack Pack 1.1.0

    Minor

    The CRM and the automation tool join the stack. SureContact and OttoKit are not limited to WordPress: both work with any stack, so their skills moved here from the WordPress Pack (2.0.0): surecontact-integration unchanged, and ottokit-integration with a correction to how a workspace is proven (below). A pack that gains skills takes a new minor version; nothing in the three skills already here changed.

    surecontact-integration, from the WordPress Pack 1.0.0

    8 changes in detail
    • Forms through the submit API, per-workspace keys, merge tags, form views for a form you render yourself, drips generated from your built email and sent byte-exact, incoming webhooks, workflows chained by tags, click triggers, versioning a live workflow, and gotchas 1–26.
    • drip-to-workflow.mjs builds the workflow graph from your built email, and --check refuses a payload whose sends differ from that build; form-view-ignore.mjs keeps your own visits out of a form's view count; surecontact-call.mjs makes one tool call with its arguments read from a file.
    • It ships its own copy of scripts/hosted-asset-guard.mjs, byte-identical to the one in the Email Pack and kept that way by the release build, so the probe-send step works without that pack. Its pointers to the email craft skill say "if installed".
    • ci/surecontact-skills.mjs runs the three selftests (9, 14 and 18 cases) and a copy of each script with one refusal removed, which its selftest must catch: 6 checks.

    ottokit-integration, from the WordPress Pack 1.0.0, corrected

    • A correction: a server never proves the workspace. As it shipped in the WordPress Pack 1.0.0 and Spotter Round 1.3.0, the skill said each MCP endpoint is bound for good to the workspace it was created in, and that a matching server_id proved the workspace. Observed on 2026-09-23 and 2026-09-24, it is the other way round: every OttoKit MCP server on an account shows whichever workspace is selected in the OttoKit web app, a switch in any signed-in browser moves every connector in every session at once, and server_id and server_name stay the same. §1, the direct calls in §2, §7 and gotchas 1, 2 and 5 now say so, with the check that works: titles you know belong to the workspace, read from list_automations before the first write and again after any pause, and the account's owner, not an agent, switching the web app back.
    • ottokit-call.mjs keeps its server-id check for what it proves (the server and the account) and runs a workspace probe before any tool call: list_automations must hold an automation whose title contains OTTOKIT_EXPECT_TITLE, a variable you now set, or it refuses and calls nothing. A server without list_automations is refused unless you pass --workspace-confirmed, and the output then says nothing proved the workspace. Its --selftest runs 36 cases (22 before) against a fake server that records every tool called, so a refusal is shown to call nothing.
    • Unchanged: definitions kept as JSON in the repository and round-tripped through export and import; each step's traps; the other gotchas. ottokit-definition.mjs validates, packs, decodes and diffs with no network, and ottokit-call.mjs keeps only the confirmed workspace's run history.
    • ci/integration-skills.mjs runs both selftests (25 and 36 cases), round-trips the shipped example, refuses a tool call with no expected title, and breaks copies of the scripts four ways, one of them a call helper that skips the probe: 16 checks.

    The README lists what each new script touches on your machine. Every script in the pack still needs Node 18 or newer and nothing else.

    App Stack Pack 1.0.0

    First release

    The web-app stack as its own pack: Cloudflare Pages hosting, Stripe billing on Supabase, and passkey sign-in on Supabase Auth. Spotter Round's checks work on any host and any framework; the skills here are for one stack, so they ship on their own.

    9 changes in detail

    cloudflare-pages (new; prepared for Spotter Round 2.0.0 and moved here before any release)

    Your pages.dev address is kept out of search, and your real domain is not. Every Cloudflare Pages project serves the whole site at <subdomain>.pages.dev as well as at its custom domain, and Cloudflare marks only preview deployments noindex. Measured 2026-09-24: a per-deployment URL and a branch alias answered X-Robots-Tag: noindex, and the production alias answered with no header until a host rule was added. The same day a search console reported five pages of one site as "Duplicate, Google chose different canonical than user" while every page carried a correct self canonical. Two host rules in _headers fix it, and a checker now proves them both ways.

    • §1 deploying: git-connected or direct upload (a one-way choice per project name), push by SHA, wrangler pages deploy --commit-hash so the deployment list names the commit, a real 404.html from day one, and "what is live" as a hash comparison.
    • §2 _headers and _redirects: host rules with a :placeholder subdomain (measured), the documented limits (100 header rules; 2,000 static and 100 dynamic redirects, every rule after the first wildcard counted as dynamic), exact paths first, extension-less rewrite targets, and wrangler pages dev as the only local server that applies them.
    • §3 every pages.dev host is noindex: what Cloudflare sends on each host, graded; the two host rules; the subdomain taken from the project listing, never the name; no host-less noindex; the checker; and a throwaway-branch test with its cleanup.
    • §4 cache skew and the module MIME error, and the zone settings that cause the same error; §5 www to apex as a zone Redirect Rule (Pages ignores a domain-level _redirects line), http to https, and the 308 to a trailing slash; §6 edge banning through a scoped token with an IP filter, and its proof of function; §7 presigned R2 uploads that must sign content-type; §8 prerendering a client-rendered app; §9 why preload and Early Hints are judged live; then gotchas 1–21, each graded measured, observed, or documented and not measured.
    • scripts/pages-noindex-check.mjs: --headers reads a _headers file with no request and refuses a missing alias or branch rule, a rule for a different subdomain, a host-less /* noindex, a noindex on a named custom domain and the documented limits. Live mode fetches each host once, cache-busted, and fails an alias that is indexable, a custom domain with a noindex header or meta tag, and any host that did not answer 2xx; a branch alias is checked only when named, and the output says so when it is not. --from-wrangler takes the hosts from wrangler pages project list --json, run through npx, which downloads wrangler from the npm registry without asking when neither the project nor your global npm has it; the README and the skill say so. --selftest runs 32 cases, the live half through a fake fetcher, and opens no socket.

    stripe-on-supabase (new)

    Billing fails quietly. A price id written into a migration works for months in test mode and fails on every plan the day the functions get a live key; a webhook left behind Supabase's default JWT check answers 401 to every delivery; a subscription status the table does not accept fails its upsert, and Stripe retries the same event for three days while the customer's row keeps its old status. None of these fails a build.

    • §1 prices as lookup keys, never price ids, so going live is data and not a migration; §2 test and live mode, sandboxes, and proving which account a key belongs to before writing; §3 secrets only in the Edge Functions (a VITE_* or NEXT_PUBLIC_* variable ships to the browser); §4 the browser names a plan, never a price or an amount; §5 the webhook: signed raw body, constructEventAsync, JWT verification off for that function alone, an idempotent upsert on a unique subscription id, every Stripe status mapped, 200 for problems a retry cannot fix, the period end that moved onto items, and a pinned API version; §6 access granted from the table by an allowlist of statuses; §7 the chosen plan carried through a magic-link sign-in; §8 live-mode settings, legal links and the hosted pages before launch; §9 type-checking the functions beside an app; then 20 gotchas, each graded measured, observed or documented.
    • scripts/billing-check.mjs reads a project and refuses 18 defects: a price id in SQL, code or JSON; a secret in a public variable, in source, or in an env file git tracks; a checkout that takes a price or an amount from the request; a webhook with no signature check, a synchronous one, a parsed body, a path that skips the check, or JWT verification still on; an unpinned SDK or API version; an upsert with no or the wrong conflict target, or one with no unique index behind it; a status the table rejects or the webhook never maps; the period end read from the wrong object; functions that type-check against the app's node_modules. It names a secret by kind and length, never its value; says what it cannot see (the Stripe account, the deployment, webhooks outside supabase/functions); refuses a folder that is not a Supabase project before reading anything; and --selftest runs 63 cases without a request.

    supabase-passkeys (new)

    A passkey unlocked with a fingerprint, face or PIN is two factors by NIST's measure, and Supabase Auth still issues its session at the first assurance level. So the users who turned on two-factor sign-in are the ones sent back to a code screen after using a passkey, and every check that requires the second level locks them out unless it is taught otherwise.

    • §1 the rule, as a decision you make; §2 the order of operations: enable it with a permanent relying-party id, the client's passkey flag, a hand-run ceremony that requires user verification and checks the flag, skipping the code step after that sign-in, every second-level check in SQL, Edge Functions and the app that also accepts a passkey session (and step-up windows that count it), the userHandle check before a step-up verify, insufficient_aal when adding or removing a passkey, the session-length setting to leave off, and the interface copy; §3 the known limit (the server cannot see the verification flag) and the exception you decide per action; §4 API facts the types do not show; §5 testing with a virtual authenticator; then 14 gotchas, each graded.
    • scripts/passkey-check.mjs reads a project and refuses 16 defects, following helpers and their callers, and prints how many places each rule examined: the missing client flag, a ceremony without required verification or the flag check, a verify call given the wrong challenge field, a step-up without the userHandle check, second-level checks in SQL, functions and the app that reject a passkey session, step-up windows that omit it, passkey.update() or delete() without passkeyId, uncaught passkey calls, missing insufficient_aal handling, sign_count used as a signal, and a committed rp_id = "localhost". Live mode reads the project's auth settings and relying-party id with the anon key, and fails when the id is not the domain you expect. A folder that is not a project is refused before anything is read. --selftest runs 64 cases, the live half through a fake fetcher.

    All-Access Bundle 2.0.0

    Major

    The CRM and automation skills join the App Stack Pack, because neither is limited to WordPress. Inside the bundle that moves two skills to another plugin (wordpress-pack:surecontact-integration is now app-stack:surecontact-integration, and the same for ottokit-integration), which is why this is a major version. The Email Pack measures the Gmail app's dark theme, draws button labels as images, and adds guides for Postmark, WordPress mail and a hosted store's customer emails. Spotter Round and the Agent Setup Pack change only their READMEs' family lists and the installer's requirement line.

    Pack versions inside
    PluginVersion
    spotter-round2.0.1
    agent-setup1.0.1
    email-pack1.1.0
    wordpress-pack2.0.0
    app-stack1.1.0

    All-Access Bundle 1.0.0

    First release

    Every pack, each as its own plugin, and one plugin that installs them all.

    Details
    PluginVersion
    spotter-round2.0.0
    agent-setup1.0.0
    email-pack1.0.0
    wordpress-pack1.0.0
    app-stack1.0.0

    Together they hold everything Spotter Round 1.3.0 held, plus post-deploy-proof, wordpress-bricks-stack, cloudflare-pages, stripe-on-supabase, supabase-passkeys and secret-handoff.

  7. 1 release

    Spotter Round 1.3.0

    Minor

    The automation that runs is the one in your repo, in the workspace you proved. Two measured incidents shaped this version. On 2026-09-24 an OttoKit run-history call filtered to one new automation returned the whole account's runs, another client's workspace included, and the run-detail call served them; the same call 30 minutes later was scoped, so it is intermittent, and a report read as "this workspace's activity" would have been wrong. The same day a regex quantifier such as {8} in a Formatter pattern was read as a data reference: the pattern matched nothing, the step reported success with an empty result, and the first live post went out with no contact on it. Both are now refused by a script before they can happen.

    17 changes in detail

    ottokit-integration (new)

    • One server per workspace, proven before every call: get_mcp_server_stats must name the server id you expect, and list_connections is account-wide, so it proves nothing about the workspace.
    • The endpoint URL is the credential: it goes in an environment variable from your secret store and a local, uncommitted MCP entry, and never into a committed file or a printed line.
    • Definitions live as JSON in the repo and reach the app through Export and Import Workflow: the CSV and base64 format, the rule fields, the reference syntax, the branch shape, and the loop validate, pack, import, export, diff. Every import is a new automation with new ids and strips every connection, so §4 is the three steps after each one: test the trigger, re-pick each connection, enable.
    • §5 is each step with its traps and a step-shape table (Email Parser, Formatter, Branch, Catch Webhook and Custom Request, Google Sheets, Calendly): braces read as references, equals_to is case-sensitive, Extract Pattern as a whitelist, and the handoff rules (no CRM key in the automation tool; a Catch Webhook URL is an unauthenticated trigger address, so it is a secret).
    • §6 replies by email through a plus-address alias on your own domain, §7 the editor (switch off before deleting; do not let the assistant edit), §8 the measured round trips, then gotchas 1–18, each graded measured, observed once, or from the vendor's description and not measured.
    • scripts/ottokit-definition.mjs, no network: validate (one trigger, unique ids, parents that exist, both paths on every branch, every path ending in an exit, no brace in a pattern, references only to steps that exist, a connection on every app step), pack, decode (joins an export split across rows) and diff against the live export, which reports a stripped connection, a changed field and a missing rule and never normalizes silently. --selftest runs 25 cases on the shipped example.
    • scripts/ottokit-call.mjs calls the one tool you name: arguments are checked before the environment is read, the server id is confirmed before any call, the URL is redacted from every output line and saved file, and run history keeps only this workspace's automations and prints how many it dropped. --selftest runs 22 cases and opens no socket.
    • examples/reply-parser.json: an Email Parser trigger, two Formatter steps, a branch with both paths, a Custom Request on each path, one app step with a connection placeholder, and exits. It is the selftest fixture, so the example is proven to validate, pack and round-trip.

    surecontact-integration

    • §2 "Form views" and gotcha 17, landed from the form-views branch: a form you render yourself reads 0 views, so send track-view yourself when it appears, and keep your own network out with scripts/form-view-ignore.mjs (IPv4 and IPv6 ranges, 14 selftest cases, now run in CI).
    • §2 "Incoming webhooks": the URL, keyed by contact UUID or email with create_if_not_exists: false (an unknown contact answers 404 and creates nothing), JSON and form bodies accepted, text/plain refused, no CORS headers, so a browser sends a form-encoded simple request. POST to inbox measured at 3–4.5 s.
    • §3 "Chaining workflows by tags": add_tag starts another workflow's tag_added in about 1 s, re-adding a held tag does not fire it, the guard after every wait, branching on tags only, and downstream workflows activated before the entry.
    • §3 "Click triggers: measure before you trust them": a click trigger fires on the first click in an email only, and a scanner's GET counts as that click, so choices go through a page you control.
    • §3 "Versioning a live workflow with create_new": 7 workflows re-versioned on 2026-09-23 and 18 on 2026-09-24, every node diffed against the previous payload before upload.
    • §3 "Handing off to an automation tool": the core:send_webhook config, and one line pointing at ottokit-integration for the other side.
    • Gotchas 18–26: the click trigger, an end that does not cancel another workflow's wait, two reply-to addresses that deliver nothing, a merge tag the webhook did not send rendering literally, the 409 on a repeat opt-in, deletes only in the app, pinned trigger schemas, chained-tag timings, and tag checks that are not an atomic lock.

    start-here

    • A route row and a registry row for ottokit-integration, and the registry now lists form-view-ignore. The sentence on which scripts contact a vendor names the automation helper beside the two email transports; the README and the sales page say the same.

    Gate

    • The vendor rule is now one rule per vendor, each allowed only inside its own skill: surecontact, supabase, and ottokit|suretriggers (which leaves the always-banned list). This closes a hole where each email skill's directory was exempt from the other vendor's word, so one vendor skill could name another unnoticed. Each rule builds its own filter, because a shared filter ending in one vendor's pattern would have dropped every hit for the others. Seven planted controls prove it; the 1.2.1 gate gets four of them wrong.
  8. 1 release

    Spotter Round 1.2.1

    Patch

    A lock protects a background and nothing else. 1.2.0 prescribed a button as a locked ink cell with a 1px accent border; measured in the Gmail iOS app the next morning, that border arrived red while the locked fill behind it held perfectly. Gmail rewrites border-color exactly as it rewrites background-color, and a tile cannot defend a property it does not paint.

    3 changes in detail

    html-email

    • §2a rule 4: the button's edge is the 1px padding of an outer locked cell, not a CSS border, with the markup to copy. It reuses the accent tile the rule cell already has, so it costs no new asset, and Apple Mail renders it identically. The warning covers border, border-color, outline and box-shadow alike — a lock pins none of them.
    • scripts/band-lock-guard.mjs refuses a colored border on any locked cell. It identifies a locked cell by its tile (a background attribute plus an inline background-image) rather than by class, so it sees the button and the accent rule as well as the band. A border in the cell's own locked color, border:none and border-radius all still pass, and a border on an unlocked cell is not its business. Ten new selftest cases, five of them false-positive controls.

    supabase-email-templates

    • The reference generator shipped exactly this defect in twelve of its thirteen templates, every one with a button (the reauthentication email has none). The new guard caught it, and the build now refuses rather than writes. Its button is the locked-frame pattern, with a TILES.frame entry for the accent tile it needs. Corrected 2026-09-25: this entry said all thirteen.
  9. 1 release

    Spotter Round 1.2.0

    Minor

    Email images have to be on the host before you send. Every guard in the kit read files on disk, so a build could be perfect and the message still arrive with broken-image boxes — which reads as a broken email and is not one. This version makes the request.

    6 changes in detail

    html-email

    • §1 states the order of operations the skill had only implied: deploy the assets, then probe-send, then go live. An email has no build step, no bundler and no base URL; every image is fetched from your host by the reader, months later. Measured 2026-09-15: an email cleared the lockup guard, the band lock and the contrast sweep and landed in two inboxes with no logo, because the PNGs were on an undeployed branch.
    • scripts/hosted-asset-guard.mjs: one real request per image URL in the built HTML. Refuses a relative, protocol-relative, data:, cid: or plain-http: reference, a non-200, a zero-byte image, and a 200 that is not an image — the one a status check misses, because a static host with a catch-all answers a missing file with 200 and the HTML of your 404 page. --static is the shape-only half for CI; --selftest serves every refusal from a real loopback socket.
    • It reads URLs, not classes, which is how it sees what band-lock-guard.mjs cannot: that guard inspects cells classed band only, so a tile on any other locked cell is outside its scope. On the same build the accent rule's tile was emitted as band-f7931a.png while the asset step wrote rule-f7931a.png — a 404 live on the site, in six emails, that a clean --tiles run passed over.
    • §2a: an un-deployed tile leaves the cell with only the color Gmail repaints, and fails silently, because a missing background image draws nothing.

    supabase-email-templates

    • The reference generator asserts the shape half and refuses to run without the guard, like the other two. §3's setup list said three files while the generator already imported four; it now names five.

    surecontact-integration

    • §3: deploy the images before the probe. The CRM does not host them for you.
  10. 1 release

    Spotter Round 1.1.0

    Minor

    Email that survives the Gmail app. Gmail's iOS app inverts colors element by element, ignores every scheme declaration, and never touches an image; a header band painted only as a color arrived pale under a lockup PNG that stayed dark, in three products on one day. The fix and its guards.

    6 changes in detail

    html-email

    • New §2a: lock the header band with a hosted solid tile (background= and background-image beside bgcolor), keep live text out of the locked cell, never name the band in the dark block, and put the footer on the card so it inverts with the body. A real send to a Gmail address is the gate; Chromium emulation cannot show this.
    • §2a rule 4, measured in a real Gmail iOS inbox on 2026-09-15 across three brands: the button is a locked ink cell with a mid-tone accent label and a 1px accent border. A brand-fill button is repainted (brass to olive, orange to brown); a locked brass fill keeps its ground but Gmail flips the ink label light onto it; a locked ink cell with a brass label is left alone, because Gmail recolors text against the color it believes it painted. The reference generator draws it.
    • scripts/band-lock-guard.mjs: refuses a band cell missing any of the three color carriers, a tile not named for its hex, live text inside a locked cell, an unstyled or missing alt, and a dark block that names .band; with --tiles it opens each tile. --selftest plants every refusal.
    • scripts/polarity-sweep.mjs: the computed-color contrast sweep from the 2026-09-13 fix, shipped: both schemes at 375 and 600px, a --gmail-sim pass that imitates the app's inversion, and a browserless --static hook check for CI. --selftest fails on a planted defect first.

    supabase-email-templates

    • The reference generator locks its band the same way, moves its eyebrow from the band into the card, and asserts the band lock on every rendered template. CI runs both new guards on its output.

    surecontact-integration

    • §3 gains the probe: send one built email to yourself and look at it in the Gmail app before any workflow write, which is also how you learn whether the transport kept your <head>.
  11. 1 release

    Spotter Round 1.0.0

    First release

    Version 1. Skills and runnable checks that make a coding agent prove its work, and let several agents share one repository without corrupting each other's history. Every runnable gate ships a negative control, because a check that has never failed is not evidence.

    14 changes in detail

    Verification

    • start-here — routes you to the check that fits what you just changed, and states the standard of evidence the rest of the kit holds work to.
    • verification-protocol — the twelve-item checklist run before any frontend change is called done. It is also free on its own, generated from this copy; the release build asserts the two files are byte-identical.
    • layout-proof — stranded words, clipped text and overflow, and the documented ways a layout audit reports a false clean.
    • visual-fidelity-gate — before/after pixel diff, three browser engines, zero layout shift.
    • build-ab-harness — compare two builds under identical conditions, and know when the comparison means nothing.

    Several agents, one repository

    • multi-agent-workflow — one worktree per agent; a tool-neutral peer detector that says cannot check instead of staying silent when it cannot look; a session baseline; and a git pre-commit floor that refuses a commit in a shared main checkout whatever tool sent it, including in an agent's auto-approve mode.
    • universal-agent-setup — a managed AGENTS.md stanza every tool reads; recommended global rules as opt-in blocks, pasted or installed with ./install.sh --global <file>, which never change your own text or overwrite a rule you edited; and a doctor that keeps agent configuration in a git repository and reports drift.

    Email

    • html-email — dark mode that survives macOS Mail, a header band and logo lockup that read as one surface in either polarity, and contrast checked on the built output.
    • supabase-email-templates — every Supabase Auth email from one generator, pushed with a partial Management API PATCH that changes nothing else, and read back field by field.
    • surecontact-integration — forms, contacts and drips, with the CRM's silent failures measured, and approved HTML sent byte for byte.

    Also

    • docs/how-it-works.md, for the person rather than the agent: the guard layers, what each can see, and every way past the floor.
    • Negative controls in examples/, run by ci/verify.sh.
    • Two scripts talk to a vendor, only when you run them and only with a key you supply: push-email-templates.mjs (Supabase) and surecontact-call.mjs (SureContact). There is no telemetry, license check or phone-home.
    • Reproducible zips: building the same commit twice gives the same sha256, on any machine in any timezone.