Release notes
Each release of cora and cora-mcp, newest first, up to the release these docs describe. The two packages release together at one version, so a release lists each change to either of them once.
0.10.0
Section titled “0.10.0”Minor Changes
Section titled “Minor Changes”-
Findings in Angular apps now name the template’s file and line to fix. Angular 22.1.4 and later writes each element’s place in its template, in a
.htmlor.svgfile or an inlinetemplate, asdata-ng-source-locationwhen its compiler optionenableTemplateSourceLocationsis on, andcora scanand cora-mcp’s scans now read it beside CORA’s own stamp. A location holds while its file still has the element’s opening tag where Angular found it.cora initturns the option on for each application of an Angular CLI workspace in its development builds: it writes atsconfig.cora.jsonbeside the application’s tsconfig, extending it and settingrootDirto the repository root, under a comment that marks it as CORA’s so a later run can rewrite it, and points thebuildtarget’sdevelopmentconfiguration at it inangular.json, song servewrites the locations and a production build does not. It sets nothing up for an Angular older than 22.1.4 or a project with noangular.json, and leaves the workspace as it is where an application names notsConfigor has nodevelopmentconfiguration, its tsconfig, or one it extends, setsrootDirs, its directory holds anothertsconfig.cora.json, or two applications with different tsconfigs share a directory, each with the docs page that shows the change by hand. See Source locations.Angular calls the option internal: angular.dev does not list it, and a minor release of Angular may change it.
cora initsets nothing up where Angular is older than 22.1.4, not installed yet or installed through Yarn’s Plug’n’Play, and a scan reads a location only from a.html,.svgor.tsfile, not from another kind of file such as.env. -
CORA’s build plugins now state the oldest release of each package they use from your project, on the Source locations page: Vite 5.0.0, webpack 5.61.0, Rspack 1.0.0, Rsbuild 1.0.1, Rollup 2.78.0, Rolldown 1.0.0 and esbuild 0.19.7;
@vitejs/plugin-vueandunplugin-vue4.5.0,vue-loader16.8.2 andrspack-vue-loader17.4.4; Vue 3.4.3, Svelte 5.0.0 and@astrojs/compiler-rs0.1.0. Vue’s is raised from 3.4.1, since Vue 3.4.1 and 3.4.2 reuse a template’s parse after the Vue plugin’s options change: a project on either gets no stamps in its.vuefiles, with one warning, until it updates Vue. A project whose@astrojs/compiler-rsis older than 0.1.0 now gets the same warning for its.astrofiles, and an Astro 7.0 to 7.2 project, whose compiler counts its offsets in bytes before 0.4.0, now gets a stamp on each element after non-ASCII text, such as an accented letter or an emoji. A Vite 5 to 7 build now stamps a file that starts with a byte order mark, and a file with a Svelte syntax error on Svelte older than 5.1.5 is left to the Svelte plugin to report, with no warning of the plugin’s own. -
New subpaths
@vispero/cora/webpack,@vispero/cora/rspack,@vispero/cora/rsbuild,@vispero/cora/rollup,@vispero/cora/rolldownand@vispero/cora/esbuild: build plugins that stamp each host element of the project’s source with its place in the source, as@vispero/cora/vitedoes, so findings in apps built with these bundlers name the file and line to fix.- The webpack, Rspack and Rsbuild plugins run CORA’s own loader before every loader the config’s rules list,
prerules that other plugins and Rsbuild’stools.rspackadd included, wherever CORA’s plugin is listed. They stamp JSX in.jsx,.tsxand.jsfiles, the templates of.vuefiles, parsed with the compiler and thecompilerOptionsof the Vue loader that builds them, vue-loader or Rsbuild’srspack-vue-loader, so a custom element itsisCustomElementnames is stamped, and the elements of.sveltefiles, parsed with the project’s Svelte 5 or later. The Rsbuild plugin stamps each environment Rsbuild builds, the server-rendered one included. - The Rollup and Rolldown plugins stamp JSX in
.jsxand.tsxfiles, and in.jsfiles, which Rollup’sjsxoption, from 4.24.0, or a JSX plugin compiles, and Rolldown where itsmoduleTypesoption reads.jsasjsx, and.vueand.sveltefiles, a.sveltefile parsed with the project’s own Svelte. The esbuild plugin stamps.jsxand.tsxfiles, and JSX in.jsfiles where the build’sloaderoption reads it there. Under Rollup and Rolldown, a.vuefile is parsed with the compiler and template options ofunplugin-vuewhere the config lists it, so a custom element itsisCustomElementnames is stamped, and otherwise with the project’s own Vue. Their transform runs first wherever they are listed, so a plugin that compiles JSX, Vue or Svelte may come before them inplugins. @vispero/cora/vitenow stamps JSX in.jsfiles too, which Vite compiles where its config says to, such as throughoxcunder the dev server andbuild.rolldownOptions.moduleTypesin a build. A.jsfile with no JSX is left as it is.@vispero/cora/vitereads the compiler and template options ofunplugin-vue’s Vite plugin as it reads@vitejs/plugin-vue’s, so a Vite project onunplugin-vuegets its.vuefiles stamped.- A
.jsor.jsxfile written with Flow’s types is read with them, as a Flow preset compiles it, so every plugin that stamps such a file,@vispero/cora/viteamong them, stamps its JSX. - A module imported as its text or its address is left unstamped: under webpack, Rspack and Rsbuild, one whose query is
?raw,?urlor?inline, or whose module type is not JavaScript, such as a text import attribute’s; under esbuild, a file that the build’sloaderoption, by the longest extension it names, or the import’stypeattribute loads as anything but JavaScript, JSX, TypeScript or TSX; under Rolldown, a module whosemoduleTypestype is not a script’s. - Left unset,
sourceStampsstamps in webpack’s, Rspack’s and Rsbuild’sdevelopmentmode and in Rollup’s and Rolldown’s watch mode, and not in a build. esbuild tells a plugin nothing of development, so there it stamps only when the setting istrue.
See Source locations.
- The webpack, Rspack and Rsbuild plugins run CORA’s own loader before every loader the config’s rules list,
-
cora initnow registers CORA’s build plugin in a webpack, Rspack, Rsbuild, Rollup or Rolldown config’splugins, as it does in a Vite config, in the config file the bundler loads, the first of its names it finds, appending the plugin topluginsin every bundler’s config. esbuild, whose builds are scripts, gets no step. It edits a CommonJS config too, one that assignsmodule.exports, in Vite’s and every other bundler’s, requiring the plugin’s.default. In every config it edits, Vite’s, Nuxt’s and Astro’s included, it follows a config the file names, asconst config = { … }thenexport default config, and a config function with a body of statements to the config it returns, as webpack’s own scaffold writes; for webpack, Rspack, Rollup and Rolldown, it registers the plugin in each config of a list of configs. In every config, Vite’s included, it takesdefineConfig, and Nuxt’sdefineNuxtConfig, as the config’s wrapper only where the file imports or requires it from its tool’s own package, such asviteorvitest/config, under any name, or calls Nuxt’s, which Nuxt makes a global, by a name the file binds nowhere; it leaves a config wrapped in any other call, such as a project’s owndefineConfig. A config it cannot be sure of is left as it is, with the docs page that shows the edit by hand, such as a list with a config it cannot edit, a function that returns its config from a branch, a config with a getter or a setter or that sets its prototype with__proto__, a file that exports a config more than once, a file that callseval, holds awithstatement or usesmoduleother than throughmodule.exports, a CommonJS config that bindsrequireitself, or a config whoseplugins, or an item of them, a later line could change. A project that lists a bundler with none of its configs in the working directory, such as one a build names with--config, is toldDid not set upthe plugin, since the working directory holds none of the configs it edits, such aswebpack.config.js, with the docs page that shows the edit by hand. It says nothing of a bundler that a set-up tool explains being listed: of Rollup and Rolldown where a Vite, Nuxt or Astro config is in the working directory, and of Rspack where an Rsbuild config is. The notice for a Nuxt or Astro project without its framework’s config now gives the same reason, that the working directory holds none of the framework’s configs it edits. A config that exports its config asexports.defaultis left as it is. A CommonJS TypeScript config that imports types alone stays CommonJS, the plugin’srequiregoes before the config reads it, and a line added above a JSDoc comment directly on the config’s first statement keeps the comment on the statement; a comment a blank line apart, such as a license, stays first. It finds Rspack’s and Rsbuild’s configs.tsfirst, as those bundlers do, and names Rollup and Rolldown among a project’s build tools. A plugin registered behind a condition, such asdev && sourceStamps()or a branch of? :, counts as registered, in a Vite config too, socora initadds no second registration. See Source locations. -
Findings in Lit components now name the template’s file and line to fix. Every build plugin,
@vispero/cora/vite,/webpack,/rspack,/rsbuild,/rollup,/rolldownand/esbuild, stamps the host elements of each template tagged with thehtml,svgormathmla file imports fromlit,lit/html.js,lit-html,lit-elementorlit-element/lit-element.js, or fromlit/static-html.jsorlit-html/static.js, in the project’s.js,.mjs,.ts,.mts,.jsxand.tsxfiles, so an element in a component’s shadow root and one in the light DOM are each located at their tag in the template. The stamp is a plain attribute in the template’s static text: no expression changes, and an element whose tag name is an expression, acsstemplate, and a template tagged with any other function are left as they are. A Vite project registers the plugin as any Vite project does, andcora initregisters it there. See Source locations.A file holding JSX and a Lit template, such as a
.tsxfile, now gets the stamps of both, and a.tsxor Lit file holding a decorated class, in the standard decorators or TypeScript’sexperimentalDecorators, now gets its stamps; before, a decorated class left its.tsxfile unstamped, with no warning.cora hooknow asks before an agent adds a comment that turns off a rule of eslint-plugin-lit-a11y, as it does for jsx-a11y’s.cora initnow names Lit among a project’s tools when its package.json listslit-elementorlit-htmlwithoutlit. -
cora init’s starter config now scans the app’s linked pages and starts the app itself. Its target is a crawl from the home page, of up to 25 pages up to 2 links away, which skips common sign-out links such as/logout, in place of the home page alone. Where the project’sdevscript, or itsstartscript where it has nodev, starts the dev server of Vite, Next.js, Nuxt, React Router, Docusaurus, Angular, Create React App, CRACO, webpack, Rspack, Rsbuild or Parcel, it setsserverto run that script with the project’s package manager, andbaseUrlto the server’s address, socora scanstarts the app before it scans and stops it after, with nothing started by hand. The port is the one the script sets, or else the one the dev server’s own config writes out, or else the tool’s default;cora initreads the script and that config, runs neither, and prints the command and the address it set. Where it can’t be sure where the dev server answers, such as for a script that serves HTTPS, listens on an addresslocalhostmay not reach or takes its port from a shell variable such as$PORT, or a dev server whose own config computes or imports its port, it writes noserver, says why, and leavesbaseUrla placeholder to set. With--base-url, for an appcora initdoesn’t start, it sets that address and noserver. A project that already has a config keeps it as it is. See The starter config.
Patch Changes
Section titled “Patch Changes”- In a project whose JSX Vue’s JSX compiler builds, CORA’s build plugin no longer stamps a component written as a lowercase or hyphenated tag, such as
<router-link>: Vue passed that stamp through to the element the component renders, so a finding there named the line that used the component. The plugin now stamps the tags Vue’s JSX compiles as HTML or SVG elements, and learns a build compiles Vue’s JSX from its Vue JSX plugin, or, in a build that lists none, from your package.json. A custom element in Vue’s JSX is not stamped, esbuild builds withunplugin-vue-jsxget no JSX stamps, and a build whose Vue JSX plugin’s@vue/sharedcannot be loaded warns once and stamps none of its JSX; see Vue with JSX. - On Windows, stopping a process CORA started, such as the server
cora scanstarts or the npx that fetches a cora-mcp release, no longer reports it left running, with anapp server not stoppedornpx not endedwarning, whentaskkillfails on it because it exited astaskkillended what it started. Whentaskkilldoes leave it running, the warning’s reason ends with whattaskkillwrote to stderr.
Minor Changes
Section titled “Minor Changes”-
@vispero/cora/vitenow stamps Astro components too: each host element of a.astrofile’s template carries its place in the source, so a finding in an Astro app names the.astrofile and line to fix. The plugin parses each template with Astro 7’s compiler,@astrojs/compiler-rs, which Astro 7 installs; an Astro 6 project adds it withnpm install -D @astrojs/compiler-rs. Custom elements are stamped; components,Fragment,<slot>,<script>and<style>are not, and a framework component an Astro page renders is stamped in its own file. Register the plugin inastro.config’svite.plugins. See Source locations. -
cora baseline updatenow counts issues, as the scan does, and the baseline holds one entry for an issue at a call site. The update printsIssues: 9 added, 0 removed, 0 changed, 0 kept, where it printedViolations:and counted the baseline’s entries. Its second line is nowNot verified: N issues, the issues the baseline still holds that the run could not verify, counted as the report counts them, whereNot fully verified:counted the entries with any page the run did not check. From an empty baseline,addedis the scan’s count of new issues. See Updating the baseline.The baseline format changes, and a baseline written from a build with source stamps must be accepted again. In
cora-baselineversion 1, a page entry’ssourcenow names itsresolution.elementis an element with a source stamp of its own, recorded as before.call-siteis new: elements with no stamp of their own, such as those a component fromnode_modulesrenders, are recorded as one entry at their call site, which lists each element, where each element was an entry of its own. One more element at an accepted call site is still a new violation. A baseline in which a page entry holds asourcewithout aresolutionno longer reads:cora scan,cora report merge,cora baseline updateand cora-mcp’s tools that read the baseline stop withcontract.invalid. If your project builds with source stamps, deletecora.baseline.json, scan, and accept the issues again withcora baseline update, even where the file still reads, so each issue at a call site becomes one entry. Do it from a commit whose gate passes, since accepting again also accepts whatever is new at that moment. A baseline from a build without source stamps reads and matches as before. See With source stamps.Update
corawherever the baseline is read, such as CI and each developer’s machine. An earlier release refuses a baseline that holds a call-site entry, and itscora baseline updaterefuses to rewrite one whose page entries hold asource.A compared results file, such as the report directory’s
results.json, holds baseline entries in the same shape. One that an earlier release wrote with such an entry no longer reads, so scan again for any results file you keep. Incora-resultsversion 1, a source coverage entry also names at least one file.Reports count an issue at a call site once.
summary.md’sFixed since the baselinecounts it when none of its elements is found, where it counted each element, and a fixed issue’s row names its source file where the baseline records one. In the SARIF log, an accepted issue that the run did not verify is located at its source file when it is at a call site, as one on a stamped element is, under its issue’s fingerprint. So in GitHub code scanning it keeps the one alert the issue has when a run finds it, where it gave an alert for each element. -
cora scancrawls a site from the command line.--crawl <url>adds a crawl from a start page, once for each crawl, and--crawl-max-pages,--crawl-max-depth,--crawl-excludeand--crawl-ignore-robots-txtset the limits and patterns every crawl on the command line shares, named for a crawl’s keys in the config. With page targets, they replace the config’stargets, as incora scan --crawl https://example.com/ --crawl-max-pages 50. See Targets, and, from a folder outside a project, Crawl a site.A crawl’s
maxPagesandmaxDepthnow default to 25 pages and two links deep, in the config and on the command line, where both were required. A config with a crawl reads as before, though itsconfigHashchanges once, so shards scanned before the upgrade and after it make no one shard set forcora report merge.cora scanlists each crawl that stopped at a limit with links left to follow underCrawls stopped at a limit, naming the setting to raise.A crawl of another site now skips what its
robots.txtdisallows. A crawl readsrobots.txton each origin its links lead to but the origin of yourbaseUrl, and follows no link it disallows for CORA, whose product token isVisperoCORA, or for every crawler,*, where no group names CORA, as RFC 9309 sets out. Arobots.txtthat answers with a server error or HTTP 429, or cannot be fetched, holds back every link on its origin. The results list each page held back as not scanned, under the new reasondisallowed-by-robots, so the gate is incomplete, and the scan exits 2, unless new issues fail it. A crawl’s start page, a page a target names and a page a redirect lands on are scanned whateverrobots.txtsays, but in a sharded run a landing page it disallows that another shard owns is scanned by neither shard, and the run is incomplete. A crawl of your own app under its base URL is unchanged. To follow such links on another origin you own, set the crawl’s newignoreRobotsTxttotrue, or pass--crawl-ignore-robots-txt. See Crawls and robots.txt. -
An issue on a page outside the app’s base URL now keeps the page’s host in its identity, so two sites’ issues stay apart. A page on the origin of
baseUrl, its scheme, host and port, from the config,--base-urlorCORA_BASE_URL, is identified by its path alone, as before, so a preview deployment andlocalhoststill share fingerprints when each run’s base URL names the origin it scans. Any other page, and every page of a run with no base URL, such ascora scan https://a.example/ https://b.example/outside a project, keeps its host: one rule’s violation on one element at the same path of two sites is now two issues, in the scan summary,summary.md, the annotations, the SARIF log, cora-mcp’s scan results, where each has its own ID, and the baseline.www.example.comandexample.comare two hosts, and a port is part of the host, so a dev server that starts on another port thanbaseUrlnames is another site until the run’s base URL names that port. A page requested under the base URL that settles on itshttpsupgrade stays under it. A project that scans full URLs with nobaseUrl, such as a preview host per pull request, now keeps each host, so its issues read as new on every new host: setbaseUrl, or--base-urlorCORA_BASE_URLin CI, to the origin each run scans, rather than accepting the issues again. See One issue on two sites.Issues on pages outside the base URL get new fingerprints, so a baseline’s entries for those pages must be accepted again. Their violations read as new until you run
cora baseline updatewith a scan’s results, which records each such page with itshost. The update cannot tell which site an old page of such an entry was on, so that page, the one without ahost, stays as not verified: delete it fromcora.baseline.jsonby hand once the issue is accepted again. Entries for pages under the base URL keep matching as they did. Do it from a commit whose gate passes, since accepting again also accepts whatever is new at that moment.The formats change. In
cora-resultsversion 1, every target gainskeepsHost, which says whether the target’s issues keep its host. A results file an earlier release wrote has none, so it no longer reads:cora report mergeandcora baseline updatestop withcontract.invalid, andcora statuspasses over it with a warning, so scan again for any results file you keep. The fixer’s loop records hold a target too, so cora-mcp cannot read one an earlier release wrote. A scan starts no loop for that record’s issue, andverify_fix, or a fix of it passed tosummarize_fixes, fails with the new errormcp.loop-unreadable, which names the record. CORA never deletes it: delete the record, or.cora/loop/in a project where the fixer ran before this release, and scan again. Incora-baselineversion 1, a page gains an optionalhost, and a baseline an earlier release wrote still reads.cora baseline updatenow records an element whose opening tag you edited under its new tag, and counts it as one changed issue, where the entry kept its old tag. An entry no longer keeps a page whose every violation carries another tag, as merged runs of differently stamped builds could leave, so a new violation elsewhere under the entry’s tag is no longer accepted through a page no element there carries. The update writes the same baseline whatever order the results list findings in and the file lists entries in, and a second update of the same results changes nothing. See Updating the baseline. -
cora initnow registers CORA’s Vite plugin in a Nuxt or Astro project, invite.pluginsofnuxt.configorastro.config, in their JavaScript and TypeScript forms, where those frameworks read Vite’s plugins; it no longer registers it in avite.configbeside their config, which they do not read. A config it cannot edit, such as one whosevitekey is not an object literal, is left as it is, and a Nuxt or Astro project without a config of its framework is toldDid not set upthe plugin there, since it found none it edits, beside a Vite config too, each with the docs page that shows the edit by hand. See Source locations. -
Every report surface now words a report one way. The scan summary,
cora status, the report directory’ssummary.md, the GitHub Actions annotations and the SARIF log name each section, severity, coverage reason and settle timeout alike, as in an annotation’s title,CORA critical issue, new: axe/image-alt, and a reason,the page did not load (HTTP 500). They list issues by source file and position, then by page, and place each issue at its source position where it has one, and at its page otherwise. A browser and viewport are named only where a run covers more than one, and counts group their thousands, as in1,204. Where a surface has a limit, such as the 10 annotations of each level GitHub shows, it keeps the first issues in that order.The scan summary’s
Issues:line names each kind of issue above zero, as inIssues: 12 (2 new, 1 needs review, 5 accepted, 3 report-only, 1 found by an agent), where it readIssues: 12 (2 new, 3 report-only), 1 needs review.Targets:counts only pages asnot scanned, and reads2 scanned before they settledwhere it read2 did not settle. Results that cover source files add aSource files:line, with the files no engine could check listed underSource files not scanned:. The gate’s headline counts them apart too, as in1 target and 2 source files were not scanned. See Results.cora statuscounts each scan’s source files apart from its targets the same way, as in2 targets (1 not scanned), 3 source files (1 not scanned), 4 issues (1 needs review). Incora status --json, thecora-statusformat’s version 1, each scan gainssourceFiles, the distinct source files its run covers, andsourceFilesNotScanned, those no engine could check, both present exactly where the run covers source files. See Checking the account, the scans and the fixes.In cora-mcp’s scan result,
cora-mcp-scanversion 1,counts.violationsis nowcounts.issues, and counts every issue of the run, those that need review included, ascora scan’s summary does: a script or agent that read the issues with a violation takescounts.issuesminuscounts.needsReview. Each issue’sruleis nowrules, every rule of its findings, sorted, so read the rule from that list, and itswcaglists every criterion they name. Every tool now marksreport: trueandaccepted: trueby the sectioncora scan’s report lists an issue in, so an issue that needs review never carriesreport: true.verify_fixgivesreport: trueonly for an issue its rescan still finds, astill-failingverdict, where it also gave it for any verdict whose rescan ran the issue’s rule asreport.SARIF rule links, each rule’s
helpUriand the link in its help, now open CORA’s guidance page for each rule that has one, as the rules reference does. The SARIF log also notes each target and each set of source files the run could not scan, as a warning of its run’s invocation.summary.md’s headings and counts table have changed, for anyone who parses them. Its columns areIssues,New issues,Needs review,Accepted,Report-only,Found by an agent,Fixed since the baseline,Not verified,TargetsandNot scanned, thenSource filesandSource files not scannedwhere the results cover source files. Its sections take the same names, each with a sentence under its heading, and an issue’s Where cell gives its source position or its page, not both. -
Outside a project, CORA now keeps a scan’s results in the folder you run it in, beside the baseline, which already went there. In a folder with no CORA config and no
package.jsonthat lists@vispero/coraabove it,cora scan <url>writes its run to.cora/runs/, its report directory in CI to.cora/report/, and the log files you ask for and crash files to.cora/logs/, and the summary names them from that folder.cora status,cora report mergeandcora baseline updatework on that.cora/, socora statuslists only that folder’s scans..cora/ignores itself in git, andstateDirectoryorCORA_STATE_DIRECTORYmoves it, relative to the folder. In a folder it cannot write,cora scanfails before it scans with the new errorcli.state-unwritable. Where the folder has not installed CORA, the summary’s command to accept issues isnpx @vispero/cora baseline update, no longernpx cora, which would run another npm package. See Scanning without a project. Getting started now opens with a first scan throughnpx @vispero/cora, which installs nothing into a project.cora-mcp started outside a project keeps its runs, loop records and browser files in
.cora/in the folder its client starts it in, and creates it only when a tool first writes state there: a scan,verify_fix,summarize_fixesor a browser tool. Listing its tools, and the tools that only read, write nothing. In a folder it cannot write, a tool that writes state fails with the new errormcp.state-unwritable, which names the directory and the fixes.CORA no longer has a per-user state directory, such as
~/.local/state/coraon Linux, and no longer readsXDG_STATE_HOME, which the plugin’s cora-mcp entries no longer pass on: remove it from your own Codex entry’senv_varstoo. Run files left there stay where they are. -
A source stamp no longer enters an element’s selector.
@vispero/cora/vitenow writes each stamp’s digest of its file in 20 characters, where it wrote 8, so every stamp is longer than the attribute values axe-core names an element by. A stamp of a tag in a file with a short path, such as a button on line 8 ofsrc/App.tsx, could enter the selector of its element or of an element inside it, so that element’s fingerprint differed between a stamped build and one without stamps, and changed with every edit of the file. In the source stamp’s format, version 1, the digest is now 20 characters. See The attribute.The element fingerprints of those issues change once. A violation on an element with a stamp of its own still matches its baseline entry by its source, so the gate passes, and the next
cora baseline updaterecords the element under its new fingerprint. A violation at a call site, or with no source location, matches its entry by its element fingerprint alone, so one whose selector held a stamp reads as new until you accept it again withcora baseline update. Do it from a commit whose gate passes otherwise, since accepting again also accepts whatever is new at that moment.A page built with an earlier release’s plugin carries stamps this release does not read, so its findings carry no source location until you build the app again. See Findings at their source.
-
@vispero/cora/vitenow stamps Svelte components too: each host element of a.sveltefile carries its place in the source, so a finding in a Svelte or SvelteKit app names the.sveltefile and line to fix. The plugin parses each component with the project’s own Svelte, 5 or later, before any preprocessor runs. Custom elements are stamped; components and thesvelte:elements are not. A file whose name holds{or}is named as written. Register the plugin beside@sveltejs/vite-plugin-svelte, or SvelteKit’ssveltekit(). See Source locations. -
@vispero/cora/vitenow stamps Vue single-file components too: each host element of a.vuefile’s template carries its place in the source, so a finding in a Vue app names the.vuefile and line to fix. The plugin parses each template with the compiler and template options of the project’s@vitejs/plugin-vue, on Vue 3.4.1 or later. A tag Vue takes as a component is never stamped, and a custom element is stamped when the Vue plugin’sisCustomElementnames it. Register the plugin beside@vitejs/plugin-vue, as in a React project. See Source locations.
Patch Changes
Section titled “Patch Changes”-
cora mcpandcora initnow run a cora-mcp release npm has cached without asking npm’s registry. Socora mcpstarts where npm’s proxy or registry can’t be reached, such as a corporate proxy while you’re off its VPN, where it waited past your agent’s startup timeout. They ask the registry only for a release npm hasn’t cached.The plugin’s MCP entry still asks the registry on each start, and a connection to the proxy or the registry that never opens holds that start. npm’s registry says how to start cora-mcp on such a network.
-
A secret written straight after an escape character, such as an account token, an npm token or a URL’s
tokenparameter after a bareESC, is now removed whole, unless another terminal sequence also splits it, from stderr, the log and crash files, the reportscora infoandcora statusprint, and cora-mcp’s error results. Where the characters after the escape could close a terminal sequence it began, such as a secret’s first letter or a URL parameter’s?and first letter,coraand cora-mcp read them as one sequence and left the rest of the secret in the text. -
A renewal of a signed-in account token that the CORA service cannot finish no longer leaves you a refresh token it refuses, which made you run
cora auth loginagain. When the service fails after the identity provider has already replaced your refresh token, such as when it cannot verify the provider’s answer, its answer now carries the replacement, and cora and cora-mcp save it in your credentials file, so a later renewal can still renew the token. Such a failure is still reported asservice.unavailable. cora and cora-mcp also keep the refresh token a renewal’s answer carries when the new account token cannot be verified, and send a renewal whose answer did not arrive, or that the service could not serve, once more, so a connection that drops no longer costs you a sign-in either. A renewal whose answer is lost twice, or arrives only at the request timeout, can still leave a refresh token the service refuses later, asaccount.expired.The login exchange,
cora-loginversion 1, gains the error codetemporarily_unavailableand an optionalrefresh_tokenon its error response, which its record inschemas/describes. A release before this one reports that code asaccount.expired.
Minor Changes
Section titled “Minor Changes”coranow honorsACCESSIBLE, the variable some tools’ prompts read for their screen-reader mode: set to anything but0orfalse, it turns onscreenReaderwhere neitherCORA_SCREEN_READERnor your user config sets it, so a person who set it once for every tool need not set CORA’s apart. Either of those still turns the setting off forcoraalone. The environment reference lists it, with the terminal’s variablescoranow reads to tell whether your terminal shows symbols such as✓.coratakes new global options for one command:--colorcolors its output for people even where it is not a terminal, such as on a pipe or in CI, and--no-colornever colors it. Each outranks your user config’scolorand the variables that set the same thing, such asNO_COLORandFORCE_COLOR, and of the two the last one given wins. The report of an invalid argument andcora’s help honor them too.- Two more of Playwright’s variables are ignored, each of which would make Playwright treat content a person cannot reach as reachable, in an agent’s snapshots and in the actions of a config’s setup or an agent:
PLAYWRIGHT_SKIP_INERT_CHECKandPLAYWRIGHT_SKIP_DIALOG_CHECK. They still apply wherever else you set them, your own Playwright tests included. cora initno longer writes its changes unconfirmed where it cannot ask you. Unless CI or a coding agent runs it, or you pass--yes, it now writes nothing and fails withcli.confirmation-requiredwherever it cannot ask: with stdin or stderr redirected, under-qor with--log-format json. A script that runscora initoutside CI needs--yes. At the question, Ctrl+C now stopscora initas it stops any other command, writing nothing and ending it by the interrupt, exit code 130 in a shell, where it answered no and exited 0. Escape still answers no, and Ctrl+D now does too, wherecora initstopped there without finishing, losing what it had still to print. A signal after you answer, such as Ctrl+C during the fetch, no longer adds a blank line to the output, and a crash while it asks no longer leaves your terminal’s cursor hidden.- A new setting,
screenReader, hascorawrite its output for a screen reader, such as NVDA, JAWS or VoiceOver. Turn it on with"screenReader": truein your user config, or by settingCORA_SCREEN_READERto1in your shell, which outranks your user config. With it on, a question such ascora init’s is asked line by line, on a line of its own with its choices in words, and you type your answer on the next, and progress such ascora scan’s is printed as new lines rather than as one line rewritten in place, at most one every 10 seconds while nothing else is printed, so nothing moves the cursor or redraws a line. It is off until you turn it on, since CORA does not detect a screen reader. The configuration guide’s Screen readers section gives the variable’s form in each shell, and says what the setting changes and what it leaves alone.
Patch Changes
Section titled “Patch Changes”-
An account token cora or cora-mcp refuses, a login answer it cannot read, and a request that cannot reach the CORA service now say what went wrong in CORA’s words rather than those of the libraries it uses, which the log keeps. A token issued by another service reads
its `iss` claim does not name https://cora.vispero.com, where it readunexpected "iss" claim value; a login answer that is not JSON readsThe service's answer could not be read: it is not JSON; and a refused connection readsCould not reach https://cora.vispero.com: the connection to 127.0.0.1:8080 was refused (`ECONNREFUSED`), with the network error’s code, where it gave Node.js’s message. The code of a certificate the machine does not trust is now written as code, such as(`SELF_SIGNED_CERT_IN_CHAIN`).cora scan’s wait for the config’sserver.urlwords what the URL last did the same way when it times out asserver.timed-out:the connection to 127.0.0.1:5173 was refused (`ECONNREFUSED`)where it saidit refused the connection,this machine does not trust the certificate it presented (`DEPTH_ZERO_SELF_SIGNED_CERT`)where it saidits request failed: DEPTH_ZERO_SELF_SIGNED_CERT, with a hint to turnbrowser.ignoreHTTPSErrorson in the config or have the server present a certificate the machine trusts, andit last answered with status 404where it saidit last answered 404. -
At a terminal, the values after their keys now start in one column, the keys padded with spaces, as Vite’s and pnpm’s summaries align theirs: in
cora info’s sections,cora auth status,cora init’sFound:, the countscora baseline updategives, and a scan’s and a merge’s summary, such asResults:andReport:. On a pipe, in CI, under a coding agent and withscreenReaderon, each is akey: valueline. -
An invalid argument to
corais now reported in CORA’s own words, with the argument as code, such asUnknown command `scna`or`--shard` cannot be "5/4": Expected `i/n`, …, rather than commander’s text with its quotes.cora hookrefuses a--harnessit does not know ascora initrefuses an--agent, such as`--harness` must be `claude` or `codex`, not "cursor", and its help lists the choices;cora statusrefuses a value of--subagent-status-linethe same way, such as`--subagent-status-line` must be `claude`, not "codex". The hint names the help that answers it, the command’s own for its options and arguments, such asRun `cora scan --help` for its options., and carries the suggestion for a typo, such asDid you mean `cora scan`?, which no longer sits on a line of its own. A command’s help now lists the global options too, such as--log-level,--colorand-q, under its own.cora hook’s help says thatcora hook --helpprints nothing, since the hook’s stdout holds only its answer, and thatcora help hookprints the help. -
cora auth logoutnow saysAlready signed out: there was no saved credentialwhen there was nothing to remove, where it saidSigned out. -
cora auth status’s keys are now in sentence case, such asOrganization:andPlan:, it saysAccount: valid, and it gives when the token expires relative to now, such asExpires: in 23 hours, where it gave an ISO time.cora auth loginnow gives the page and the code as a step underTo sign in:, where it said them in one sentence, with the code written without backticks, so a double click selects the code alone, and it marksSigned in: …with✓at a terminal, ascora auth logoutmarksSigned out.cora auth logout’s note thatCORA_TOKENis set, and still provides a credential, is now a notice of its own, with▲at a terminal. -
cora baseline updatenow says what it wrote, then counts the violations on lines of their own:Wrote cora.baseline.json, thenViolations: 3 added, 1 removed, 2 changed, 12 kept, then, when some are,Not fully verified: 2 violations, where it printedBaseline cora.baseline.json: 3 added, 1 removed, 2 changed, 12 kept; 2 not fully verified. At a terminal, a✓marks what it wrote. -
Errors’ messages and hints,
cora’s help and the linescoraprints for people now write each command, flag, environment variable, setting, path or package they name in a sentence between backticks, such asSign in with `cora auth login`, or set `CORA_TOKEN`, so the name stands out from the sentence with color off too, and the CLI reference and the errors reference show it as code. The backticks stay in every mode. Code a line shows on its own takes none, so it copies whole, such as the path afterResults:, a command underNext, run:or the file inWrote cora.config.ts. A coding agent reads the same backticks in cora-mcp’s tool errors and in the message a failedcora hookshows. -
cora initsets up CORA’s fixer for Codex only from a file that is there, so Codex findsa11y_fixerin a package of an npm or Yarn workspace that rancora initbefore installing@vispero/cora. Before the install, it no longer declares the fixer atnode_modules/@vispero/corabeside the package’spackage.json, where npm and Yarn, which hoist packages to the workspace root by default, do not install it. Instead it prints a run after the install,npx cora init --agent codex, which declares the cora that was installed. On Yarn’s Plug’n’Play, which keeps@vispero/corain a zip archive Codex cannot read, it writes a copy of the fixer to.codex/agents/a11y-fixer.toml; runcora initagain after you upgrade cora to refresh it. Where apackage.jsonlists cora but none is installed, outside Plug’n’Play, its next steps start with the package manager’sinstall. In a Yarn project, its next steps, and the commandcora scanprints to accept new violations, run cora asyarn cora. If an earliercora initdeclared the fixer at a file that is not there, the nextcora initpoints the declaration at the installed cora, or at the copy on Plug’n’Play, changing only that value.cora initnow says why where it leaves the project without a fixer Codex loads: a declaration Codex does not load, a file at the copy’s path that is no fixer, or an installed cora too old to hold one, which it says to upgrade.The hooks read the agents Codex loads by Codex’s own rules: a
config_filethat starts with~/names a file in your home directory, and an agent without a description, an agent file in.codex/agents/withoutdeveloper_instructions, an agent withnickname_candidatesCodex rejects, or a role in anagentstable Codex rejects, which Codex ignores, is no fixer. -
When something CORA reads is invalid, its error now says what is wrong in CORA’s words, rather than zod’s. This covers a config, a flag, an environment variable and the user config; a results file, a baseline, a loop record, a hook record and the credentials file; the arguments of a cora-mcp tool call; and the service’s login responses, an account token’s claims, a target and a page’s answer to CORA. An issue reads like
page.timeout: Expected a number,Unknown key `pageTimeout`,`CORA_LOG_FILES_KEPT` cannot be "0": Expected more than 0,runs: Expected an arrayorexpires_in: Expected a whole number, where zod saidInvalid input: expected number, received stringorUnrecognized key: "pageTimeout". A value that takes a closed set names it as a sentence does, such as`--log-format` must be `human` or `json`, not "xml". -
cora’s error reports now name each file and directory relative to the directory you ran the command in, as you would type it, such asThe CORA config from `cora.config.json` is invalid, where they named its absolute path; a path outside that directory stays absolute, and.names the directory itself. The log file keeps the absolute paths. Whencora report mergeorcora baseline updatecannot read some inputs, each one is listed once, with its reason, where it was named twice. -
An error’s report at a terminal now shows its code, such as
error[config.invalid], in bold in your terminal’s red, its message in your terminal’s own color, itshint:anddocs:labels in bold, and itsdocs:address underlined, where the whole message was red. The code between backticks in the message and the hint shows in your terminal’s cyan, its backticks kept. The report stays plain wherever color is off, and on a pipe, in CI and under a coding agent unless you force color. -
CORA’s fixer is kept going while files it checked change. In Claude Code by default, and in Codex, the fixer runs in the background, so the main agent can finish its turn, and your next request can start, while the fixer still works.
- At each of the fixer’s calls of CORA’s tools, the hooks record a digest of the content of the files it knows of. When it finishes while a file differs from what the newest scan or
verify_fixof each page saw, the hooks keep it going: it verifies its own fixes there, scans those pages again, works what it finds, including a fix of its own that the change undid, and starts its answer with every result of its run. A file it held that no scan saw as it is goes back to the main agent’s next check. Because the check compares content, a change made through a shell command and a deleted file count, and a file rewritten unchanged does not. - In Claude Code’s auto mode, the fixer hands its report back through a tool call that ends its run, so the same check runs before that call. The hooks refuse the hand-back at most twice, with what to do.
- When the main agent finishes a turn with new UI edits while the fixer still runs, the hooks hand the edits to that fixer, tell it of them at its next call of CORA’s tools, and ask the main agent to leave the files alone rather than dispatch a second fixer. In Claude Code a fixer counts as running while Claude Code lists it among the session’s background tasks.
- The hooks no longer count a test file, such as
Form.test.tsx,Form.spec.tsxor Cypress’sForm.cy.tsx, as a UI edit to check, so writing a test no longer asks for the fixer. - The
cora:a11y-check-ui-editsskill and the hooks’ dispatch instruction ask the main agent to run the fixer in the foreground where it can. To have Claude Code’s main agent wait for the fixer, setCLAUDE_CODE_FORK_SUBAGENTto"0"in your settings’env, as In Claude Code describes, with its cost. - The plugin’s Claude Code hooks now also reach the fixer’s calls of CORA’s tools when it runs as the project agent,
cora-a11y-fixer.
The hook record’s format,
cora-hook-stateversion 1, gains the optional fixer fieldsfiles,owed,snapshots,askedToRescan,seenAt,handbacksRefusedandhandedBack, which its record inschemas/describes. - At each of the fixer’s calls of CORA’s tools, the hooks record a digest of the content of the files it knows of. When it finishes while a file differs from what the newest scan or
-
The gate’s verdict now counts zero as a number and takes no full stop, as one statement:
Gate passed: 0 new issues at or above minor,Gate failed: 2 new issues at or above minor, and 1 target was not scanned, where it saidNo new issuesand ended each sentence with a full stop. It reads so in the summary on stderr, in the Markdown summary,summary.mdand the GitHub Actions job summary, and in the verdict’s GitHub Actions annotation. -
An option that takes a closed set of values lists them in its help, and in the CLI reference, as code after its description, such as
the coding agent that runs the hook (`claude` or `codex`)forcora hook --harness, where it gave(choices: "claude", "codex"). -
cora’s help now takes your terminal’s styles, as gh’s and cargo’s do: its section titles, such asUsage:andOptions:, in bold; each command’s name and each option’s flags in cyan, with placeholders such as<url>left plain; the code between backticks in a description in cyan, its backticks kept; and each example’s command in cyan. Its colors follow the rest ofcora’s output:--coloror--no-color, wherever it sits on the command line, then your user config’scolor, thenNO_COLORandFORCE_COLOR, and it stays plain on a pipe, in CI and under a coding agent unless color is forced.cora --versionstays plain. -
In an IDE’s integrated terminal where Claude Code’s extension sets
CLAUDECODE,corano longer takes you for Claude Code:cora initasks before it writes,cora scan --headedcan show its browser,cora auth loginopens your browser only where your platform can show it, printing the page and code alone over SSH on macOS or Windows, or on Linux outside WSL without a display, andcora infonames no coding agent runningcora. Claude Code runscorawhere it also setsCLAUDE_CODE_CHILD_SESSION, or where stdin is not a terminal, as in its commands, hooks and MCP servers.cora initstill sets Claude Code up from such a terminal, and now finds Claude Code running it by theAI_AGENTvalue Claude Code sets. OpenCode’s, Goose’s and Junie’s variables in your own shell are weighed the same way.cora infoalso names the coding agent whose variables your environment carries, beside the one that runscora, asIn environmentand, with--json,codingAgent.inEnvironment: in such a terminal, it names Claude Code there, so a bug report shows whycora initoffered to set it up. -
cora infonow reports an emptySSL_CERT_FILEorSSL_CERT_DIRas set, since Node.js reads an empty one as naming no certificates, which leaves CORA’s requests to its service without the system’s roots. -
cora info’s keys are now in sentence case, such asVersion:,State directory:andConfig hash:, where they were lowercase, withconfigHashin camelCase. A value it did not find readsnot found, a user config that is missing included, where it readNot foundormissing. TheService:section gives the service’s address on its heading line, ascora auth statusnames it, and its request timeout as every duration, such as30.0 s, where it read30000 ms. TheEnvironment:section lists the variables that are set, then the names of the others on one line, afterUnset:, rather than a line for each.cora info --jsonkeeps its field names. -
cora initfetches the cora-mcp release only where it sets a coding agent up: one it detects, one named with--agent, or Claude Code for--project-agent. A run that sets none up, such as one in a project with no coding agent’s files, from a shell with no coding agent’s CLI or variables, no longer downloads the release or printsFetched @vispero/cora-mcp@<version>. For an MCP clientcora initdoesn’t set up, such as Windsurf, fetch the project’s cora-mcp release once before the client’s first start, as Setting up cora-mcp shows. -
cora initnames each file it wrote, and each file a notice or an error names, ascora’s other output does: with your platform’s separator, such asUpdated .claude\settings.jsonon Windows, where it wrote/, and by its absolute path where the file is outside the directory you ran it in, such as an installed cora above it. Its diffs still name each file with/, as git’s do. -
cora init’s question,Write these changes?, now honors your user config’scolor, so"color": falsekeeps it plain at a terminal too, and takes CORA’s own look: the answer you have picked shows in brackets,[Yes] / No, and once you answer, the line keeps the question with the answer you gave, such asWrite these changes? Yes. These keys work as before:yornanswers, the arrow keys move between Yes and No, Enter gives the one picked, and Escape answers no.@vispero/coranow depends on@clack/corein place of@clack/prompts, which installs one package fewer. -
cora initnow says each thing it did once, in the shape it takes acrosscora. ItsFound:summary keys are in sentence case, such asPackage manager:andCoding agents:, it saysnot foundwhere it finds no package manager, and it names coding agents found the same ways together, such asClaude Code, Codex (their files in the project), where it joined each with its own reasons by semicolons. It names each file it wrote on a line of its own,Wrote cora.config.tsfor a file it creates andUpdated vite.config.tsfor one it changes, and says how long fetching cora-mcp took, such asFetched @vispero/cora-mcp@0.7.0 for coding agents in 1.2 s. At a terminal, each of these lines starts with✓. A file it leaves as it is, CORA’s fixer agent for Codex that it sets up only once cora is installed, a config above the project that the new one takes precedence over, and a cora-mcp release it could not fetch are each a notice after the files it wrote, which starts with▲at a terminal. When your project’s Prettier fails to load, or to format a new filecora initwrites, a notice after the diffs, before it asks, says so, such asLaid out `cora.config.ts` without the project's Prettier, which failed on it, so you can run your formatter on the new files. Each takes the place of aWARNlog record, and of the line that said the same where there was one. A file’s notice gives the docs page that shows the change to make by hand on a line of its own, and a failed fetch’s says why in the words ofcora mcp’s error, such asCould not fetch `@vispero/cora-mcp@0.7.0`: npm reported an error. Thesetup step skipped,config takes precedence over one aboveandprettier not usedrecords are now debug records, the last with Prettier’s own error, andrelease not fetchedis gone, since the debug recordrelease fetch failedkeeps the reason with npm’s own error.Wrote nothingandNothing to change: the project is set upno longer end with a period. -
GitHub Actions annotations now call what a scan reports an issue, as the summary on stderr does, such as
CORA new critical issue: axe/image-altandCORA serious issue to review: …, where they saidviolationandfinding. The Markdown summary,summary.mdand the GitHub Actions job summary, heads its first sectionNew issues, where it saidNew violations. The CLI reference’s exit code 1 readsnew issues at or above `policy.failOn`to match. -
Update jose, which verifies your CORA account’s token, to 6.2.15, and Babel’s parser and unplugin, which
@vispero/cora/vite’s plugin uses to stamp your JSX with its source locations, to 7.29.13 and 3.7.0. -
When the CORA service sends its signing keys in a form cora cannot read, the
account.invalidit reports now hints to update@vispero/coraor run the command again in a few minutes, as it does when the service cannot send them, where it told you to sign in again or check your token. -
Messages that name several files or inputs now list them as a sentence does, such as
Found no results files in `shards` or `retries`fromcora report mergeandafter `cora init` wrote `cora.config.ts` and `.mcp.json`, where they joined them with commas alone.cora hook’s notice that Codex’s fixer is not set up names the command to run as`cora init --agent codex`, withoutnpx, since the hook cannot know how your project runs cora. -
A log record on stderr in the human format now writes a field that holds an object as pairs of its own, as Go’s
log/slogwrites a group, such aserr.message="…" err.stack="…", so an error’s message and stack read with their line breaks escaped once, rather than as JSON inside quotes.--log-format jsonand the log files write such a field as JSON, as before. -
The Markdown summary,
summary.mdand the GitHub Actions job summary, now words its tables ascora scan’s summary does. TheTargets not scannedandRedirected to a scanned pagetables give each reason in words, then its code and its detail, such asthe page did not load (`navigation-failed`): `HTTP 500`, where they gave the code alone, and name each target’s browser and viewport where the run covers more than one, ascora scan’sNot scanned:list does. The table of targets scanned before their page settled gives each settle timeout ascorawords every duration, such as5.0 swhere it gave5000 ms. -
When
cora mcpcannot fetch the cora-mcp release, its error now says why in CORA’s words, such asCould not fetch `@vispero/cora-mcp@0.7.0`: npm reported an error, where it gave the last lines of npm’s own error. npm’s error is now a debug record, whichcora --debug mcpprints and keeps in the log file. When cora-mcp cannot fetch the release that matches the project’s cora, itsmatching release unavailablewarning now gives the reason, such asfailedortimed-out, and npm’s error stays in the debug recordrelease fetch failed. -
Each of
cora init’s next steps is a line of its own, so each command copies whole: for Codex,Then trust CORA's hooks when Codex asksfollows its commands as a step of its own, where it ended the last command’s line. Where nopackage.jsonis there to installcorainto, the commandcora scanandcora report mergeprint to accept new violations saysnpx @vispero/cora, ascora init’s next steps do. -
cora scanandcora report mergenow list each target they did not scan underNot scanned:, before the verdict, with why in words and the detail the browser or the engine gave in parentheses, such ashttp://localhost:4173/: the page did not load (HTTP 500). At a terminal, each starts with▲. A target that redirected to a page the run scanned counts as scanned, as before, and is not listed. The progress line’sRetryinggives its reason in the same words, such as(the page did not load)where it gave(navigation-failed), and the coverage reasons reference gives each code’s words. Thetarget skippedlog record, which said this before, is now at thedebuglevel, incoraand in cora-mcp, whose scan tools give a target they did not scan in their result. Thevisit retriedrecord, which the progress line shows asRetrying, is now at theinfolevel, where it was atwarn. -
On a Node.js release older than the one they support,
coraand cora-mcp now reportnode.unsupported-versionbefore they load any of their dependencies, so a dependency that needs a newer Node.js can no longer fail first. Every Node.js release from 16.20 on that is older than the one they support gets the report. -
cora’s output for people now sets its groups apart with one blank line, as gh and git do, and puts none at the start or end, none inside a group and never two in a row, on a terminal, on a pipe and under the screen-reader preference alike.cora initsets apart what it found, each file’s diff, its question, the files it wrote, its fetch of cora-mcp and the next steps, which it now prints last, after the fetch.cora info’s sections andcora status’sAccount:,Scans:andFixes:are one blank line apart, and a scan’s summary, an error report and theLog:line each sit one blank line below what came before them, a run of log records included. An error message’s blank lines are left out of its report, so itshint:follows its message directly. cora-mcp’s stderr, its client’s log, likewise sets its error report one blank line below its records. Scripts that read--jsonor--out -are unaffected. -
The report of a failure before a command starts, such as an invalid argument, now honors your user config’s
color, so"color": falsekeeps it plain at a terminal too. In CI and under a coding agent,corawrites plain, static stderr, a terminal included, such as the pseudo-terminal a coding agent may run commands in: no progress line and no color. Pass--color, set"color": truein your user config, or setFORCE_COLORto bring the colors back. -
When cora or cora-mcp cannot start a program, its error now gives the system’s code for why, such as
Could not start `npx`: `ENOENT`, rather than Node.js’s own text. An app server whose command cannot start reads the same way,Could not start the app server's command: `ENOENT`, where it readThe app server's command could not start: ENOENT. -
The progress line, such as
cora scan’sScanning 3/11, now follows a resize of your terminal: it is drawn again at the new width, and leaves no rows behind on the screen in a terminal that rewraps its lines as it narrows, such as GNOME Terminal, Windows Terminal or tmux. A terminal that moves rows into its scrollback as it narrows can still keep part of the line there. Where a terminal reports no width,coradraws no progress line, rather than one at full length that wraps. A progress line cut to fit your terminal now ends in…, or in...where your terminal draws no Unicode. -
Redacting a text, and removing other tools’ terminal control sequences from it, now take time that grows with the text’s length, where a few kinds of text took time that grew with its square, two seconds or more at 128K characters: a long run of
eyJwithout dots, a long run of operating-system commands with no terminator, and sequences each formed by removing the one inside it. Past 16 such layers, CORA drops every character left that a sequence starts with, the escape character among them, rather than removing more. CORA also no longer fails when a secret it knows holds a lone surrogate, such as a local-storage value in itsbrowser.storageStatefile. It now redacts, too, the userinfo of a URL that follows a digit or an underscore, such as_https://user:pass@host, and a secret it knows in a URL’s path segment that also holds a malformed escape, such as%zz, both of which it left as written. -
CORA now redacts two secrets whole where they overlap in a text, one ending with what the other starts with, where it left the end of one: two it knows, or one it knows and a credential it recognizes, such as a Bearer token, a JSON Web Token or a URL’s
tokenparameter. This holds wherever it redacts, such as its log and crash files and the lines it prints for people. What it redacts is replaced where it stands, so the rest of the text reads as written: the space afterBearerstays as it was, a URL keeps the case and the encoding it was written in, and the punctuation written after a credential in a URL’s query, such as a:or a closing), stays. -
CORA now redacts an environment variable’s value that is a file’s path, such as an
SSL_KEY_PATHit was given, where it names that path from the current directory or the project root, as its summaries, its notices and a log record’spathdo, and where@vispero/cora/vitenames it from Vite’s root in a warning. It redacted the path only where it was written in full. -
CORA now redacts a secret that another tool’s terminal control sequence splits, such as a color in a Playwright message that falls inside a Bearer token, a JSON Web Token or a secret CORA knows. This holds for the lines it prints for people, its log and crash files, its JSON log records on stderr, the reports
cora status --jsonandcora info --jsonprint, the message a coding agent shows for a failedcora hook, and cora-mcp’s error results. A text where that happens is written without its control sequences. A secret that holds a line break, such as a PEM key, is now redacted in an error’s report on stderr and incora status’s account lines, where each of its lines was printed. -
The
cora:a11y-reportskill no longer asks to read a second file of its own. In Claude Code it asked permission to read itsexamples.mdfrom the plugin’s cache before it presented the fixer’s summary, and indontAskmode the read was denied. Its example reports are now part of the skill. -
cora scan’s andcora report merge’s summary now gives the gate’s verdict, then its counts and files under their keys, and puts the command that accepts new violations on a line of its own underTo accept them, run::Gate failed: 2 new issues at or above minorTargets: 11 (1 did not settle)Issues: 10 (2 new), 1 needs reviewDuration: 14.2 sResults: .cora/runs/20261006T170501123Z-k3j9x2ab.jsonTo accept them, run:npx cora baseline update .cora/runs/20261006T170501123Z-k3j9x2ab.jsonA merge gives
Runs: 4 from 4 filesin place ofDurationandResults. At a terminal, the verdict shows as a badge,[Gate failed]. A script that reads the summary, such as one that matchesScanned 11 targetsorTo accept them: npx cora …, needs its pattern updated; the exit codes are unchanged. -
With
screenReaderon,cora initnow prints each file’s diff in words: a line names the file and counts the lines it removes and adds, such asChanges to vite.config.ts: 1 line removed, 2 lines added, orNew file cora.config.ts: 8 lines added, a line names each stretch’s lines, such asLines 1 to 7, now lines 1 to 8:, and each line under it starts withadded:,removed:orunchanged:in place of its+,-or space, since NVDA does not speak a-at its default punctuation level. Without the setting, the diff stays a unified diff. -
cora statusnow lists its scans and fixes as records: at a terminal, in columns under their headings, such asRun,TargetsandIssues, with✓,✗or▲beside each verdict; on a pipe, in CI, under a coding agent and for a screen reader, one labeled line each, such as20261006T170903123Z-a1b2c3d4: 1 target, 1 issue, 0 need review, from mcp, 2 minutes agoandin progress: rule axe/button-name, page http://localhost:5173/settings, element button.icon, last result still-failing, 2 attempts. A run ID, a rule, a page and an element are never cut. The account now gives its organization, its plan and when its token expires, as inAccount: valid, Acme, plan team, expires in 23 hours. Outside a project, it saysProject: not foundrather than leaving you to readScans: none. When there are more results files than the five it reads, the scans end with a count of the rest and where they are, as inand 12 more in .cora/runs.cora status --jsonkeeps its fields. -
cora’s own lines on stderr now take your terminal’s styles by what each part means. A scan’s and a merge’s summary show each key and heading in bold, such asResults:andTo accept them, run:, each count with its words in bold, in the verdict’s line too, such as2 new issues, and the verdict in bold green, red or yellow, or at a terminal as a badge in that color. Each path and the command that accepts new violations are in cyan, each address is underlined, and the duration and the detail of why a target was not scanned are dimmed. The progress line shows a target’s place in the count in bold beside its underlined address.cora initshowsFound:andNext, run:as headings, what it found under keys in bold, withnoneandnot founddimmed, the diff’s file lines in bold and its ranges dimmed, and each command of the next steps in cyan.cora baseline updatebolds its keys and counts,cora auth loginunderlines the address it asks you to open and shows the code in cyan, theLog:line shows its path in cyan, and a log record’s time is dimmed. Code between backticks in a line’s sentence, such asCORA_TOKENincora auth logout’s notice, shows in cyan, its backticks kept. These lines stay plain on a pipe, in CI and under a coding agent unless you force color, and wherever--no-color,colororNO_COLORturns color off. An added or removed line incora init’s diff whose text starts with++or--now takes the green or red of any other added or removed line, where it was printed plain. -
The reports
cora info,cora statusandcora auth statusprint on stdout now take your terminal’s styles, ascora’s lines on stderr do: headings and keys in bold, the counts incora status’s scans and fixes in bold, as a scan’s summary shows them, andcora status’s verdicts in bold green, red or yellow, such as the account’svalidandnot validand a fix’sdone,escalatedorstill-failing. A word that says a value is absent, such asnot foundornone, is dimmed, an address is underlined, and a path, such as each ofcora info’s, or the variablecora auth statusnames as a credential’s source, is in cyan. The reports stay plain on a pipe, in CI and under a coding agent unless you force color, and wherever--no-color,colororNO_COLORturns color off. -
Ctrl+C during
cora scannow stops the scan the waySIGTERMandSIGHUPdo:coracloses its browser at once, erases itsScanningline, writes what it logs as it stops, and ends by the signal, where Playwright’s own handler closed the browser and exited first, leavingScanning 3/11 …^Con screen. A browser still launching when you stop the scan is closed as soon as its launch returns, and one that does not close within 10 seconds, or that a second Ctrl+C finds still open, is killed beforecoraends, rather than left running. cora-mcp ends the same way on each of the three signals, closing a browser still launching before it ends. After Ctrl+C at a terminal, whatcoraprints next starts on a row of its own, below the terminal’s^C. -
When text from outside CORA holds another tool’s terminal control sequence, such as the colors in a Playwright message, a page’s title or a token’s claim,
coranow leaves the sequence out of the line it prints, rather than printing what remained of it, such as[2J. This holds for its lines on stderr, such ascora init’s diff and an error’s report, for the reports ofcora status,cora infoandcora auth status, for the message a coding agent shows for a failedcora hook, and for the fixer’s rows thatcora status --subagent-status-linewrites for a coding agent’s panel. In those three reports, a line break or another control character in such text now prints as a space, so a value can no longer add a line or move the cursor, ascora auth statusalready ensured. A control character in the message a failedcora hookshows, and in the report of a failurecoraor cora-mcp reports before it starts, such as on a Node.js release older than it supports, now prints as a space too. cora-mcp likewise leaves such a sequence out of the error report it writes to its client’s log on stderr. -
When
@vispero/cora/vitecannot read thesourceStampssetting, its warning now gives the error ascorareports it, with its hint and the address of its docs, and names each file inside Vite’s root from it, such asThe CORA config from `cora.config.json` is invalid, where it named its absolute path. -
When
cora auth login --with-tokenrefuses the token you piped in, finds none, or finds it expired, its hint now tells you to check that token or sign in withcora auth login, where it told you to check, or replace, the token inCORA_TOKEN. -
An argument cora-mcp does not take, in an MCP entry’s
args, is now reported in CORA’s words, with the argument as code, such asUnknown option `--bogus`or`--headless` cannot be "yes": Expected no value, rather than Node.js’s text with its quotes. -
Update playwright-mcp, which gives cora-mcp its
browser_*tools, to 0.0.84, whose snapshots leave out inert content and the page outside an open modal dialog, as a person meets them. cora-mcp servesbrowser_tabswithout the release’s newisolatedContextoption, which would open a tab in a browser context CORA’s scans do not read, and fails a call that names one asmcp.invalid-argument.
Minor Changes
Section titled “Minor Changes”-
cora auth loginandcora auth statusname your organization.cora auth loginprintsSigned in: organization <name> (<id>), plan <plan>, andcora auth statusprints anorganization name:line directly afterorganization:. Both read the name from your account token, without asking the CORA service.A token that does not carry the name, such as a CI token created before the service named organizations in its tokens, shows
unknownincora auth status, and the organization’s ID alone aftercora auth login. Create a new CI token to show the name.The account token’s format,
cora-tokenversion 1, gains the optional claimorg_name, which its record inschemas/describes. -
cora scanno longer hangs without output when Chrome does not start orbrowser.setupnever finishes. A launch that takes longer than 60 seconds now fails asbrowser.launch-failed, and abrowser.setupthat outlasts the newbrowser.setupTimeoutsetting fails assignin.setup-failed, naming the bound; both close the browser and exit 2.browser.setupTimeoutis in milliseconds, defaults to 60000, can be set in the project config or the user config, and can also be set withCORA_BROWSER_SETUP_TIMEOUT. Upgrade: abrowser.setupthat takes longer than 60 seconds now fails by default, incora scanand cora-mcp alike, so raisebrowser.setupTimeoutfor it, as Signing in describes; someone who raisedmcp.callTimeoutfor a slow sign-in raisesbrowser.setupTimeoutas well.browser.setupnow receives{ signal }as a second argument, which aborts whenbrowser.setupTimeoutpasses or the run stops, so a hook that waits on it can stop; a hook that ignores it can still hold the process open. cora-mcp honorsbrowser.setupTimeoutwithinmcp.callTimeout, whichever runs out first ending the opening and naming itself.cora scannow gives up on a browser that does not close within 10 seconds, with a warning, as cora-mcp already did. -
browser.ignoreHTTPSErrorsincora.config.tsorcora.config.jsonscans a dev server whose HTTPS certificate your machine does not trust, such as a self-signed one, as Playwright’s option of that name does. With it on, the browser ofcora scanand of cora-mcp loads such a page, andcora scancountsserver.urlready when it answers with such a certificate. Off, the default, such a page is recorded as not scanned withnavigation-failed, and the server’s wait fails withserver.timed-out, naming the certificate error. On, the browser checks no certificate on any host, so turn it on only for an app you serve yourself. It does not changeconfigHash, and only the config file sets it. -
cora scan --headedshows the browser, so you can watch it scan.browser.headedin your user config, orCORA_BROWSER_HEADED=1, shows it in every scan on your machine, and--headedwins over both. In CI, under a coding agent, or where no display can show a window, such as WSL without WSLg or an SSH session on macOS or Windows, the scan runs headless and warnssetting ignored, naming the setting and why. cora-mcp’s browser still followsmcp.headless. -
A page still loading when cora waits for it to settle is now held until its
loadevent, withinpage.settle.timeout, so it is scanned with its stylesheets. This reaches the page a redirect in the browser lands on, such as one a page’s script sends the browser to withlocation.replaceafter it loads, and a tab cora-mcp’sscan_pagescans before it has loaded. Such a page could be scanned before its stylesheets arrived and still be recorded as settled, so its findings can change, notably those of rules that read styles, such as color contrast. A page that has not loaded whenpage.settle.timeoutpasses is now recorded as not settled, and thepage did not settlewarning namesloadinwaitingOn. Raisepage.settle.timeouton the route of a page that loads slowly. -
cora-mcp’s browser runs headless where no display can show its window, such as on Linux without an X11 or Wayland display, under WSL without WSLg, or in an SSH session on macOS or Windows, rather than failing to launch with
browser.launch-failed. cora-mcp warnssetting ignoredas it starts, namingmcp.headlessandno-display. Setmcp.headless,CORA_MCP_HEADLESS=1or--headlessto run it headless without the warning. The plugin’s entries now forward an SSH session’s variables,SSH_CONNECTION,SSH_CLIENTandSSH_TTY, so cora-mcp sees an SSH session under Codex too.
Patch Changes
Section titled “Patch Changes”-
cora init’s next steps for Claude Code give one install command,claude plugin install cora --marketplace Vispero/cora-plugins --scope project, and no longer say that trusting the project loads the plugin with no command. Claude Code loads the plugin from a trusted project’s settings only in the session that first adds the marketplace, so later sessions start without CORA’s server and skills. If you set a project up with an earliercora initand skipped the install, each person who works on it runs the command once. -
A page that redirects in the browser two or three times in a row while cora waits for it, such as with
location.replaceafter each page loads, is now followed to the page the chain ends on and scanned there. cora counted each such redirect twice, so a chain of two left the target unscanned as a page that kept navigating (navigation-failed). A page that navigates more often than a chain of three redirects still ends that way, as the errors reference’spage.navigatedentry now says. -
The docs gain a Formats reference with a page for each file and result CORA writes for you or your agent to read: the results file,
cora-results, the baseline,cora-baseline, and the result of each of cora-mcp’s tools and its error result, fromcora-mcp-scantocora-mcp-error. Each page lists every field of each version, with its type, whether it is required and what it holds. The Scanning and Baselines guides link their file’s page, and the MCP tools reference links each tool’s result to its page.The JSON Schema records
@vispero/coraships inschemas/for these formats now describe the format and each of its fields, and the config’s and the user config’s records describe their file too.verify_fix’s description now names its fourth inconclusive case: the browser leaves the element out of its accessibility tree for a reason that shows neither a fix nor a hidden element, such as a modal dialog open over it. -
A rule’s
help.textin the SARIF log keeps the rule’s whole description, as itshelp.markdowndoes, rather than cutting it at 1,024 characters. Only itsshortDescriptionandfullDescriptionare cut, to the 1,024 characters GitHub code scanning accepts. A description that long comes only from a finding’s message, for a rule this version of CORA does not know. -
Each line
coraprints on stderr is one row: a control character in text from outside CORA, such as a claim of the account token, prints as a space, so it cannot move the cursor or break the line, and a tab prints as the spaces to the next tab stop, every 8 columns, so a diffcora initshows of a file indented with tabs keeps its indent. An error’s message still prints one line for each of its own lines. -
cora-mcp starts when npm’s cache holds an older list of cora-mcp’s versions, as after the plugin updates to a new release, or of one of its dependencies’. The plugin’s MCP entries, and the fetch that
cora mcp,cora initand cora-mcp’s launcher run before they use a release, runnpx --yes --prefer-offline=false --fetch-retries=0 --fetch-timeout=3000 @vispero/cora-mcp@<version>, which asks the registry for the version, even where your npm config setsprefer-offline, and starts it from npm’s cache when the registry refuses the connection or sends nothing for 3 seconds.cora init’s fetch waits 30 seconds instead, so a registry mirror slow to answer still leaves the release cached for the agent’s first start. Under--prefer-offline, npm failed withETARGETon every start instead. If you added cora-mcp to an agent by hand, replace--prefer-offlinein its entry with--prefer-offline=false --fetch-retries=0 --fetch-timeout=3000, as Setting up cora-mcp shows.
Patch Changes
Section titled “Patch Changes”- Update mdast-util-to-markdown, which writes the report directory’s
summary.md, and unplugin, which@vispero/cora/vite’s plugin uses to stamp your JSX with its source locations, to 2.2.1 and 3.6.0.
Minor Changes
Section titled “Minor Changes”-
SAGE is now CORA.
@vispero/coraand@vispero/cora-mcpreplace@vispero/sageand@vispero/sage-mcp, and every name the packages use changes with them: the commands, variables, project files, records, plugin, service and docs. Nothing answers to the old names, so a project upgrades in one step:- Install
@vispero/cora, and@vispero/cora-mcpwhere an agent runs it, and remove@vispero/sageand@vispero/sage-mcp. The commands are nowcoraandcora-mcp. - Rename every
SAGE_*variable toCORA_*, in CI too, such asSAGE_TOKENtoCORA_TOKEN. - Rename
sage.config.tsorsage.config.jsontocora.config.tsorcora.config.json,sage.baseline.jsontocora.baseline.json, and the.sage/folder to.cora/. In the config, change the@vispero/sage/configimport to@vispero/cora/config, and the$schemaaddress tocora-config’s, by hand:cora initkeeps an existing config’s content. - Move your user config, if you keep one, from the
sagefolder to thecorafolder beside it, such as~/.config/cora/config.jsonon Linux, and change its$schemaaddress tocora-user-config’s. - Accept the baseline again: every fingerprint changes, since the names fingerprints hash now say CORA. Scan with
cora scan, then accept its violations withcora baseline update. Thebaseline.outdatederror is gone with SAGE 0.1’s baselines: a baseline whose pages record no browser fails ascontract.invalid, naming each page. - Records and their
$schemaaddresses now name CORA, such ascora-results. Edit any you keep as above; a new project’scora initwrites CORA’s. A script that readscora info --jsonfinds the package undercora, and the cora-mcp release it runs undercora.coraMcp. - In Claude Code, remove
enabledPlugins["sage@vispero"]from the project’s.claude/settings.json, or runclaude plugin uninstall sage@visperowhere you installed it, and pointextraKnownMarketplaces.visperoatVispero/cora-plugins, sincecora initkeeps values it finds. Then runcora init, which enablescora@vispero. In Codex, runcodex plugin marketplace remove vispero, then addVispero/cora-pluginsandcora@vispero. The skills and agents are nowcora:…; rename any permission rule that namesmcp__sage__*ormcp__plugin_sage_sage__*tomcp__cora__*ormcp__plugin_cora_cora__*. - Sign in again with
cora auth login, and create a new CI token: the service is nowhttps://cora.vispero.com, and tokens from SAGE’s service no longer work.
The docs are at
https://cora.vispero.com/docs/. - Install
Patch Changes
Section titled “Patch Changes”-
Update
@babel/parser, which reads your JSX, TSX and Vite config files for@vispero/cora/viteandcora init, to 7.29.11, and the MCP SDK, which cora-mcp builds on, to 2.4.0. -
The CLI reference now says that
cora hookneeds an account in part: without one, it says why, as a warning, then only asks you before an edit of what decides what CORA reports. It said that the hook needs no account.In your editor’s hover over
cora.config.json,rules.overridesno longer names lint engines, which no release has, andpage.excludenames the crawl’sexclude, where it named acrawl.excludekey that does not exist.$schemasays to change the minor in a docs address when the cora you run moves to another, and names VS Code’s quick fix, Configure Trusted Domains. In the user config,browser.devtools,browser.slowMoandbrowser.keepOpennow say that they are for CORA’s own developers: only a run of CORA from its source takes them, and a release ignores them with a warning.The error reference now says that
cora scan --debugkeeps the whole output of the app’s server in the log file, forserver.exitedandserver.timed-out; that Yarn, unlike npm and pnpm, does not install theplaywright-corepeer dependency, forplaywright.not-installed; and that a longermcp.callTimeoutneeds the agent’s own tool timeout raised too, formcp.browser-timeout.cora-mcp’s README now links Setting up cora-mcp for each agent’s setup, and the MCP tools reference for its tools.
-
Update magic-string and unplugin, which
@vispero/cora/vite’s plugin uses to stamp your JSX with its source locations, to 1.4.5 and 3.5.0. -
On Windows, a cora or cora-mcp session stopped by a signal Node cannot raise there, such as closing the console after a Ctrl+C, now ends at once rather than failing with an internal error.
Minor Changes
Section titled “Minor Changes”-
SAGE’s fixer now leaves the issues your baseline accepts unchanged unless your request asks for them, and proposes a fix for each. A request asks for an accepted issue when it names, as what to fix, the issue’s element, the component or a file it is in, its rule, such as “fix the contrast issues on /pricing”, or the issues the baseline accepts. A request to fix a page, or what a scan finds, has it fix only the issues the baseline does not accept, and the UI files SAGE’s hooks name as changed only say where it scans. In a report-only run, an accepted issue is reported like any other.
Before it summarizes, the fixer scans again, as it loads, each page that has an accepted issue and renders a file it edited, and reports any accepted issue its edits made CI’s gate count as new, with the fix it would make, and any it could not check.
sage:a11y-reportpresents the accepted issues its edits made new first, as now counted as new, and the issues SAGE marks as accepted, and the fixer left, apart from the rest, as accepted by your baseline.The fixer now also starts its answer with each of SAGE’s summaries exactly as the tool returned it, so the report reads them as data, and SAGE’s hook tells a fixer it turns back to do the same. When SAGE’s hook asks the agent to dispatch the fixer after UI edits, the brief quotes what you asked about accessibility, if anything, and names the edited files apart from it, since those only say where the fixer scans.
-
sage-mcp now reads the project’s baseline,
sage.baseline.json, assage scanreads it, and marks each issue the baseline accepts withaccepted: true, so an agent sees the same issues accepted as CI’s gate:- on the issues of a
scan_urlresult, onexplain_finding’s issue, and on astill-failingverdict ofverify_fix, by the rescan’s listing of the issue; - on
summarize_fixes’ issues, by the latest scan that compared each with the baseline, so an issuescan_urlfound accepted stays marked afterscan_pagescans of the same page; - never on the issues of
scan_page, since no gate compares a page state an agent reached, and never without a baseline.
The tool results’ format stays at version 1, with
acceptedas a new optional field.scan_url,explain_findingandverify_fixread the baseline afresh on each call and, likesage scan, fail on one they cannot read, such as one a newersagewrote, withcontract.unsupported-version.Each remediation loop record in
.sage/loop/keeps the baseline’s verdict on its issue in a new optional field,baseline. An older sage-mcp working on the same project skips such a record rather than rewrite it, and warns of it. - on the issues of a
Patch Changes
Section titled “Patch Changes”-
sage --helplists each command by a one-line summary, and each command’s own help, such assage scan --help, now opens with a description of what the command does and ends with examples. The CLI reference gives each command the same description and examples, and says whether it needs an account. -
The config reference opens with an example
sage.config.tsthat sets every key but$schema, and says of each key whether it entersconfigHash, which the shards of one run must share. A key the config must hold reads Required: yes, andlog.levelin the user config reference shows its default for each program:warnforsageandinfofor sage-mcp. A route’spageandruleslist the keys they take, each linking the global key, in place of a section per route key.In your editor’s hover over
sage.config.ts, a key no longer says whether the config’s hash covers it, which the config reference now states, and a route’spagesays that itsexcludereplaces the global list rather than adding to it. -
An empty
.gitdirectory no longer marks a repository. SAGE takes a directory for a repository’s root when its.gitis a file, as in a worktree or a submodule, or a directory that holdsHEAD. A sandbox that mounts an empty.gitwhile it runs a command, as Codex’s does in each directory the command may write, no longer moves that root. The repository root is where source stamps from@vispero/sage/vitename files from, where a scan reads the files that place findings in your source, and wheresage hookstops looking for your project’s Claude Code agents. The warning@vispero/sage/viteprints when nothing carries source stamps now says that no directory is a git repository’s root, where it said that none holds.git. -
sage init --help, and the CLI reference, say howsage initlays out what it writes: it keeps each file’s own layout, and formats a new file with the project’s Prettier, or else as the nearestpackage.jsonis indented, or with two spaces. They also say that it leaves a file it cannot edit as it is, and names the docs page that shows the change to make by hand. -
The user config’s
mcp.callTimeoutsays where its default comes from: the request timeout of the MCP SDK’s client, which Codex keeps. It no longer says that most MCP clients wait that long, which Claude Code does not. -
Errors and coverage details no longer name sage’s internals. When a page answers a script sage runs in it in a shape sage does not expect,
scan.unsupported-pagenow says what sage was doing, such asThe page's answer when sage read its viewport is invalid:, where it named an internal result, such asThe kernel's in-page viewport result is invalid:. Results files carry that text as theunsupported-pagecoverage detail, and as the axe engine’sengine-errordetail. Aninput-unavailabledetail now names the input as users read it, such asthe live page, rather thanlivePage. A script that matched the old text of these details needs the new text. -
sage-mcp’s README adds the server to Claude Code and Codex with
npx --yes --prefer-offline @vispero/sage-mcp@<version>, the command SAGE’s plugin andsage mcprun, which starts a version from npm’s cache without asking the registry again. It also says what the docs’ “Adding sage-mcp to an agent” covers.
Minor Changes
Section titled “Minor Changes”-
sage scanstarts the app’s server before it scans and stops it once it has, from the config’s newserverkey, as Playwright’swebServerdoes, so a CI job needs no step that starts a server in the background and hopes it is ready:server.commandstarts the server, through the platform’s shell in the project root, andserver.urlis where it answers once it is ready.sage scanwaits until a request there gets any status below 404, scans, then ends the command and what it started, on Linux, macOS and Windows, whether the scan passes, fails or is stopped. As with Playwright’swebServer, a process that leaves the command’s process group, or on Windows one whose parent has already exited, keeps running.server.timeoutbounds the wait, 60000 ms by default. A server not ready within it fails with the new error codeserver.timed-out, naming the setting and its value, and one that exits first fails withserver.exited, naming its exit code or signal and the end of its error output, never the command.- A server already answering at the URL fails the scan with
server.already-running, unlessserver.reuseExistingServeris on, whensage scanscans it, starts nothing and leaves it running.server.timeoutandserver.reuseExistingServercan also come from your user config and fromSAGE_SERVER_TIMEOUTandSAGE_SERVER_REUSE_EXISTING_SERVER, so a machine where you keep the server running reuses it while CI does not. - The command runs with
SAGE_SOURCE_STAMPSset to1, so an app it builds with@vispero/sage/vitecarries source stamps, unlesssourceStampsisfalse. - How the app is served changes no finding, so the config’s hash leaves
serverout. sage-mcp reads the key and never starts the server. - A signal that stops
sage scan,SIGINT,SIGTERMorSIGHUP, stops the server it started at once, though a step of the scan has yet to finish. A second signal while the server stops kills its processes outright beforesageends by it.
An older release rejects a config that holds
server, as any unknown key. -
sage.config.jsontakes a new$schemakey naming the config’s JSON Schema, so an editor such as VS Code checks each key and value as you type and completes them:@vispero/sagenow ships the schema of every file it reads and writes, underschemas/in its package, such asschemas/sage-config/v1.json. Name your installed copy’s by its path from the config’s directory, such as./node_modules/@vispero/sage/schemas/sage-config/v1.json: it names the schema of whatever version you install, so it stays right as you upgrade, and the editor reads it from disk with nothing to download.sage initwrites$schemain everysage.config.jsonit creates. Where it finds sage installed, as Node finds it from the config’s directory, it writes the path to that copy, such as../node_modules/@vispero/sage/schemas/sage-config/v1.json, a monorepo’s hoisted root included, so install sage before you run it. Where it finds none, it writes the schema in the docs of its own minor,https://sage.vispero.com/docs/<minor>/schemas/sage-config/v1.json, such as0.4for any 0.4 release; VS Code downloads it once you trustsage.vispero.com, through the quick fix Configure Trusted Domains on its warning. Once you install sage, change$schemato the path yourself:sage initleaves a config already in the directory as it is, as it does one an earliersage initwrote.- Nothing in SAGE acts on
$schema. The sage that loads the config checks it by its own schema, the key is no setting, with no variable and no flag, and the config’s hash leaves it out, so a results file records the sameconfigHashwith it as without it. sage-mcp reads a config that holds it assage scandoes. - Your user config takes
$schematoo. No sage is installed beside it, so it names the docs’ copy,https://sage.vispero.com/docs/<minor>/schemas/sage-user-config/v1.json, which VS Code downloads once you trust the domain. Every release on your machine reads the file, so name the minor of the sage you run most. SAGE acts on nothing in it, and warns of nothing for it; one that is not a string is ignored with a warning, as any value the user config does not accept.
An older release rejects a project config that holds
$schema, as any unknown key, and ignores the key in your user config with a warning, as any key it does not know. -
The config’s new
page.excludeleaves elements out of a page’s scan, such as a third-party chat widget, an embedded player or a consent banner you do not own, for every page inpageand for the pages a route matches inroutes[].page:- Each entry is a CSS selector, matched in the page’s main document. No rule evaluates the element it matches or anything inside it: name an iframe to leave out the document it shows, or a shadow host to leave out its shadow tree. The rest of the page is scanned as before.
- A route’s
excludereplaces the global list on the pages it matches, rather than adding to it, soexclude: []scans those pages whole. - A page where a selector matches records each engine’s coverage as
partial, with the new coverage reasonexcluded-by-confignaming the selectors that matched, so a page your config leaves partly out reads apart from a scan that fell short. A page where none matches is scanned whole, and its coverage iscomplete. - The baseline counts a page that is partial only for
excluded-by-configas scanned completely. So a violation your baseline accepts on an element you start to exclude reads as fixed in the next comparison, andsage baseline updateremoves it, as it removes the accepted violations of a rule you turn off. sage-mcp’sverify_fixcounts such a page as covering the issue’s element too. - sage-mcp’s
scan_pageandscan_urlleave the same elements out, beside theexcludean agent passes, and their results’ coverage gives the reason. scan.invalid-scopehas new causes. Apage.excludeselector that is not valid CSS, or that matches the page’s root element, such ashtml, which would leave out the whole page, fails the scan, namingpage.excludeand the selector, on the first page where it is met; a selector that matches nothing on a page is allowed. To scan nothing on a route, turn its rules off in itsrules.overrides, or keep a crawl off it withcrawl.exclude. An agent’sincludethat matches an element on or inside one the config excludes failsscan_pageandscan_url, since an agent’s scope only narrows what the config scans, and so does an agent’sexcludethat matches the page’s root element, which left nothing out before.page.excludechanges what a scan finds, so the config’s hash covers it, and only the config file sets it. Results files record a differentconfigHashthan earlier releases did for the same config, since the hash now covers the key’s default,[].
An older release rejects a config that holds
page.exclude, as any unknown key. -
How SAGE waits for each page, and how long it gives it, are now settings of the project’s, set for every page in the config’s new
pagekey and for the pages whose route matches a pattern in its newrouteskey, so a site whose pages settle slowly or load past a minute can be scanned completely.page.settle.quietWindowandpage.settle.timeoutset how long a page must go without a change to the DOM before it counts as settled, 250 ms by default, and when SAGE gives up waiting and scans it as it is, 5000 ms by default, as before.page.waitForholds a scan, before the page settles, until an element matches a CSS selector or, insage.config.ts, until a function that receives Playwright’sPage, and asignalthat aborts once SAGE stops waiting, resolves. A page never ready, because the function throws or the selector is not valid CSS, is recorded not scanned with the new coverage reasonnot-ready, andscan_pagefails with the new error codepage.not-ready.page.timeoutbounds each visit of a page, 60000 ms by default, as before.sage scan --page-timeout,SAGE_PAGE_TIMEOUTandpage.timeoutin your user config replace the project’s value for one run or one machine; a route’s own value still wins on the pages it matches. sage-mcp’s scans stop at it, or atmcp.callTimeout, whichever comes first.page.retries, orSAGE_PAGE_RETRIES, visits a page again in a fresh tab when a visit fails to load it or runs out of time, 0 times by default. A retried target is recorded once, andsage scan’s progress line showsRetryingwhile it runs again. sage-mcp never retries a scan.routesentries each take apattern, a regular expression tested against a page’s URL without its origin, such as^/reports/, and apagewith any of the options above. Every matching entry applies, in order, its options winning overpage, and a later entry’s over an earlier one’s. They apply tosage scan’s targets, the pages a crawl finds and sage-mcp’sscan_urlandscan_page, and a page that redirects is waited for by the options of the page it lands on, while its visit keeps the requested page’spage.timeoutandpage.retries.- A page’s load is now bounded by its visit alone: Playwright’s default navigation timeout of 30 seconds no longer cuts off a load inside a longer visit, nor does a navigation timeout a
browser.setupgives the browser context. - A
timed-outreason’s detail, andmcp.browser-timeout’s message for a scan, now name the setting that ran out and its value, as inpage.timeout (60000 ms) passed while loading the page, where the detail readThe deadline passed while loading the page. - The settle options and
page.waitForchange what a scan finds, so the config’s hash covers them, and only the config file sets them.page.timeoutandpage.retrieschange only whether a slow page finishes, so the hash leaves them out. Results files record a differentconfigHashthan earlier releases did for the same config, since the hash now covers the page options’ defaults.
-
The report directory is now
report/in the project’s state directory, and a new setting moves the state directory.- Breaking:
sage scanin CI andsage report mergewrite the report directory to.sage/report/, where they wrotesage-report/in the working directory. Workflows written for 0.1 and 0.2 readsage-report/: change their paths to.sage/report/, such as code scanning’ssarif_file: .sage/report/results.sarifand Azure Pipelines’publish: .sage/report, or keep the old place with--report sage-report..sage/is hidden, soactions/upload-artifactuploads the report only withinclude-hidden-files: true, as the CI guide’s workflow does.--report <dir>still writes where it names, relative to the working directory. stateDirectory, in your user config orSAGE_STATE_DIRECTORY, moves.sage/: a path relative to the project root, or an absolute one, which names the directory too. It is never the project root or a directory that holds it, whose.gitignorewould hide the project’s files:SAGE_STATE_DIRECTORYset so fails withconfig.invalid-environment, and a user config’s value so is ignored with a warning, and.sage/applies. Everything.sage/holds moves with it: run files, log and crash files, sage-mcp’s browser files and remediation-loop records, and the default report directory. It ignores itself in git wherever it is. The baseline, your credentials, and any file or directory--outor--reportnames stay where they are. Outside a project, SAGE keeps its state in the per-user state directory, as before, whatever the setting says. sage-mcp reads the setting as it starts. A release that does not know the setting ignores it with a warning and keeps its state in.sage/.
- Breaking:
-
Which rules count is now a selection the config states the way people state conformance, for every page or for the pages whose route matches a pattern, and a rule can be shown without failing the gate.
- Breaking: the config’s
rulesis now an object ofstandard,bestPracticesandoverrides, and no longer a record of rule IDs. Move each'axe/<rule>': 'on'or'off'intooverrides:rules: { 'axe/region': 'off' }becomesrules: { overrides: { 'axe/region': 'off' } }. A config in the old form fails withconfig.invalid, namingrules. rules.standard,'WCAG 2.2 AA'by default, runs the rules of SAGE’s default set at its WCAG level and below, in its version and earlier, from'WCAG 2.0 A'to'WCAG 2.2 AAA', andrules.bestPractices,trueby default, runs its best practices. A config that sets neither runs the rules it ran before.rules.standardalso takes'Section 508', which runs WCAG 2.0 AA’s rules,'EN 301 549 V3.2.1', WCAG 2.1 AA’s, and'EN 301 549 V4.1.1', WCAG 2.2 AA’s; each covers only the WCAG part of its standard. A rule outside the default set, such as one axe-core calls experimental or deprecated, runs only where an override names it by its ID.rules.overridesset rules by a selector: a rule ID such asaxe/region, a WCAG success criterion such as1.4.6, a category such asforms, or an engine’s rules such asaxe/*orai/*, the most specific key winning, in that order. A criterion, a category or an engine’s rules reach only the rules in the default set, soaxe/*: 'on'turns on no experimental or deprecated rule, and a key that names only rules outside the default set, such as'1.3.4', runs none: SAGE logs a warning,rule key reaches no rule, naming the IDs to set instead. Each takeson,offorreport. Areportrule runs, and its findings are listed in a section of their own in the terminal summary,summary.md, the SARIF log, as notes taggedreport-only, and GitHub Actions annotations, as notices; they never fail the gate and are never baselined. An issue that gates and also holds findings of a rule reported on another page, such as one component on two routes, takes its severity, its SARIF level and its annotation’s level from its gated findings alone, and its reported findings are listed with the report-only issues. Two criteria that reach one rule with different settings, and a key that names no rule, fail withconfig.invalid.- Each entry of
routestakesrulestoo, applied after the global selection on the pages it matches, deciding the rules its own keys reach. sage scan --onlyand--skipnarrow a run’s rules with the same selectors, comma-separated and repeatable, and never run a rule the config leaves off; sage-mcp’sscan_urlandscan_pagetake the same selectors asrules, andlist_rulestakes them as a filter and gives each rule’scategory, itssetting,onorreport, anddecidedBy, the config key that decided it, such asrules.standard; given a page’surl,list_ruleslists the rules its route runs there. An issue of areportrule carriesreport: trueinscan_url’s,scan_page’s,explain_finding’s,verify_fix‘s andsummarize_fixes’ results, so an agent can tell which issues gate CI. sage-mcp’s tool formats change version 1 in place.sage baseline updatenow reads the config, and drops the accepted violations of a rule the config does not run on their page’s route; areportrule’s accepted violations stay as they are, and the report never counts them fixed. A compared results file lists them in a new optionalbaseline.reported.- Results files record a rule a selection reported on each target’s coverage, in a new optional
reportlist, whichsage report mergereads. Results files record a differentconfigHashthan earlier releases did for the same config, since the hash now covers the rule selection’s defaults, and a run narrowed by--only,--skipor sage-mcp’srulesrecords one of its own. - The rules reference gives each rule’s WCAG level, its category, and whether it is in the default set and runs by default. A release that adds a rule to the default set or removes one says so in its changelog.
- Breaking: the config’s
-
SAGE’s plugin has a new skill,
sage:a11y-check-ui-edits, which has a coding agent’s UI edits checked where SAGE’s hooks do not ask for the fixer, such as in Codex, under Claude Code’sdisableAllHookssetting, or in a checkout withoutnode_modules. The coding agents guide describes it.- Once the agent has edited your app’s UI files, such as components, templates, styles or markup, in a project whose
package.jsonlists@vispero/sage, it dispatches SAGE’s fixer before it finishes, to scan the pages that render them and fix what it finds. With no fixer to dispatch, as in Codex, it checks those pages itself with SAGE’s tools. Where SAGE’s hooks ask too, the agent dispatches the fixer once. - It does nothing while you have paused SAGE, as the pause and resume skills now tell the agent, so in Codex,
$sage:pauseand$sage:resumepause and resume this check. - The agent loads it on its own, and Claude Code keeps it out of your
/menu.
The other skills change too:
- The fixer lists a page that did not settle before it was scanned, or before
verify_fixrescanned it, among the pages it scanned, marked as unsettled, andsage:a11y-reportpresents such a page as one where SAGE may have missed issues. sage:a11y-regression-testsis for an agent that verified a fix withverify_fixitself, or that you ask for regression tests, so the main agent writes no second test for a fix the fixer already pinned.
- Once the agent has edited your app’s UI files, such as components, templates, styles or markup, in a project whose
-
SAGE’s fixer runs in Codex too, SAGE’s plugin keeps itself up to date, and a new
/sage:fixskill has the fixer carry out your request. The coding agents guide describes each.sage init, in a project that uses Codex, declares SAGE’s fixer agent for Codex,a11y_fixer, in the project’s.codex/config.toml, as[agents.a11y_fixer]with aconfig_filethat points at the agent file in the project’s@vispero/sage, so the fixer moves with each upgrade, since Codex’s plugins carry no agents. It appends the declaration to a config you have, and leaves a config that already declares an agent of that name, or a.codex/agents/that holds one, as it is. In a project without apackage.json, it writes a copy of the agent file into.codex/agents/a11y-fixer.tomlinstead. It writes no Codex MCP entry or hooks: the plugin carries them, andsage initprints the commands that install it. The fixer names no model, so it runs on your session’s model, or onagents.default_subagent_modelfrom your Codex config, at high reasoning effort, and it closes its browser before it returns, since Codex gives each agent a sage-mcp of its own.sage initsetsautoUpdateon SAGE’s marketplace in the project’s.claude/settings.json, where the entry lacks it, so Claude Code updates the plugin in the background after it starts. Codex upgrades a marketplace added from a Git repository each time it starts, with no setting.- In Claude Code, the hooks’ ask at a stop, to dispatch the fixer after UI edits or to keep the fixer on the issues it attempted, now shows as hook feedback rather than as a hook error. The hooks’ instructions have the agent hand the fixer a brief that stands alone: your request as you wrote it, what it refers to, and anything you decided or ruled out, such as report only or no edits.
- In Codex, the plugin’s hooks now route SAGE’s tools to the fixer, ask the main agent once a turn to dispatch it after UI edits, keep it on the issues it attempted, and act on
$sage:pauseand$sage:resume, as they do in Claude Code. They route and ask only where the project’s.codex/, or your Codex home (CODEX_HOME, or~/.codex), gives Codex an agent nameda11y_fixer; elsewhere they leave SAGE’s tools to the agent that calls them, and tell you once a session to runnpx sage init --agent codex. As the fixer starts,sage hook subagent-starthands it SAGE’s procedure and regression-test skills from the project’s@vispero/sage, since Codex preloads no skill into an agent, andsage hook pre-tool-useturns back a spawn of the fixer that would hand it your conversation’s history, telling the agent to spawn it withfork_turnsset to"none"and your request. - The plugin’s
sage:a11y-check-ui-editsskill now loads for any request that creates or changes UI, such as pages, components, forms, layouts, templates, styles or markup, and has an agent that cannot dispatch the fixer, such as a subagent, name the files it changed instead. UI edits made while SAGE was paused stay unchecked after you resume it, unless you ask for a check. The fixer reports the fixes it left without a regression test, such as where your project has no test framework. - The new skill
sage:fix, which only you invoke, as/sage:fixin Claude Code or$sage:fixin Codex, has the main agent dispatch the fixer with the text you type after it, such as a page, a component or report only, and present its result.
-
SAGE’s plugin for Claude Code and Codex asks you before an agent edits what decides what SAGE reports: a
sage.baseline.json, asage.config.jsonorsage.config.ts, your user config, or a comment that turns accessibility checks off or down, such aseslint-disable-next-line jsx-a11y/alt-text, a bareeslint-disablein a UI file,/* eslint jsx-a11y/alt-text: off */, or a Sveltesvelte-ignore a11y_…. A comment the edit only keeps, such as on a line it rewrites, or narrows, such asoffturned down towarn, asks nothing, while one it widens, such as aneslint-disable-next-lineturned into aneslint-disable, asks; text inside a string is no comment. The reason names the files or rules and what they control. ItsPreToolUsehook on each harness’s edit tools, Claude Code’sEditandWriteand Codex’sapply_patch, runssage hook pre-tool-usein both harnesses, whether or not the plugin carries the harness’s fixer agent.- Claude Code asks you in every permission mode; in a headless run it refuses the edit and gives the agent the reason.
- Codex cannot ask from a hook, so it shows the reason as a warning, and the edit goes ahead.
- The hook decides from the edit alone, so it asks without an account, with a config that cannot load, and when its log file cannot be created. It never refuses an edit itself, and an edit through a shell command reaches no hook.
-
sage initsets SAGE up in a project, and shows every change before it makes it:- It finds the project’s package manager, its frameworks, build tools and test frameworks, and the coding agents it uses: Claude Code and Codex, from their files in the project, their CLIs on
PATHand the agent runningsage init.--agentnames the agents to set up instead. - Beside the starter config, it registers SAGE’s Vite plugin,
@vispero/sage/vite, in a Vite project’s config, and adds SAGE’s plugin marketplace and plugin to a Claude Code project’s.claude/settings.json, keeping each file’s layout and comments. - It shows each change as a diff and asks once before writing. In CI, inside a coding agent, without a terminal, or with the new
--yes(-y), it writes without asking. Declining writes nothing and exits 0. - New files follow the project’s Prettier config or
.editorconfig, or else itspackage.json’s indentation, or else Prettier’s defaults of two spaces, so a two-space project gets a two-space config. - A file it cannot edit safely, such as a Vite config whose
pluginsspreads another list, is left as it is with the reason and the docs page that shows the change to make by hand, and the other changes still apply. - The next steps name each agent’s command to install SAGE’s plugin, and
sage auth loginwhen no account is signed in on the machine.
Breaking: re-running
sage initin a directory that already has a config no longer fails withinit.config-exists, which is gone: it keeps the config as it is and sets up only what is missing. A file that changes between the diff and the write fails with the newinit.file-changed, and nothing is written.sage infolistsPATHandPATHEXTamong the variables it reports as set or unset, andsage status --subagent-status-linecuts the fixer’s row to the columns a terminal draws, so a row holding CJK text or emoji no longer overflows it. - It finds the project’s package manager, its frameworks, build tools and test frameworks, and the coding agents it uses: Claude Code and Codex, from their files in the project, their CLIs on
-
SAGE’s plugin for Claude Code and Codex has a pause skill and a resume skill that only you can invoke:
/sage:pauseand/sage:resumein Claude Code,$sage:pauseand$sage:resumein Codex. The pause stops SAGE’s hooks, until you resume or the session ends, from asking the agent to dispatch SAGE’s fixer after UI edits and from keeping the fixer going, while the hooks that ask you before an edit of what decides what SAGE reports still ask, and SAGE’s tools still run in the fixer. The resume has the hooks ask again, for UI edits made from then on. The hook confirms each with a warning, and a new session starts unpaused.sage hookanswers a new event,user-prompt, the person’s prompt. The plugin binds it, once it carries the harness’s fixer agent, to Claude Code’sUserPromptExpansionof the pause and resume skills alone, and to Codex’sUserPromptSubmit, which runs on every prompt.- The session’s record in
.sage/hooks/,sage-hook-stateversion 1, holds an optionalpaused: true.
-
SAGE’s plugin carries SAGE’s fixer agent for Claude Code,
sage:a11y-fixer, and three skills that Claude Code and Codex load, so a coding agent’s accessibility scans and fixes run in the fixer, out of the main conversation. The coding agents guide describes each.- The main agent dispatches the fixer when you ask to scan, check, audit or fix accessibility. The fixer follows SAGE’s remediation procedure, the skill
sage:a11y-remediation-loop, which it preloads: it scans the pages, fixes what the page already settles and verifies each fix in a browser withverify_fix, proposes what needs a person’s judgment, such as alt text or a color, pins each verified fix with a regression test, and returns SAGE’s summary. Asked only what is wrong, it reports and changes nothing; asked something no scan is needed for, such as which rules SAGE runs, it answers and changes nothing. When a page’s server does not answer, it starts the project’s dev script and stops it before it returns. It undoes its own edits by editing them back, never with a git command that discards your uncommitted work. It runs on Sonnet at high effort, stops after 300 turns, and its tools are reading, finding and editing files, the shell, and sage-mcp’s tools. sage:a11y-reporttells an agent how to present the fixer’s result: a headline count, the issues awaiting your decision first, the links, “fixed and verified in a browser”, and never “compliant”.sage:a11y-regression-teststells an agent how to pin a verified fix in the project’s own test framework, such as role and accessible-name assertions in Playwright Test or Testing Library queries in Vitest or Jest, or to propose a test where the project has none.- In Claude Code, the plugin’s hooks route SAGE’s tools to the fixer, ask the main agent once a turn to dispatch it after UI edits, hand it the edited files as it starts, keep it on the issues it attempted, and act on
/sage:pauseand/sage:resume. The plugin carries no fixer agent for Codex, so in Codex its hooks act on protected edits alone. - The plugin’s Claude Code entry loads sage-mcp’s tools as each session starts, with
"alwaysLoad": true, rather than behind Claude Code’s tool search. - In Claude Code, the fixer’s row in the panel of running agents shows its progress: its issues by where each stands, and the one it is working on with its latest verdict. The plugin sets Claude Code’s
subagentStatusLineto the project’ssage status --subagent-status-line claude, a new option that writes the fixer’s rows and leaves every other agent’s row to Claude Code. AsubagentStatusLineof your own replaces the plugin’s. - The plugin’s skills that adapt accessibility-agents carry its notice beside them, in
dist/harness/skills/NOTICE.md, andThirdPartyNotices.txtcarries it too.
- The main agent dispatches the fixer when you ask to scan, check, audit or fix accessibility. The fixer follows SAGE’s remediation procedure, the skill
-
sage init --project-agentsets SAGE’s fixer up in Claude Code as a project agent with a sage-mcp of its own, so that only the fixer has SAGE’s tools. The coding agents guide describes it.- It writes
.claude/agents/sage-a11y-fixer.md, SAGE’s fixer with sage-mcp defined inline, which Claude Code starts when the agent starts and stops when it finishes, in a folder you trusted. It sets Claude Code up with SAGE’s plugin too, whose skills, hooks and status line the agent uses. - It turns the plugin’s sage-mcp and the plugin’s fixer off for the project, in
.claude/settings.json, withdeniedMcpServers: [{ "serverName": "plugin:sage:sage" }]andpermissions.deny: ["Agent(sage:a11y-fixer)"], each where the file lacks it. - It marks the agent file as SAGE’s copy, and a later
sage initreplaces a copy nobody has changed with its own release’s, so the file keeps up as you upgrade@vispero/sage. It leaves a file you edited as it is. Its copy of Codex’s fixer agent, in a project without apackage.json, is marked and refreshed the same way. - In a project that holds the agent, the plugin’s hooks tell the main agent to dispatch
sage-a11y-fixer, and the hooks and the fixer’s status row take it for SAGE’s fixer, as they takesage:a11y-fixer. The plugin’ssage:fixandsage:a11y-check-ui-editsskills dispatch it when it is among the agents the main agent can dispatch. - SAGE’s fixer agents for Claude Code, the plugin’s and the project’s, ask the main agent to name no model when it dispatches them, unless you name one, so the fixer runs on Sonnet as its file sets.
- SAGE’s plugin for Claude Code and Codex now installs from SAGE’s plugin marketplace, the private repository
Vispero/sage-plugins: each release publishes the plugin there at its own version, once npm serves the release. In a projectsage initset up, trusting the folder in Claude Code loads the plugin with no install command, andsage init’s next steps give the two commands that install it ahead of a session. Sign in to GitHub withgh auth login, thengh auth setup-git, to reach the marketplace.
- It writes
-
sage statusshows whether your SAGE account is valid, the project’s newest scans, and where each issue SAGE’s fixer works on stands: in progress, not attempted, escalated to a person, or done, with its attempts and the newest verdict ofverify_fix. It checks the account without renewing it, reads.sage/without changing anything in it, and exits 0 with or without an account or a.sage/directory.sage status --jsonprints the same as one JSON object in a new versioned format,sage-status, for scripts and coding agents’ status lines. -
How long
sageand sage-mcp wait on each request to the SAGE service is now a setting of your own, for a network whose proxy or VPN is slow.service.requestTimeoutin your user config, orSAGE_SERVICE_REQUEST_TIMEOUT, sets it in milliseconds; it is 30 seconds by default, as before. Renewing a signed-in credential waits up to twice as long, as before. A request that reaches it fails withservice.unreachable, whose message now names the setting and its value, as init did not answer within service.requestTimeout (30000 ms).sage auth statusprints the service it checked your account with, as its first line:service: https://sage.vispero.com.sage inforeports the servicesagetalks to, the setting that chose it and the request timeout, in aServicesection and asservicein its--jsonoutput.account.missingnames the service it found no credential for, as inNo account credential for https://sage.vispero.com: SAGE_TOKEN is unset and there is no credentials file.- A new error,
config.development-only, refuses a setting that only a run of SAGE from its source takes, which SAGE’s own developers use, when a variable sets it; a release ignores such a setting in your user config, with a warning, rather than failing.
-
Pages are waited for longer and more completely before they are scanned, so a page that renders on a timer, fades in or loads its content after a request is scanned as it ends up, and the same page gives the same findings from one run to the next. These defaults change what some pages’ scans find, and change every run’s
configHash.-
The quiet window is 500 ms by default, where it was 250 ms. A page’s documents must go
page.settle.quietWindowwithout a change to the DOM before it counts as settled, so content a page renders after a debounce of up to a few hundred milliseconds, such as code blocks that make themselves reachable by keyboard after a 250 ms timer, is scanned with it. A config that setsquietWindowkeeps its value. -
Settling sees changes inside open shadow roots. A web component that renders into its shadow root after load, including one that attaches its root after settling starts, now keeps the page from counting as settled until it stops changing, so its content is scanned. A closed shadow root stays out of reach.
-
Settling waits for animations that end. After the quiet window, a page is held until its finite animations, such as a fade or a transition, have ended, its open shadow roots’ included, so colors are never sampled mid-fade. An infinite animation, such as a spinner, a paused one, a scroll-driven one and one that would end past
page.settle.timeoutnever hold it. -
Settling waits for the page’s requests. A page is held until its requests for documents, stylesheets, scripts, fonts, images and the data its scripts fetch have gone the quiet window with none in flight, so content that arrives after a request is scanned. Event streams, WebSockets and media never hold it, and the new
page.settle.requestTimeout, 3000 ms by default, set globally and per route, stops it waiting for one request open longer, such as a long poll. sage-mcp’sscan_pagewaits for the requests that start after the scan begins. -
A page that did not settle is recorded and reported. When
page.settle.timeoutpasses before a page settles, the page is still scanned as it is, and now its target’s page coverage in the results file holdsunsettled, with the timeout that passed, so a report can tell which findings come from a page still changing. This adds an optional field to version 1 of the results format. The readers name such targets too:sage scan’s andsage report merge’s summaries, whose first line counts such targets, as inScanned 11 targets (2 did not settle) in 14.2 s;- the report directory’s
summary.md, in a section,Targets scanned before their page settled, with each target’s settle timeout; - its
results.sarif, in a note of the run’s invocation for each such target; - the GitHub Actions annotations, in a notice after the verdict’s naming them;
- sage-mcp’s
scan_pageandscan_urlresults, whosescannedtarget carriesunsettled, andverify_fix‘s, which carries it when its rescan’s page did not settle, since a verdict on a page still changing may be wrong; each an optional field of version 1 of the tools’ results.
Each appears only when some target did not settle. The
page did not settlewarning names what settling still waited on inwaitingOn.
-
-
sageand sage-mcp now follow one rule for what they take from the machine they run on: a choice you made outside SAGE, about your machine, network, trust or diagnostics, is honored, and whatever would change what a scan finds or reports, or stop it working, is fixed, with a SAGE setting where you need to vary it. The environment variable reference now gives each variable’s kind under the rule. The first two changes below change findings and output you may rely on, as 0.x releases may:- Pages load in
en-USandUTC, whatever the machine’s locale and time zone. Chrome took both from your machine before, so a page that formats a date or picks its language by locale could give one machine’s scan findings another’s lacks. The newbrowser.localeandbrowser.timezonekeys set others, such asfr-FRandEurope/Paris, to scan the page visitors in another locale see; a tag or a time zone SAGE does not know, or an offset such as+01:00, fails withconfig.invalid. Both are in the config’s hash, so every config’sconfigHashchanges: shards scanned by an earlier release form a shard set of their own when merged with this release’s. A worker a page starts still readsnavigator.languagefrom the machine. sage-mcp’s browser keeps thebrowseroptions it opened with untilbrowser_close, as before, and its scans now record the hash of the options they ran under rather than the edited config’s. See Locale and time zone. - Your user config’s
colorwins overNO_COLORandFORCE_COLOR."color": falsekeeps thesagecommands’ stderr plain even underFORCE_COLOR=1, and"color": truecolors it underNO_COLOR, as no-color.org asks of a tool’s own config. Withcolorleft out,NO_COLOR,FORCE_COLOR, Node.js’sNODE_DISABLE_COLORSand the terminal decide, as before. - jiti’s diagnostics apply as
sage.config.tsloads.JITI_DEBUG=1prints how jiti loads the config, andJITI_SOURCE_MAPS=1gives jiti’s code for it source maps, which errors’ stacks follow under Node.js’s--enable-source-maps. jiti’s other variables still change nothing. - Three more of Playwright’s variables are ignored, each of which would change how Playwright’s actions or pages behave:
PLAYWRIGHT_SKIP_NAVIGATION_CHECK,PW_CHROMIUM_ATTACH_TO_OTHER, andPWTEST_UNDER_TESTwith npm’s forms of it. They still apply wherever else you set them, your own Playwright tests included. - A warning when certificate checking is off. While
NODE_TLS_REJECT_UNAUTHORIZED=0turns off Node.js’s certificate check,sageand sage-mcp logcertificate checking offatwarnas each run starts, on stderr, in sage-mcp’s client’s log and in the plugin’s hooks’ log file, where Node.js’s own warning may never show. sage inforeports more of what changes a run. It reports whetherNODE_OPTIONSis set, never its value, andNODE_DEBUG,PWDEBUGIMPL,JITI_DEBUG,JITI_SOURCE_MAPSandNODE_DISABLE_COLORS. It finds Playwright’s download directory as Playwright does: through npm’s forms ofPLAYWRIGHT_BROWSERS_PATH, such as aplaywright_browsers_pathin.npmrc, and with a relative path resolved against the directory npm or pnpm ran its script from.
- Pages load in
Patch Changes
Section titled “Patch Changes”-
The editor hover for
@vispero/sage/configand@vispero/sage/vitenow describes each function’s parameters, what it returns and the errors it throws, by their codes, and links the types it names. The conformance standardsrules.standardtakes read as plain values, such asWCAG 2.2 AA, as you write them insage.config.json. -
The config reference now describes each key with the doc comment your editor shows when you hover the key in
sage.config.ts, so the two always say the same.rulesgains the link to the rules reference that only its hover gave, and a few keys read as their hover did, such aspolicy.failOn, which lists its severities ascritical,serious,moderateorminor.Each key’s default now appears once in each place: on its Default line in the reference, and in the hover as
@defaultValue, no longer also in the hover’s prose. The hover writes a value bare, such asminororWCAG 2.2 AA, as the reference does, where it wrote a TypeScript literal, such as'minor'. -
The scripts SAGE runs in a page now carry names, so a browser’s devtools list each by its name rather than as an anonymous script, and you can tell SAGE’s work from the page’s own when you inspect a page in sage-mcp’s browser. Each is named
sage://sage/<package>/<module>/in-page.js, such assage://sage/core/kernel/in-page.js, so Chrome’s Sources panel lists them together in one group,sage. -
Fetching the
@vispero/sage-mcprelease is bounded insage init, and makes one request fewer:sage initwaits at most 10 minutes for the release it fetches for coding agents, where it waited as long as npm took. Past that, it ends npm and, as when the fetch fails, warns that agents will fetch the release on their first start, and exits 0 with the config it wrote.sage init,sage mcpand sage-mcp’s launcher no longer have npm send the registry an audit of the release’s dependencies when they fetch it, which npx awaited before running the release.
-
sage initstops onSIGINT,SIGTERMandSIGHUP, writing no file from then on and ending by the signal. A signal while it asks cancels the question.sage initandsage mcpstop on the same signals while they fetch the@vispero/sage-mcprelease: each ends npx and what npx started, waits for them, and ends by the signal, where aSIGTERMorSIGHUPsent tosagealone, such as a CI job’s, left npx running until npm’s own timeout.sage mcpstill forwardsSIGINTandSIGTERMto the release once it has handed over, and ends by the signal once the release has ended. A firstSIGHUPsent tosage mcpalone during the hand-over no longer ends it while the release goes on serving: it waits for the release, and a secondSIGHUPends it.sage-mcp’s launcher stops on the same signals while it fetches the project’s release, ending npx and then itself by the signal. During its hand-over to that release, a first
SIGHUPsent to the launcher alone no longer ends it while the release goes on serving: the launcher waits for the release, as it does for theSIGINTandSIGTERMit forwards, and a secondSIGHUPends it. -
sage-mcp’s instructions now send an agent to SAGE’s fixer, and
summarize_fixesdescribes every entry the fixer passes.- The instructions tell an agent that can dispatch SAGE’s fixer agent,
sage:a11y-fixerin Claude Code, to dispatch it for every request that needs SAGE’s tools, questions about its rules and issues included, with the request’s scope, and to present the fixer’s summary with SAGE’ssage:a11y-reportskill without rescanning. An agent without the fixer uses SAGE’s tools directly, by rules the instructions now give: this server’s own browser tools, explaining an issue and reading its rule’s fix guidance before an edit, verifying only an issue it changed, and callingsummarize_fixeslast. They say that neither a scan without issues nor a fixverify_fixfound fixed shows that a page conforms to WCAG, define an issue and its remediation loop, and name the pause,/sage:pausein Claude Code, which only you can invoke. summarize_fixesasks for an entry infixesfor each issue the agent changed, proposed a change for or left for you. The description of an entry’schangenames the marker each kind of entry starts with,Applied as decided:,Proposed:orNot changed:, and the 2048 characters past which the summary cuts it.- SAGE’s pause skill tells the agent that, while SAGE is paused, a person’s request for a scan or a fix is still carried out by dispatching SAGE’s fixer, since SAGE’s hooks still send its tools there.
- The instructions tell an agent that can dispatch SAGE’s fixer agent,
-
On Windows, when
taskkillcannot end the npx that fetches a sage-mcp release,sage mcp,sage initand sage-mcp’s launcher log a warning,npx not ended, with the release and why. The process still ends npx when it exits. -
A file SAGE rewrites, such as
sage.baseline.json, keeps its permissions, so a file you made private or executable stays so, and its owner and group where the system lets SAGE set them. A file reached through a symbolic link is rewritten where the link points, and the link stays. -
SAGE’s plugin hooks cost less on each tool call:
sage hookstarts faster: checking an account token signed by a key the release holds no longer loads the HTTP client or the OAuth library, and the hook loads none of SAGE’s fix guidance.- Without a valid account, the hooks say why once a session, as a warning, rather than on every tool call, and do nothing else for the rest of the session.
- In Codex, which runs the hook on every prompt, a prompt that does not invoke
$sage:pauseis answered at once, without a log file. - When the release does not hold the key an account token was signed with,
sage hookandsage statuswait at most 3 seconds for the service’s current keys, rather than the service’s request timeout, even over a connection whose TLS handshake stalls, then report the token asaccount.invalid, with the hint to update sage or connect to the network.
-
sage hookanswers five more events,post-tool-use,stop,subagent-start,subagent-stopandsession-end, which start SAGE’s fixer agent,sage:a11y-fixerin Claude Code anda11y_fixerin Codex, once an agent edits the app’s UI, and keep it working until its fixes are done or handed to a person:post-tool-userecords each UI file, such as a.tsx,.vue,.htmlor.cssfile, that an agent other than the fixer edits in the project, in.sage/hooks/<session>.json, one file per agent session.stopasks the main agent, at most once a turn, to dispatch the fixer to scan the pages that render those files and fix what it finds.subagent-starthands those files to the fixer as it starts, andsubagent-stopasks the fixer to go on while an issue it worked still needs another attempt, until each is done or escalated after three attempts, then to report withsummarize_fixes.session-endremoves the session’s file.
Without a valid account, each event answers as
pre-tool-usedoes, with onesystemMessagethat says why, and reads and writes nothing. -
An interrupted
sage scannow ends by the signal that interrupted it and writes no results, and an interrupted sage-mcp no longer reports the call it cut short as an internal error.- Changed exit status:
SIGTERMorSIGHUPduring a scan, as a cancelled CI job or a closed terminal sends, ended it with exit code 2 anderror[internal.unexpected], and left a crash file that asked you to report it. The scan now stops, writes no results file or report, leaves no crash file, and ends by the signal, which a shell reports as 143 forSIGTERMand 129 forSIGHUP. - Ctrl+C ends a scan with 130 every time, as it mostly did, where it sometimes ended it with exit code 2 and a crash file instead.
- Ctrl+C sent to sage-mcp while a tool call runs, such as a scan, answered the call with
internal.unexpectedand left a crash file. The server now answers the call as stopped, withsage-mcp stopped serving before the call finished, as it already did onSIGTERMandSIGHUP, leaves no crash file, and ends with 130.
- Changed exit status:
-
summary.md, and the GitHub Actions job summarysage scanandsage report mergeappend it to, now name the source position of each issue on an element of an app built with source stamps, asfile:line:column, which an editor or a terminal opens at the exact place, before its page, in the Where column, such assrc/routes/orders/[id].tsx:42:9onhttp://localhost:4173/orders/1001, as its annotation and its code scanning alert already locate it. An issue without a source location is listed by its page alone, as before. -
sageand sage-mcp now trust the operating system’s certificate store, as well as Node.js’s own root certificates, for their requests to the SAGE service, so signing in and checking your account work behind a TLS-inspecting proxy, such as Cloudflare WARP or Zscaler, whose root certificate your organization has installed on the machine.- They read the store where the operating system keeps it: the certificate stores on Windows, the keychains on macOS, and OpenSSL’s certificate file and directory on Linux, or those
SSL_CERT_FILEandSSL_CERT_DIRname. A root certificate in a fileNODE_EXTRA_CA_CERTSnames is still trusted too.sage inforeports whetherNODE_EXTRA_CA_CERTS,SSL_CERT_FILE,SSL_CERT_DIRandNODE_TLS_REJECT_UNAUTHORIZEDare set, and SAGE’s plugin passesNODE_EXTRA_CA_CERTSandNODE_TLS_REJECT_UNAUTHORIZEDon to sage-mcp, under Codex too. Node.js 23.0 to 23.9 cannot read the store, so there they trust only what Node.js trusts by itself. - When the machine does not trust the certificate the connection presents,
service.unreachablesays so and gives the TLS error’s code, such asSELF_SIGNED_CERT_IN_CHAIN, and its hint names the fixes: install the proxy’s root certificate in the operating system’s store, or name a PEM file that holds it inNODE_EXTRA_CA_CERTS. See Behind a TLS-inspecting proxy. - An https proxy named by an IP address, such as
https://10.0.0.5:8443, now works on Node.js 25 and later, which refuse an IP address as a TLS server name.
- They read the store where the operating system keeps it: the certificate stores on Windows, the keychains on macOS, and OpenSSL’s certificate file and directory on Linux, or those
-
A text SAGE cuts to fit a bound, such as an unscanned frame’s address in a run file, a SARIF rule’s description or a long text in a sage-mcp tool’s result, is cut between the characters a person reads as one, so an emoji of several code points is never left in pieces before the
…. -
sage-mcp’s instructions open with the routing to SAGE’s fixer, now naming Codex’s,
a11y_fixer, beside Claude Code’s: the fixer calls SAGE’s tools itself, Codex’s is spawned withfork_turnsset to"none", the agent hands the fixer the person’s request with what it refers to and its scope, such as report only or no edits, any other subagent that cannot dispatch the fixer names the files to check in its answer, and only where there is no fixer does an agent call SAGE’s tools directly. They name the pause in both harnesses,/sage:pausein Claude Code and$sage:pausein Codex. For a service other than production’s, they name it in a shorter paragraph.verify_fix’s description now says to act on a verdict that carriesunsettledand treat the page as unsettled, as SAGE’s fixer does, rather than to verify again, which would spend an attempt.Every SAGE tool now states
openWorldHint:scan_url, which loads any URL the agent names, reaches an open world, and the other tools act only on sage-mcp’s browser, the project and its state. So Codex runsverify_fixwithout asking you, as it already ran the read-only tools. The MCP tools reference lists each tool’s annotations.The hint of
account.missingsays that an MCP client that passes a server only the variables its entry names, such as Codex, needsSAGE_TOKEN, andXDG_CONFIG_HOMEwhere you set it, named in the entry. -
sage-mcp’s instructions name SAGE’s project agent for Claude Code,
sage-a11y-fixer, whichsage init --project-agentwrites, beside the plugin’ssage:a11y-fixer, as SAGE’s fixer, which calls SAGE’s tools itself. -
verify_fixnow judges a fix only when the element whose accessibility entry it read is the very element it compares the rescan’s findings with. A page that moves the followed element’s selector to another element whileverify_fixreads its entry, and back before it compares the findings, now getsinconclusive, whereverify_fixcould judge the fix by the other element’s entry and answerfixedorsuppressed. The screenshot an issue’s loop record keeps of its element is of the element whose entry it read too.
Minor Changes
Section titled “Minor Changes”-
sage-mcp has a new tool,
get_fix_guidance, which gives how to fix what a rule finds, in any framework, for every rule sage runs: the rule in plain language, who its failures affect, each situation it fails in with its fix and an example in HTML, and the fixes that silence the rule or look right but still fail people. It takes a rule ID, aslist_rulesand a scan’s result give it. Its result is the newsage-mcp-guidanceformat, version 1, whosedocsUrlis the rule’s guidance page. A rule ID sage has no guidance for fails with the new error codemcp.unknown-rule.- The docs have a guidance page for every rule, with the same guidance, the rule’s WCAG criteria or that it is a best practice, and its fix class. The rules reference links each rule’s ID to its guidance page, which links axe-core’s documentation of the rule.
- Part of the guidance adapts content from accessibility-agents, under the MIT License. sage-mcp’s
ThirdPartyNotices.txtcarries its notice, and each guidance page links it.
-
sage-mcp has a new tool,
summarize_fixes, which a fixer calls last, once its fixes are verified. It reports what became of each issue the agent’s scans found: the rule in plain language, who it affects, its WCAG criteria or that it is a best practice and not a WCAG failure, its severity, page, element and each place in the source its findings sit at, with its call sites, whether it was fixed, is proposed for a person’s review or was escalated to one, what the fixer changed and the test that pins the fix, the element’s accessibility-tree entry before and after, and links to each criterion’s page in Understanding WCAG 2.2, the rule’s documentation and its guidance page. It takes, for each issue the fixer changed, what changed and the test. Its result is the newsage-mcp-summaryformat, version 1, within the tools’ size budget; an issue it leaves out stays for the next call.- Taking the summary ends the remediation loop of each issue it reports, removing its record and screenshot from
.sage/loop/. - The docs’ guidance pages link each WCAG criterion of a rule to its page in Understanding WCAG 2.2.
- Each attempt
verify_fixrecords keeps the element’s accessibility-tree entry in the rescan, in a new optionalafterfield of the loop record,sage-loopversion 1. - Breaking, for loop records: a verification’s claim on a loop record now records when it expires, in a new required
expiresAtfield ofsage-loopversion 1, which changes in place before 1.0.0. Every sage-mcp process sharing a project judges a claim by that expiry rather than by its own call bound, so processes with different bounds agree on which claims are live. A record that an older sage-mcp left while one of its verifications was under way lacks the field: this release skips it, with a warning, as it skips any record it cannot read, until you delete it from.sage/loop/. An older sage-mcp in the same project likewise skips a record that holds either new field, as it skips any record a newer sage wrote.
- Taking the summary ends the remediation loop of each issue it reports, removing its record and screenshot from
-
get_fix_guidancetakes an optionalframeworkargument, whose one value isreact. With it, the result addsframework: the rule’s fixes that differ in React, with JSX examples, and the fixes that look right in React but still fail people. A rule whose fix is the same in React has no such section, so its result gives the guidance for any framework alone, as it does without the argument. Thesage-mcp-guidanceformat gainsframeworkas an optional field at version 1.- Each rule’s guidance page in the docs shows its React section, under “In React”, after the fixes for any framework.
-
Every setting now follows one rule: it takes its value from its flag, then its environment variable, then your user config, then the project’s config, then its default.
- Every setting that holds a single value has a
SAGE_variable named from its key.SAGE_BASE_URLsetsbaseUrl, forsage scanand sage-mcp’sscan_urlalike,SAGE_POLICY_FAIL_ONsetspolicy.failOn, andSAGE_BROWSER_STORAGE_STATEsetsbrowser.storageState; outside a project, a relative path in it resolves against the working directory. A value the setting does not accept fails withconfig.invalid-environment. - A new user config,
config.jsonin your per-user config directory ($XDG_CONFIG_HOME/sageor~/.config/sageon Linux,~/Library/Application Support/sageon macOS,%APPDATA%\sageon Windows), holds your own settings for every project:log.level,log.file,log.format,log.filesKept,color, and sage-mcp’smcp.headless,mcp.callTimeout,mcp.actionTimeoutandmcp.navigationTimeout. SAGE never writes it. A release ignores a key or a value it does not take, with a warning naming it, so a key a newer release adds never breaks an older one.sage infoshows where the file is, whether it was read, and what it ignored, in its text and its--jsonoutput. log.filesKept(SAGE_LOG_FILES_KEPT) sets how many log files of each kind, and how many crash files,logs/keeps; it is 10 by default, as before.colorturns color on stderr on or off for thesagecommands;NO_COLORandFORCE_COLORstill override it.- sage-mcp runs its browser headless from its
--headlessargument,SAGE_MCP_HEADLESS=1or the user config’smcp.headless, the argument first. Its bound on each tool call ismcp.callTimeout(SAGE_MCP_CALL_TIMEOUT), 60 seconds by default, and a call or a browser that reaches it says so, naming the setting and its value. Its browser tools takemcp.actionTimeoutandmcp.navigationTimeout(SAGE_MCP_ACTION_TIMEOUTandSAGE_MCP_NAVIGATION_TIMEOUT), 5 and 60 seconds by default, as before. sage-mcp reads the user config as it starts, so an edit takes effect when your agent restarts it. SAGE_LOG_FILE=0now writes no log file, where it wrote one named0.- An invalid value in the project’s config now fails with
config.invalideven where a flag or a variable replaces it. --base-urlis checked as the config’sbaseUrlis, and an invalid one fails withcli.invalid-argumentand the message--base-url cannot be "<value>": Expected an absolute http or https URL.- A person’s own settings never enter
configHash, so two people’s runs of one project’s config record the same hash, as before.
The docs add a user config reference and an environment variable reference, and the config reference lists each key’s variable and flags.
- Every setting that holds a single value has a
-
Source stamps can be turned on from the project’s config: its new
sourceStampskey says when@vispero/sage/vitestamps.truestamps every build and the dev server,falsestamps neither, and left out, the dev server stamps and a build does not, as before.SAGE_SOURCE_STAMPSis now the setting’s variable, read as every setting’s is, and replaces the config’s value:1ortruestamps, and0orfalsestamps nothing, the dev server included. Before, only1stamped a build, and every other value was ignored.- The plugin reads the setting once, as Vite starts, from the config in the project root it finds from Vite’s root, so it loads
sage.config.tsin Vite’s process. A config it cannot read, or aSAGE_SOURCE_STAMPSthat is no switch, never fails the build: Vite prints one warning naming the error’s code, and the plugin stamps as if the setting were left out, so a build then carries no stamps, even withSAGE_SOURCE_STAMPS=1. - A config whose exported object throws as it is read, such as from a getter, now fails with
config.load-failed, naming the file, where it was an internal error. - A secret the config puts in a secret-named variable as it loads, such as by loading a
.envfile, is redacted from what SAGE writes after it: the logs and failure reports ofsageand sage-mcp, sage-mcp’s error results, and the plugin’s warnings. sage scanand sage-mcp accept the key and leave it out ofconfigHash, since it changes how the app is built rather than what a scan finds. An invalidSAGE_SOURCE_STAMPS, such asyes, now fails them withconfig.invalid-environment, as any setting’s invalid variable does.
The source locations guide and the config reference describe the key.
-
verify_fixfollows an element whose fix changed its selector, on an app built with SAGE’s source stamps. When the selector the scan recorded no longer leads to the element, as after a fix that changes its class or turns adivinto abutton, it finds the element by where its opening tag starts in your source, the file, line and column of its stamp, which an edit inside the tag leaves as they were. A rescan that still finds the rule failing on that tag answersstill-failing. On an unstamped build, after a fix that moves the tag to other lines, and for a tag the page renders as several elements, as in a list, the verdict staysinconclusivewithelement-missing, and its reason’s detail says which.verify_fixchecks in the page which of the rescan’s findings of the issue’s rule are on the very element it follows, so an issue that still fails isstill-failingwhatever selector the scan now names its element or its iframe by, as after a fix that gives either an ID, on any build. A finding it cannot compare with the element, as when the scan’s time runs out first, answersinconclusivewithevidence-unavailable.- Breaking, in place at version 1: a
sage-looprecord in.sage/loop/holds the finding’s source location, on a build with source stamps, in a new optional field,source. A sage-mcp without the field skips such a record with a warning, so itsverify_fixdoes not find that issue.
Patch Changes
Section titled “Patch Changes”- The gate’s line that
sage scanandsage report mergeprint on stderr now words the verdict assummary.md’s headline and the verdict’s GitHub annotation do: the verdict, then the sentences that say what decided it, such asGate passed: No new issues at or above minor.,Gate failed: 2 new issues at or above minor. 1 target was not scanned.orGate incomplete: 1 target was not scanned, so the run could not confirm a pass.A script that matches the line, such as one that greps for^Gate passed$, needs its pattern updated. The exit codes are unchanged. - Under GitHub Actions,
sage scanandsage report mergenow report to the run itself. They append the report directory’ssummary.mdto the step’s job summary, the fileGITHUB_STEP_SUMMARYnames, and print annotations as GitHub’s workflow commands on stdout: one with the gate’s verdict, then the new violations, the issues that need review and the violations an agent found, each at its source file and line when the app was built with source stamps. GitHub shows at most 10 annotations of each level from a step, so the verdict’s annotation says how many issues it left out; every issue is still inresults.sarifandresults.json. With--out -,sage scanprints no annotations, since stdout carries the results. If your workflow appendssage-report/summary.mdtoGITHUB_STEP_SUMMARYitself, as the CI guide’s workflow did, remove that step, or the summary shows twice. Under any other CI provider, nothing changes. - Update jose, which verifies SAGE’s account tokens, to 6.2.13.
sage hook <event> --harness <claude|codex>answers a Claude Code or Codex command hook: it reads the event’s input on stdin and writes its answer on stdout, as one JSON object or nothing, and always exits 0, writing nothing to stderr, so a hook never blocks the agent’s tool call. That holds however it ends: an error nothing caught, a console write from any module, and SIGINT, SIGTERM or SIGHUP, each of which ends it at once with exit 0. Its first event ispre-tool-use. Without a valid account it answers with onesystemMessagethat says why and how to sign in; it checks the account without renewing it. It logs to.sage/logs/hooks-<session>.log, one file per agent session. It takes no help option, and no global option afterhook, so--helpand--versionnever reach its stdout:sage help hookprints its help.sage hook pre-tool-useturns a call of one of SAGE’s tools back when it comes from any agent but SAGE’s fixer,sage:a11y-fixerin Claude Code anda11y_fixerin Codex, with an instruction to dispatch the fixer with the user’s request, asking it only to report findings when the user asked only for them, so scans and their findings stay out of the main conversation. The fixer’s own calls run.
Minor Changes
Section titled “Minor Changes”- Add
sage auth token, which prints the account token on stdout, alone and followed by a newline, for a script that needs it, asgh auth tokenprints a GitHub token. It checks the account first, so it renews a signed-in token that expires within the hour, and prints only a token that verifies: without a credential it fails withaccount.missingand exit code 2. WhenSAGE_TOKENis set, it prints that token. The token stays out of stderr, log files and crash files. See https://cora.vispero.com/docs/accounts/#printing-the-token. - Breaking: results files, baselines and sage-mcp’s scan and issue results now record the browser beside the viewport. Every target and every baseline page has a
browserfield, the browser’s name, such aschrome. A run lists each browser it launched, with its name and version, inbrowsers, in place of its onebrowser. sage-mcp’ssage-mcp-scanandsage-mcp-issueresults give the browser of the page they scanned and of each finding, and each issue’scombinations, the browsers and viewports it was seen in. These formats change in place at version 1, so this release reads no results file or baseline that sage 0.1 wrote: scan again rather than merging results files from sage 0.1. A baseline sage 0.1 wrote fails with the newbaseline.outdatedcode. sage 0.1 scanned only in Google Chrome, so add"browser": "chrome"to each of its pages: in any editor, replace each"viewport":insage.baseline.jsonwith"browser": "chrome", "viewport":. The baseline then accepts exactly the issues it did before. An issue seen in several browsers or at several viewports counts once, and the Markdown summary, the SARIF log andexplain_findingname each combination it was seen in, such asin Google Chrome at 1280 by 720. A target whose page shows another viewport than the one sage planned it at is recorded as not scanned, under the new coverage reasoncombination-mismatch. - Breaking: sage-mcp’s
sage-mcp-scanandsage-mcp-issueresults now give each issue in the shape an agent fixes it from. On an app built with SAGE’s source stamps, each issue in a scan’s result and each finding ofexplain_findinghas asource: its file, line, column, opening tag andresolution, which iselementfor the element’s own tag orcall-sitefor the host element around the place your code uses a component that renders it. Each issue also hascallSites, the number of distinct call-site locations its findings are at. Each finding ofexplain_findingreplaces itsreproducetext with two fields.reproducenames the finding’s page and says how to reach it again: itskind,load,stateornone, with its URL, browser, viewport and named state.verifynames the rule a rescan must run and the issue ID to look for in its result. A finding no longer has its ownurl,reachedBy,state,browserandviewportfields: read them fromreproduce, whosekindisloadfor a page reached by navigation. These formats change in place at version 1, so a client or skill that readreproduceas text, or a finding’s page from those fields, readsreproduce’s fields instead. - sage-mcp has a new tool,
verify_fix, which checks a fix of an issue by its ID. It rescans the issue’s page, loading it again or, for a page the agent reached by interaction, as the current tab shows it, and compares the element’s accessibility-tree entry with its entry when the issue was found. It answersfixed,still-failingwith the ID the rescan lists the issue under,suppressedwhen the fix hid the element from assistive technology, orinconclusivewith its reason, and the loop’s next step:continue,doneorescalate. Its result is the newsage-mcp-verifyformat, version 1.scan_pageandscan_urlrecord each issue’s element in.sage/loop/: its accessibility-tree entry and a screenshot of at most 2 MiB. The record of an issue no fix was checked for goes once a scan of its page as it loads covers its rule and no longer finds it. No screenshot is sent to the agent.- Breaking, in place at version 1: the
sage-mcp-scanresult’sscanned.coveragehas a new required field,omitted, counting the coverage reasons the budget leaves out. A scan’s issues fill its result first and its coverage reasons the rest, and each URL and reason detail a result carries is cut to 2,048 characters. - Breaking, in place at version 1: a
sage-looprecord’s evidence can beunavailablewith the reasontimed-out, when the scan’s time ran out before the capture took it, and its screenshottoo-large. verify_fixclaims an issue’s loop while it judges a fix, so another sage-mcp process’s scan neither removes the loop nor replaces its evidence meanwhile. A verification whose loop started again with newer evidence fails with the new error codemcp.loop-changedand records no attempt. Asage-looprecord holds each verification’s claim in a new optional field,verifying, and each attempt names the run of the rescan that judged it in a new required field,run, a break in place at version 1. A scan never replaces or removes what a newer scan or verification wrote to.sage/loop/.- Every tool result stays within its budget. Error results list their
issueswhile they fit and count the rest in a new optional field,omittedIssues. Texts a result takes from a page or a file are cut as their JSON takes them, andexplain_findingfails ascontract.invalidfor a run whose unknown rule has an ID longer than 256 characters. - A scan cancelled while it captures its issues’ elements writes no run.
Patch Changes
Section titled “Patch Changes”sageand sage-mcp also ignore two of Playwright’s environment variables that change its screenshots:PW_TEST_SCREENSHOT_NO_FONTS_READY, which would take a screenshot before the page’s fonts load, andPLAYWRIGHT_LEGACY_SCREENSHOT, which would launch Chrome without the feature Playwright takes screenshots through. They still apply to your own Playwright tests.- A navigation that lands while sage reads a finding’s source location inside an iframe, after finding the iframe, now counts as a navigation.
sage scanand sage-mcp’sscan_urlskip the page withnavigation-failed, andscan_pageanswerspage.navigated. Before, the whole scan failed with Playwright’sElement is not attached to the DOM. - Update p-timeout, which bounds SAGE’s waits on browsers, pages, processes and the service, to 8.0.0.
- Both packages now have a README on npm, and npm metadata: SAGE’s documentation as the homepage, the troubleshooting page and
sage@vispero.comas where to report a bug, keywords and an author. The hint of an internal error,internal.unexpected, now namessage@vispero.comas where to send its crash file or report. @vispero/sagecarries SAGE’s plugin tree for Claude Code and Codex indist/harness/: each harness’s manifest, and the MCP entry each loads, which runs sage-mcp throughnpxat the package’s own version and passes itSAGE_TOKEN,XDG_CONFIG_HOME,XDG_STATE_HOME,DISPLAYand the proxy variables by name.- The SARIF log in the report directory,
results.sarif, now names SAGE’s docs site as the tool’s information address, intool.driver.informationUri: the docs of the sage release that wrote it, such ashttps://sage.vispero.com/docs/0.2/. sage scanand sage-mcp’sscan_urlreport a page that shows another viewport than planned as not scanned, undercombination-mismatch, even when itsincludematches nothing at that viewport. Before, the scan failed withscan.invalid-scope, or a same-host redirect’s landing was reported asredirected.explain_findingkeeps its result within its 16,000-character budget for an issue seen in many combinations. It lists the issue’s combinations in canonical order while they fit, then its findings. Thesage-mcp-issueresult has a new field,omittedCombinations, which counts the combinations left out,0when every one is listed, and itscombinationsmay be empty when none fits. The format changes in place at version 1.
- On Windows, sage-mcp now waits for
taskkillto end npx and every process npx started before it exits, when fetching your project’s sage-mcp release takes longer than its 5-second bound. Before, sage-mcp could exit first, which left npx’s Node processes running.sage,sage mcpand sage-mcp likewise wait fortaskkillwhen they pass Ctrl-C or a termination signal on to the copy they handed over to. Ataskkillthat has not finished within 30 seconds is let go. - A file that
sageor sage-mcp cannot write because part of its path is a file rather than a directory, such as a.sage/runsthat is a file, now fails asfile.write-failed, naming the file, rather than as an internal error. A log file that cannot be closed once the work ends is now a warning naming the file, and the command ends as its work did, rather than failing as an internal error. explain_finding’s description names the combinations of browser and viewport its result lists for the issue.
Patch Changes
Section titled “Patch Changes”@vispero/sage/viteparses JSX with Babel’s parser 7 again, which runs on every Node.js release sage supports. Babel’s parser 8 left out Node.js 23 and 24.0 to 24.10, which sage’senginesadmits.sageand sage-mcp now check the Node.js release before they do anything else. On a release older than the 22.18.0 theirenginesfield names, which npm installs on with only a warning, every command,--versionand--helpincluded, exits 2 with the newnode.unsupported-versioncode, naming the running release and the one to install, instead of failing later withinternal.unexpectedand a crash file to send.- Update undici, the HTTP client that reaches the SAGE service, to 7.30.1.
Minor Changes
Section titled “Minor Changes”- Breaking: sage and sage-mcp now need a SAGE account.
sage scan,sage report mergeandsage baseline updatecheck the account before they run, and sage-mcp checks it before each call to any of its tools, thebrowser_*tools included. Without a credential, a command exits 2 withaccount.missing, and a tool answers with anaccount.missingerror result whose next action issage auth login. A token that has expired fails withaccount.expired, one that does not verify withaccount.invalid, and a check the SAGE service does not answer, as it renews a signed-in credential or sends its current keys, with aservice.*code. Sign in withsage auth login, or in CI setSAGE_TOKENto a CI token from your SAGE account.sage info,sage init,sage mcpand theauthcommands need no account, and neither do@vispero/sage/viteand@vispero/sage/config; sage-mcp lists its tools without one. sage-mcp reads the credentialsage auth loginsaves, orSAGE_TOKENwhen its MCP client passes it on, and reaches the SAGE service through the proxyHTTPS_PROXY,HTTP_PROXYandNO_PROXYname, each request naming its client assage-mcp/<version>. sage-mcp now depends onundici,joseandopenid-client(all MIT), listed in its third-party notices. - Add
sage auth login,sage auth logoutandsage auth status.sage auth loginsigns in to your SAGE account: it prints a page on https://sage.vispero.com and a code, opens the page in your browser where one can show it, and once you approve the login saves the credential incredentials.jsonin your per-user config directory;--with-tokensaves an account token read from stdin instead.sage auth logoutremoves that file, andsage auth statusprints the account on stdout, with its source,SAGE_TOKENor the file. sage reaches the SAGE service through the proxy thatHTTPS_PROXY,HTTP_PROXYandNO_PROXYname, each also read in lower case, and each request names the client and its version,sage/<version>; the service refuses a release older than the oldest it serves withservice.client-outdated.sage infolistsDISPLAY,WAYLAND_DISPLAY,WSL_DISTRO_NAME,SSH_CONNECTION,SSH_CLIENTandSSH_TTY, which decide whethersage auth loginopens a browser. sage now depends onundici,jose,openid-clientandopen(all MIT), listed in its third-party notices; only theauthcommands load them. - Add
@vispero/sage-mcp, an MCP server that gives a coding agent a browser and SAGE’s scans. The browser is playwright-mcp’s, with itsbrowser_*tools for navigating, clicking, typing, tabs, snapshots and screenshots;browser_run_code_unsafeis withheld.scan_pagescans the page as the agent left it, such as with a dialog open;scan_urlloads a URL, or a path resolved against the config’sbaseUrl, and scans it assage scanscans a target;list_ruleslists the rules the project’s config runs; andexplain_findingexplains an issue from a scan, with each element, why it failed and how to check a fix. Scans use the project’s config, rules and sign-in, record each run in.sage/runs/, and fail with an error code, a hint and a docs link. An agent starts it withnpx -y @vispero/sage-mcp@<version>. It then runs at the version of@vispero/sagethe project has installed, fetching that release on its first start, and--headlessruns the browser without a window. In a project with@vispero/sage,sage mcpruns the sage-mcp release at the version the project has installed, passing its options on. - Add the policy gate and CI reporting.
sage scannow compares its run withsage.baseline.jsonand exits 0 when no new violation at or abovepolicy.failOnappears, 1 when one does, and 2 when a target could not be scanned or an error stopped it;--shardand--no-gatewrite the run without comparing it.sage report mergemerges results files, such as every shard of a CI run, and gates them.sage baseline updateaccepts the violations in results files into the baseline, removing only what a run proved fixed, and refuses to rewrite a baseline holding fields a newersagewrote. In CI, and wherever--report <dir>asks, the commands writesage-report/with the compared results, a SARIF 2.1.0 log for GitHub code scanning and a markdown summary.sage initwrites a starter config, andsage infoprints the environment a bug report needs, as text or--json. - Add
sage scan, which scans pages, lists of pages and same-origin crawls with axe-core in Google Chrome, through the project’s own Playwright, and writes a results file to.sage/runs/or--out. It signs in through a Playwright storage-state file or asetuphook insage.config.ts, records redirected and failed targets, shards with--shard i/n, and reports errors with a code, a hint and a docs link. Logging is configured with--log-level,--log-file,--log-format,-v,-qand--debugor the matchingSAGE_LOG_*variables, and an internal error writes a crash file.@vispero/sage/configexportsdefineConfig. - Scans read the source stamps
@vispero/sage/vitewrites:sage scan, and sage-mcp’sscan_pageandscan_urlin a project, give a finding on a stamped element its place in the source aslocation.source, at the element’s own opening tag or, for an element without a stamp of its own, at its nearest stamped ancestor’s, with a source fingerprint built from its rule, its file and that tag. A stamp counts only while its file, read from the project’s repository as it is on disk, is unchanged since the build. Those findings’ issues regroup under the source fingerprint, so their issue IDs change once, and the SARIF log locates them at their source lines rather than at the repository root, so GitHub code scanning closes each one’s alert at the root and opens it at its line. A baseline written from a build without stamps still accepts the same violations. See https://cora.vispero.com/docs/source-locations/. - Add
@vispero/sage/vite, a Vite plugin that writes each host element’s place in the project’s.jsxand.tsxfiles into the page as itsdata-sage-sourceattribute: the file from the repository root, the opening tag’s start and end, and a digest of the file’s text. It stamps under Vite’s dev server but not in Vitest’s test runs, and in a build only withSAGE_SOURCE_STAMPS=1, whichsage infolists. It brings three runtime dependencies,unplugin,@babel/parserandmagic-string(all MIT), listed in the third-party notices; nosagecommand loads them. See https://cora.vispero.com/docs/source-locations/.
Patch Changes
Section titled “Patch Changes”-
Parse JSX for
@vispero/sage/vite’s source stamps with Babel’s parser 8. -
Check
--base-urlassage scanandsage initparse it: a value that is not an absolute http or https URL iscli.invalid-argument, with the same message for both commands, before the command runs.sage scanused to report it asconfig.invalid, naming the command line as the config’s source. -
Keep
sage’s stdout to what it writes there on purpose:console.log,console.info,console.debug,console.dirandconsole.dirxmlcalls in the process, such as one in a config’sbrowser.setup, now go to stderr, so they no longer corrupt the resultssage scan --out -writes to stdout.--versionand--helpstill print on stdout, and sage-mcp keeps its stdout to MCP messages as before. A failure either bin reports before logging starts now ends with adocs:line naming the error’s entry in the errors reference, as every other error report does. -
Bound every scan in time and report a page that does not answer.
sage scanends a target’s visit after 60 seconds and records the target astimed-out, so a page or browser call that never answers no longer hangs the run. A page blocked by a dialog or a script that never yields fails with the newpage.unresponsivecode after about 11 seconds, andsage scanandscan_urlrecord it aspage-unresponsive. A blank or non-http page, or one that breaks sage’s in-page checks, fails with the newscan.unsupported-pagerather thaninternal.unexpectedand a crash file. Run files record the coverage reasonstimed-out,page-unresponsiveandunsupported-page, an engine error’s detail is the first line of its message, and a page that navigates while an engine runs is recordednavigation-failedrather thanengine-error. Settling waits for a same-origin iframe whosesrcchanged just before it started, and no longer waits for one that changed its own URL through the history API. When no target is configured,sage scansays “There are no targets to scan”. -
Record the commit of a working tree whose
git statusprints more than 1 MiB, whichsage scanused to leave out of the run, and stop waiting for a git command after 30 seconds, recording the run without a commit. sage and sage-mcp now bound their waits withp-timeout(MIT), listed in their third-party notices. -
Detect CI providers and coding agents with
std-env, which recognizes about 50 providers, under sage’s own policy:CI=falseturns detection off, a providerstd-envcounts as CI is CI, Google Cloud Build is recognized byBUILDER_OUTPUT, and otherwiseCIset to anything but0is CI.sage infonames a provider and an agent asstd-envdoes, such asgithub_actionsandclaude, and lowercasesAI_AGENT. Azure Pipelines is recognized bySYSTEM_TEAMFOUNDATIONCOLLECTIONURIrather thanTF_BUILD,JENKINS_URLalone counts as Jenkins, and platformsstd-envcounts as CI, such as Cloud Run and Railway, run sage in CI mode. sage and sage-mcp now depend onstd-env(MIT), listed in their third-party notices. -
Print
Log: <path>as the last line of every command that wrote a log file and prints a summary, after a failure as well as after success, sosage --debug scannames its log file when it fails. -
List
XDG_CONFIG_HOMEandAPPDATAamong the environment variablessage inforeports as set or unset: they locate sage’s per-user config directory, on Linux and on Windows. On Windows, a file sage or sage-mcp replaces while another process, such as a virus scanner, holds it open is written once that process lets go, within a second, rather than failing at once withfile.write-failed. -
Describe every key of
sage.config.tswhere an editor shows types:SageConfig’stargets,policyandbrowser, and a crawl’scrawl,maxPagesandmaxDepth, now carry doc comments, andSageConfiglinks the new config reference, https://cora.vispero.com/docs/reference/config/, which lists every key ofsage.config.tsandsage.config.jsonwith its type, its default and what it does. -
Crash files name the program that crashed, so sage-mcp’s ends with
sage-mcp hit an internal errorrather thansage hit an internal error, and they keep the latest startup record of each kind, such asbrowser launched, however often the process logged it. -
Your editor’s hover on
defineConfig, on each key ofsage.config.tsand on the@vispero/sage/viteplugin, and the config reference on the docs site, now say what each key does, the values it takes and what leaving it out does, how it works with related keys, and the error code SAGE raises for a bad value, with a runnable example ondefineConfig. The errors reference’sconfig.invalidentry names asage.config.tswithout a default export among its causes. -
Docs links name the docs of the release that prints them. An error’s
docs:line, and thedocsUrlof an MCP error result and of a log record, link the error’s entry in the docs of the release’s minor,https://sage.vispero.com/docs/<major>.<minor>/reference/errors/#<code>, such as/docs/0.4/for every 0.4 release, so a link keeps describing the release that printed it after newer releases change the docs. -
Warn once for each engine whose coverage of a target is incomplete, as
engine coverage incompletewith the coverage’s status and reasons, in place of aframe not scannedwarning per frame and anengine failedwarning, which are now debug records. A scan scoped to part of the page warns of nothing for its scope. An engine result whose status and reasons disagree is recorded asengine-erroron that target rather than failing the scan. -
Rename the error code
state.write-failedtofile.write-failed, since it covers any file sage writes, such as the report directory, the baseline and the configsage initwrites, not only those in.sage/. A program sage or sage-mcp hands over to that cannot start, such as npx missing from thePATHinsage mcp, is now reported as the newinstallation.spawn-failedwith a hint, rather than as an internal error with a crash file. sage-mcp’s error results gain an optionalissuesfield that lists each problem with its path, such as each invalid argument of a tool call. -
Count issues, the findings that share a fingerprint, in the summary
sage scanandsage report mergeprint, in its gate line and in the headline ofsummary.md, as the summary’s table already did:10 issues (2 new), 1 needs reviewandGate failed: 2 new issues at or above minor. One element’s violation at two viewports used to count twice in the gate line and once in the table. The gate fails on exactly the runs it failed on before. -
sageand sage-mcp ignore three of Playwright’s environment variables that would change what a scan does:PWDEBUG(or npm’spwdebugsetting), which would run the browser headed with the Playwright Inspector paused and so fail or stall each scan,SELENIUM_REMOTE_URL, which would run the browser on a Selenium Grid instead of the machine’s Chrome, andPLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK, which would send service workers’ requests without the headers and HTTP credentials a config’ssetupsigns in with. They still apply to your own Playwright tests.sage infoalso reports whetherDEBUG_FILE, which writes Playwright’sDEBUGdiagnostics to a file, is set. -
A page that breaks sage’s in-page checks fails with
scan.unsupported-pageand lists what was wrong with the result, and a page that reports a zero or negative viewport is not scanned for the same reason. A frame whose path passes through a shadow host or iframe selector that matches more than one element within its own root is recorded asframe-not-scanned, rather than scanned through one of the matches. -
List
DEBUGamong the environment variablessage inforeports as set or unset: Playwright reads it in sage’s process and prints its diagnostics on stderr, outside sage’s redaction. -
Report in
sage infowhere the project’s own Playwright expects its bundled Chromium, the path itschromium.executablePath()gives.sage infoimports the project’splaywright-coreonly for that, and only when the project has one; it still finds Playwright’s download directory fromPLAYWRIGHT_BROWSERS_PATHor the default cache. -
Report a project’s
@vispero/sagewhosepackage.jsonnames nosagebin as the newinstallation.bin-missing, with a hint to reinstall it, where sage used to printinternal.unexpectedwith a stack trace. npx, and the sage-mcp release it starts, now run on the Node that runs sage or sage-mcp:sage mcp,sage init’s pre-fetch, and sage-mcp’s fetch and hand-over put that Node’s directory first on npx’sPATH, so an MCP client that starts sage-mcp by an absolute Node path no longer runs the release on an oldernodefound earlier on thePATH. The project’s sage that sage hands over to gets the samePATH. -
An error that lists problems by path writes a key that is not an identifier in quoted brackets, such as
rules["axe/color-contrast"]for a rule in the config, so a rule ID’s slash or a key’s dot no longer reads as a nested path. sage-mcp’s error result for an invalid config lists each problem in itsissuesfield, as the one for invalid tool arguments does. -
Log how
sagechose the copy that runs a command, as the startup recordlaunch decided, in every command’s log and crash file and in a hand-over to the project’s ownsage.sage inforeports that decision’s reason as itscopy:no-project,no-project-sage,same-copy,deferral-disabledorown-copy-command, in place ofproject,outside-projectandhand-over-skipped; its JSON also names the config’s hashconfigHash, as run files and log records do, in place ofhash. -
When the SAGE service no longer holds the code
sage auth loginpolls with, such as a code a login already used, the login fails withaccount.login-failedsaying so and telling you to start a new login withsage auth login, rather than saying that the service answeredinvalid_grant. -
Update LogTape, the logging library, to 2.4.2, and zod, the schema library, to 4.6.7.
-
Publish sage and sage-mcp as minified JavaScript, about half the size it was, so installing sage and fetching sage-mcp through npx download less. The stack trace of an internal error, in a crash file or on stderr, now shows the minified code’s short function names in place of sage’s own.
-
Detect npm from
npm-shrinkwrap.json, which npm prefers topackage-lock.json:sage inforeports it as the lockfile, andsage initprints npm’s install command for a project that has only that lockfile. -
Start, report and end
sageandsage-mcpthe same way. A log file or state directory that cannot be created leaves alog file not openedwarning, and the command or the server runs without a log file, wheresageused to fail. Thesage startedrecord names the command, such asreport merge. A failure before sage-mcp serves is logged ascommand failedrather thanstartup failed, and an invalidSAGE_LOG_*value given to sage-mcp is printed redacted, withhint:anddocs:lines. sage-mcp’s crash records for a tool carrycode,hintanddocsUrl, assage’s do, andconfig loadedis a startup record only for the first browser launch. A sage error that nothing catches, such as one a timer in a config’sbrowser.setupthrows, is reported as that error rather than as an internal error with a crash file. -
Sign in and read the config one way.
sage scanand sage-mcp sign in through one step that registers the storage state’s values as secrets before Chrome launches, andsage scan’sbrowser launchedrecord carriesheadless, as sage-mcp’s does. sage-mcp reads the config once for each tool call, which the browser that call opens shares, and logsconfig loadedfor each call that reads it. A browser context that never opens, with nobrowser.setuphook, fails asbrowser.launch-failedrather thansignin.setup-failed. A browser that does not close within 10 seconds is left to sage-mcp’s exit with abrowser did not closewarning, so it no longer holds the next call, and the next browser tool opens in a new browser rather than in the one left behind.sage infologsconfig loadedas the other commands do. -
Scan with sage’s own axe-core on pages that load their own. sage keeps its axe-core under a private global in each frame and never reads or replaces the page’s
axe, so a page that ships axe-core of any version or configuration, such as a Storybook with its accessibility addon, gets the same findings as one that does not, the page’saxe, AMDdefineand CommonJSmodulestay as they were, and sage’s axe-core never answers the page’s own runs across frames. -
Say that the project’s Playwright names no version when its
package.jsonhas none, in theplaywright.unsupported-versionmessage, rather than calling itversion undefined. Versions compare part by part with a prerelease below its release, so a prerelease of Playwright 1.51.0, such as1.51.0-alpha-1, is below the 1.51 floor and fails as unsupported; it used to pass. -
Rename the product to SAGE. The packages are now
@vispero/sageand@vispero/sage-mcp, and their binssageandsage-mcp, so a project installs@vispero/sageand runssage scan, and a coding agent starts the MCP server withnpx -y @vispero/sage-mcp@<version>. The earlier package and bin names have no alias.The files and directories a project holds take the name too: the config is
sage.config.tsorsage.config.json, the baselinesage.baseline.json, the state directory.sage/and the report directorysage-report/, and outside a project the per-user state directory is namedsage. The environment variables areSAGE_LOG_LEVEL,SAGE_LOG_FILE,SAGE_LOG_FORMATandSAGE_NO_DEFER. None of the earlier names is still read.The formats are
sage-results,sage-baselineandsage-config, withsage-mcp-*for the MCP server’s, and a results file records the version that wrote it assageVersion. A SARIF log names the toolSAGEand keys its fingerprintssage/element/v1andsage/source/v1. Every fingerprint value changes, and a results file or baseline written before is rejected as another format, so scan again and runsage baseline updateto write a new baseline. Log records take the categorysage, so a record’sloggerissage.cliorsage.mcp, and the startup record issage started, with its version assageVersion. The MCP server announces itself assage-mcp, and under the client keysageits tools are namedmcp__sage__*.Messages, hints, the CLI’s help and the MCP server’s instructions and tool descriptions name SAGE too. An error’s
docs:line is now a full link to its entry on the docs site athttps://sage.vispero.com/docs/, as is thedocsUrlof an MCP error result and of a log record. -
List
HTTPS_PROXY,HTTP_PROXYandNO_PROXY, each also in lower case, among the environment variablessage inforeports as set or unset. sage, sage-mcp and@vispero/sage/vitekeep the credentials in the URL ofHTTPS_PROXYorHTTP_PROXY, in either case, out of their output and logs, as they do the values of variables whose names containTOKEN,SECRET,PASSWORDorKEY. -
Write the report directory so that a write failing partway never leaves a
results.jsonbeside a mix of two runs’ files:sagedeletes an earlier run’sresults.jsonbefore writing anything and writes its own last. When the SARIF log orsummary.mdleaves items out for its destination’s limit,sagewarnsreport items omittedwith the file and the count; the SARIF warning wasSARIF results omitted, and rows the summary left out were not warned about. -
Check the config’s
rulesoverrides when the config loads, sosage info,sage report mergeand every sage-mcp tool that loads the config reject a rule ID no rule has, or a lint rule’s ID, withconfig.invalid, assage scanalready did. The error names the config file and writes each issue’s path asrules["axe/no-such-rule"].JITI_*environment variables, such asJITI_TRY_NATIVEorJITI_ALIAS, no longer change howsage.config.tsloads or what it prints. -
Keep the 10 log files of each kind in
.sage/logs/that were written most recently, rather than the 10 whose names sort last, so the log of a sage-mcp server that is still running outlives those of servers started after it and stopped. sage-mcp’sexplain_finding, given no run, reads the runs in.sage/runs/newest first only until one holds the issue, rather than reading every run first. -
Treat a page as the same page across http and https on one host: a crawl follows the links on its start’s host over either scheme, rather than only those on its origin, a target that redirects to its own host over the other scheme keeps its landing page as a target, and
scan_urlkeeps such a landing page too. A crawl’sexcludepatterns are tested against a link with its credentials redacted, and theredirect landedlog record names a landing on another hostother-hostrather thancross-origin. -
Rename the error codes
account.service-unreachabletoservice.unreachableandaccount.client-outdatedtoservice.client-outdated, since each names a problem with the SAGE service rather than with the account. The earlier codes have no alias. A request that the SAGE service answers with a 500, 502, 503 or 504 status now fails as the newservice.unavailable, whose hint says to run the command again in a few minutes, rather than asaccount.login-failedoraccount.expiredwith a hint to sign in again. A token signed by a key the release does not hold, whose key sage could not fetch from an unavailable service, is stillaccount.invalid, now with a hint to update@vispero/sageor wait rather than to sign in again. The errors reference now says that codes keep their meaning and stay listed from 1.0.0 on; before 1.0.0 a release may rename or remove a code, as this one does. -
Keep a browser context’s init scripts working in an iframe a page adds while a scan waits for it to settle: the scan no longer reads the iframe before it loads, which made the init scripts run in its empty document only.
-
Reject a target URL with no slashes after its scheme, such as
http:example.com, astarget.invalidinsage scanand sage-mcp’sscan_url, the waybaseUrlalready was. Such a target used to resolve tohttp://example.com/. -
explain_findingadvises rescanning at the finding’s viewport, set withbrowser_resize, and names the issue ID and rule to look for in the rescan’s result. It no longer says that a fix holds once the issue is no longer listed, since a rescan that drops the issue does not show a fix by itself. -
Retire the error code
mcp.page-unresponsive. A page a dialog blocks ispage.unresponsive, whichscan_pagefails with andscan_urlrecords as not scanned, and a scan that runs past 60 seconds fails asmcp.browser-timeout.scan_urlrecords a load exactly assage scandoes, a scan that runs out of time included, which it records astimed-outbefore closing the browser. A client’s cancellation of a call now takes effect: a call cancelled while queued never runs, and a running scan stops after its current step, so the next call starts at once, and no crash file is written.scan.invalid-scoperesults list each selector that cannot apply as an issue. -
Log sage-mcp’s tool calls at the level their outcome deserves: a tool that fails is
tool failedatwarn, a bug in sage-mcp isinternal erroraterrorwith its crash file, and a browser tool’s own error result isbrowser tool failedatinfo.browser launchedis a startup record only for the first launch. When playwright-mcp’s tools cannot be listed,tools/listand every browser tool’s call answer JSON-RPC error-32603with the redacted report as the message and thesage-mcp-errorpayload as its data, where the raw error used to reach the client.