Skip to content

Releases

v0.28.2

Choose a tag to compare

@jeffyaw jeffyaw released this 12 Nov 02:59
v0.28.2
8b65414

Changed

  • The ctxlint launcher's oam floor is now 0.19.5, up from 0.19.1. OAM_MIN tracks the latest oam release, and ctxlint was re-verified on oam 0.19.5 through the launcher (npm run verify:oam-floor: initialize + tools/list). An oam host at 0.19.4 or earlier now hands off or falls back to Node instead of running the CLI in-process.

Security

  • Dev-dependency updates for the open Dependabot and pnpm audit advisories: vite 8.3.8, vitest and @vitest/coverage-v8 4.1.12, zod 4.6.7, and overrides for hono, @hono/node-server (now 2.x), fast-uri, undici, body-parser, qs, ip-address and minimatch, plus glob 14.0.5 (was 13.0.6). Of these, glob, zod, fast-uri and minimatch are bundled into dist/index.js and so reach the published package; the rest are build and test tooling only.

v0.28.1

Choose a tag to compare

@jeffyaw jeffyaw released this 31 Oct 02:47
v0.28.1
bfc74b5

Changed

  • The ctxlint launcher's oam floor is now 0.19.1, up from 0.18.0. OAM_MIN tracks the latest oam release, and ctxlint 0.28.0 was re-verified on oam 0.19.1 through the launcher (npm run verify:oam-floor: initialize + tools/list, plus a tools/call). An oam host at 0.19.0, a 0.19.0 dev build or 0.18.x now hands off or falls back to Node instead of running the CLI in-process.

Fixed

  • server.json now launches the MCP server. Its npm package carries the positional serve argument, so a registry client that runs npx @yawlabs/ctxlint from it starts the MCP server instead of a one-shot lint of its working directory.

Internal

  • scripts/check-oam-floor.mjs now runs when invoked through a junctioned or symlinked checkout. Its direct-run guard compared process.argv[1] with the main module's URL, which Node realpaths and argv[1] is not, so from such a path the check skipped main() and exited 0 with no output. Both sides are now realpath'd (ported from yaw-mcp 1.1.2's verify-oam-floor guard).

v0.28.0

Choose a tag to compare

@jeffyaw jeffyaw released this 09 Oct 21:12
v0.28.0
b6c92fc

Changed

  • The MCP server now sends instructions, and the four audit tools return summary first. The initialize result carries a short plain-ASCII routing note (well under 2000 bytes): which audit tool covers which file class, that ctxlint_fix writes unless dryRun is true, and that ctxlint_token_report is the cheap probe. Yaw MCP renders a server's instructions once per namespace per session. ctxlint_audit, ctxlint_mcp_audit, ctxlint_session_audit and ctxlint_skill_audit serialize their result as {summary, version, scannedAt, projectRoot, files, ...} without the two-space indent: Yaw MCP caps a tool result at 100000 bytes by default and cuts from the tail, so on a large project the summary counters used to be the first thing lost. The fields and their values are unchanged; a client that parses the JSON sees no difference, and one that read the raw text sees one compact line.
  • The ctxlint launcher's "no usable oam" error now names the fix for what it found. Under CTXLINT_RUNTIME=oam it used to print "Install or update from https://oamjs.org" for every cause. It now says oam self-update when an outdated oam was found, to check the binary when one would not run, to point OAM_BIN somewhere real when it names a missing file, and to install oam only when none was found -- and on Linux other than x64, where oam publishes no build, that there is nothing to install. CTXLINT_RUNTIME=node is offered last in every case.
  • The launcher finds an oam installed to OAM_INSTALL_DIR. oam's own install-target variable is now searched first, before %LOCALAPPDATA%\oam\bin, ~/.oam/bin and PATH; an oam installed there and not on PATH was never found.
  • A handoff from an oam host no longer passes oam's permission flags on to Node. oam 0.18.0 appends its --permission / --allow-* execArgv entries to every child's NODE_OPTIONS. When the launcher runs on oam (an oam below the floor, or CTXLINT_RUNTIME=node), it now spawns its child with those tokens removed from the inherited NODE_OPTIONS; Node 22 refuses to start at all with an oam-only flag such as --allow-net there (exit 9). It cannot stop the host oam re-adding its own flags, which happens only when the launcher itself was started under --permission.
  • The handoff from an oam host inherits stdio unless that oam is older than 0.9.0. Piping was a workaround for pre-0.9.0 oam treating stdio: 'inherit' as 'pipe'; from 0.9.0 on the launcher hands over the same fds instead of copying every MCP/LSP byte through itself. An oam host whose version cannot be read still pipes.

Internal

  • release.sh step 3 now also runs scripts/check-oam-floor.mjs (ported from @yawlabs/aws-mcp: OAM_MIN agrees with every floor claim in the launcher and its test, and is not behind the latest oam release -- CTXLINT_ALLOW_STALE_OAM=1 releases on an old floor deliberately), scripts/verify-oam-floor.mjs (hosts bin/ctxlint.mjs serve through oam run on the release machine's oam and completes initialize + tools/list; a machine with no oam is a warning, an oam below the floor fails), and @yawlabs/mcp-compliance ^0.20.4, now a devDependency on the line Yaw MCP grades with, at --strict --min-grade A, rewriting compliance-badge.svg from the run. An unreachable GitHub API, a missing oam, a missing mcp-compliance install and skipped compliance tests each print a warning rather than passing silently. The offline half of the floor check also runs in the test suite (src/__tests__/oam-floor.test.ts). New scripts: check:oam-floor, verify:oam-floor, compliance.
  • The compliance badge is regraded with mcp-compliance 0.20.4 against the current seven tools: grade A (100%), 45/45 passed, 5 capability-gated tests skipped. The previous badge dated from 2026-04-16, before ctxlint_skill_audit existed.

v0.27.6

Choose a tag to compare

@jeffyaw jeffyaw released this 07 Oct 14:58
v0.27.6
ae8536e

Fixed

  • HTML/XML comment stripping no longer mis-handles a malformed nested comment such as <!<!-- x -->-- y. Context-file line classification blanks complete <!-- ... --> pairs with length-preserving filler instead of deleting them before testing for an unclosed opener, so deletion can no longer splice <! and -- into a <!-- that was never written and latch the rest of the file as comment. The Maven POM reader (maven-modules) strips comments to a fixed point, so a comment re-formed by one removal pass is not read as live <module> entries, and treats a POM with an unclosed <!-- as unreadable instead of reading the elements after it. stripInlineHtmlComments also loops to a fixed point; its output is unchanged, since its filler cannot form a new pair. Resolves CodeQL js/incomplete-multi-character-sanitization alerts #4, #5 and #6 (ctxlint renders no HTML, so these were correctness edge cases, not injection).

v0.27.5

Choose a tag to compare

@jeffyaw jeffyaw released this 07 Oct 12:53
v0.27.5
4c23cec

Internal

  • The Windows 8.3 short-path probe in the findRenames tests now reads the temp-dir path with cmd delayed expansion (cmd /d /v:on, !VAR!) instead of %VAR%. cmd substitutes %VAR% before it parses the line, so moving the path into an environment variable (#89) kept the command text constant but still let a & or ) in the path act as a command operator; a !VAR! value is substituted after parsing and stays data (CodeQL js/shell-command-injection-from-environment, alert #3). /d also skips any registry AutoRun command. Test-only; the CLI and server are unchanged.

v0.27.4

Choose a tag to compare

@jeffyaw jeffyaw released this 06 Oct 21:23
v0.27.4
70b736b

Security

Changed

  • git now runs without inherited GIT_* environment variables. simple-git 4 strips every inherited GIT_* variable (and EDITOR, VISUAL, PAGER, SSH_ASKPASS) before it spawns git, and ctxlint accepts that rather than allow-listing any back. A repository reached only through GIT_DIR/GIT_WORK_TREE, or one that needs GIT_CONFIG_COUNT/KEY/VALUE (a common way to set safe.directory in containers), is no longer seen as a git repository: the staleness and rename checks skip, as they do outside any repository. Run ctxlint from inside the work tree, or set safe.directory in a git config file, instead. In return, a parent repository's GIT_DIR no longer leaks into ctxlint's lookups when it runs from a git hook.
  • The oam floor moves from 0.15.2 to 0.18.0, so the ctxlint launcher no longer runs the CLI on an older oam. ctxlint is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.18.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS, through the launcher with OAM_BIN pointing at it and CTXLINT_RUNTIME=oam: ctxlint serve completes a full MCP handshake listing all 7 tools (the same 7 as on Node), ctxlint_audit on this repo answers identically to Node apart from its scannedAt timestamp (its git-backed checks run git through simple-git 4.0.2 on oam; the rename check answers identically to Node), and the server process is that binary (oam-aarch64-pc-windows-msvc.exe run .../dist/index.js -- serve as the launcher's child). A host that launches oam run bin/ctxlint.mjs on 0.18.0 runs the CLI in-process, as before. If the launcher finds only an oam from 0.15.2 to 0.17.x, it now falls back to Node under the default CTXLINT_RUNTIME=auto, and exits with an error under CTXLINT_RUNTIME=oam -- it says so on stderr, naming the version it found and the floor (measured with oam 0.17.0: is oam 0.17.0, older than 0.18.0; using Node instead.); an oam 0.17.x host running the launcher hands the CLI off to Node the same way. On Node every check and every MCP tool answers the same, so nothing is lost but the runtime. A client that runs oam run /path/to/ctxlint/dist/index.js directly bypasses the launcher and keeps the oam it names. Run oam self-update, or set CTXLINT_RUNTIME=node to make the choice explicit.

Internal

  • release.sh waits up to 600 s, not 120, for npm to serve a new version before the MCP Registry step, and polls npm's uncached per-version document up to 120 times 5 s apart in its final check instead of reading the CDN-cached npm view once after 3 s. On 2026-09-29 the @yawlabs/fetch-mcp 0.8.2 release spent 295 s of its 300 s gate waiting for npm to serve the new version. Release tooling only; the server itself is unchanged.
  • release.sh runs every mcp-publisher call to the MCP Registry, each login and each publish attempt, under coreutils timeout (MCP_PUBLISH_TIMEOUT_S, default 90 s) where one (or Homebrew's gtimeout) is on PATH -- without one the call runs unbounded, and a warning says so -- because mcp-publisher waits for the registry's answer with no limit of its own and a registry that never answered would have hung the release. A publish attempt that gets no answer is retried on the same 30, 60 and 90 s clock as the registry's own 429-504: one the limit stopped, which used to hang the step, and one whose connection failed or dropped (the client's error sending request or error reading response), which used to fail it at once. A login the limit stopped says the registry did not answer; one that failed any other way says how to read mcp-publisher's output (a 401 is the registry refusing the token exchange; a 429, a 5xx or a connection error is the registry or the network) instead of naming token scopes, which never fail a login: the registry reads the org role at login and refuses only at publish. A publish refused with a 403 says what the io.github.YawLabs namespace takes: a YawLabs org Owner whose token can read org roles. Release tooling only; the server itself is unchanged.

v0.27.3

Choose a tag to compare

@jeffyaw jeffyaw released this 29 Sep 20:09
v0.27.3
f35c20f

Internal

  • release.sh reads npm's per-version document wherever it asks whether this version is published, and a resume no longer dies on npm's E403. Step 6's "already published -- skipping" check used npm view, which reads the whole packument that registry.npmjs.org serves from Cloudflare's edge for up to 300s, so a re-run right after a later step failed could publish again and stop on npm's "cannot publish over the previously published versions" E403 as if it were a token problem. That check now goes through npm_version_live (the uncached per-version document the MCP Registry itself fetches, now also cache-busted and capped at 20s), and the workstation publish loop treats that E403 as the skip it should have been, with a warning in place of the success line. Step 8's MCP Registry retry now also waits out the live registry's (v1.8.1) wording for trouble on npm's side -- "Likely transient, retry later" (429, 5xx, an inconclusive 404) and "failed to fetch package metadata from NPM" -- and its not-found shape needs not found (status: N) with the version in the same output, so a package missing outright still fails on the first attempt. It also waits out the registry's own HTTP 429, 502, 503 and 504, and logs in again before the first attempt and every retry, since its login comes before the npm gate and registry tokens last 5 minutes.

Documentation

  • The README's Add to Yaw MCP button now sits directly under the title, and the follow badge links to @YawLabs. The one-click install button and its one-line caption moved up from further down the page, so they are the first thing under the name on npm and GitHub, and the X badge at the bottom points at @YawLabs instead of @TokenLimitNews, and the Token Limit News link is gone. npm shows the README from the published package, which is why it takes a release to carry this there; the package's code is unchanged from 0.27.2.

v0.27.2

Choose a tag to compare

@jeffyaw jeffyaw released this 15 Sep 23:41
v0.27.2
cb83548

Security

  • The MCP SDK devDependency moves from ^1.29.0 to ^1.30.0, and pnpm audit goes from 28 findings (10 high, 17 moderate, 1 low) to 0. Every finding was development-scope, and all but the direct vitest devDependency were transitive, through the SDK or the build/test toolchain: fast-uri 3.1.2 -> 3.1.7, hono 4.12.25 -> 4.13.7, @hono/node-server 1.19.14 -> 1.19.17, ip-address 10.2.0 -> 10.7.0, qs 6.15.2 -> 6.16.0, postcss 8.5.19 -> 8.5.28, nanoid 3.3.16 -> 3.3.19, the lockfile's brace-expansion 5.0.7 -> 5.0.12, and vitest / @vitest/coverage-v8 / @vitest/mocker 4.1.9 -> 4.1.11 (Dependabot auto-dismissed seven of its alerts -- hono GHSA-8j4g and GHSA-g6gw, qs x2, nanoid and brace-expansion x2 -- so those showed up only in pnpm audit). The pnpm.overrides floors are raised to each advisory's first patched version (hono ^4.13.5, @hono/node-server ^1.19.15, fast-uri ^3.1.6, ip-address ^10.3.1, qs ^6.16.0, postcss ^8.5.23, brace-expansion ^5.0.9) so a lockfile refresh cannot slide back into the advised ranges; with pnpm, raising the SDK floor alone re-resolves nothing transitive. Of the alerted packages, only fast-uri (via the SDK's ajv validator) was inlined into the published 0.27.1 dist/index.js: that bundle carried 3.1.2, inside the advised range of six high-severity host-confusion and SSRF advisories, and the bundle now carries 3.1.7. ajv resolves only ctxlint's own tool schemas, so the vulnerable path was never reachable from user input, but the shipped copy is updated regardless. The lockfile's brace-expansion is inlined as well now, through the glob/raw change in the next entry; none of the others is in the bundle.
  • dist/index.js now bundles the lockfile's patched brace-expansion instead of the pre-5.0.6 copy vendored inside glob. glob's default entry point (since 13.0.2) is a self-contained index.min.js with its own minimatch and brace-expansion inlined, and the 0.27.1 bundle shipped that copy: its numeric-range loop has no max cap, so it is open to GHSA-jxxr-4gwj-5jf2 and the later brace-expansion DoS advisories. No pnpm.overrides floor can reach code vendored that way, so the 0.15.1 entry below was not accurate when it said the >=5.0.6 override fixed "the bundled copy": the override only moved the lockfile's copy, which the bundle never used. src/core/scanner.ts and src/core/checks/paths.ts now import from glob/raw, glob's unbundled entry that depends on minimatch as a normal package, so esbuild inlines the lockfile's minimatch 10.2.5 and brace-expansion 5.0.12, with the max, maxLength, maxDepth and maxRewrites caps. Same glob version and API; dist/index.js grows by about 107 KB against published 0.27.1, about 94 KB of it from this change, because the unminified modules replace glob's minified one. Neither Dependabot nor pnpm audit sees vendored code, so this was found by reading the published bundle.
  • fixtures/masked-exit-status/package.json moves its vitest range from ^2.0.0 to ^4.1.11, closing the one Dependabot alert (GHSA-5xrq-8626-4rwp) raised against a manifest that is never installed: the fixture is linter input for the commands/exit-status-masked test, has no lockfile, is not a workspace, and is excluded from the npm tarball. The test reads only dependency key names, so the value is inert.
  • Pre-commit users pin the version through .pre-commit-hooks.yaml (npx @yawlabs/ctxlint@<version>) and need to bump rev to pick up the patched bundle. GitHub Action users on the default version: latest, npx users and MCP Registry clients get it on their next resolve.

v0.27.1

Choose a tag to compare

@jeffyaw jeffyaw released this 14 Sep 14:23
v0.27.1
529d09c

Changed

  • npm and MCP Registry listing metadata: bugs URL, core keywords, and server.json title/repository/websiteUrl
  • release.sh writes a ## [x.y.z] entry for every release, and the GitHub release notes come from it. Step 4 promotes [Unreleased] when it has content, leaving a fresh, empty [Unreleased] heading above the new section for the next change, and otherwise generates the section from the commit subjects since the previous tag; Keep-a-Changelog link references, when the file has them, are moved along with it. Step 7 then takes the release notes from that entry instead of from git log subjects, and its backstop fails on any missing entry rather than only when [Unreleased] had content. Before this, a release with nothing under [Unreleased] got no entry at all -- the [0.26.0] section below was backfilled by hand in #75 -- and its release page listed raw commit subjects, its own version-bump commit included, which is how v0.26.0's was published. Promoting also consumed the [Unreleased] heading, so the next change had to add it back (#76).

Internal

  • release.sh gives npm propagation twice the time before the MCP Registry publish. Step 8 waits for npm to serve the new version before registering it, then retries the registry publish when it answers that the version is not on npm yet. The v0.27.0 release exhausted the 60s wait and succeeded only on the last of three retries, about 150s after the publish, while v0.25.4 the same day cleared the wait in under 60s. The wait is now 120s and there are four retries (30s, 60s, 90s apart), 300s in all; the extra time elapses only on a run that is still propagating. Both loops were exercised against a stubbed mcp-publisher: a publish that fails three times still succeeds on the fourth attempt, and one that fails four times exits through the existing failure message that names what has already landed.

v0.27.0

Choose a tag to compare

@jeffyaw jeffyaw released this 14 Sep 04:18
v0.27.0
89721da

Fixed

  • Glob-scoped .ctxlintignore rules show up in the "Ignore rules" report when they drop nothing. A rule with a file glob (tokens sub/*.md) counted as fired as soon as its glob matched any file, whether or not it dropped a finding, so a rule that suppressed nothing was never listed as drift -- unlike every other ignore rule, which counts as fired only when it drops something. And when the only rules loaded were glob-scoped and none dropped anything, the report was skipped entirely, hiding a rule whose glob matched no file and any rule missing a reason. A glob-scoped rule now counts as fired only when it drops a finding, and the report is built whenever any rule is loaded. Glob-scoped rules are applied one at a time in file order, so the first matching rule still wins and is the one credited. As a result, JSON _meta.ignoreReport and the text footer now also appear for a run whose only rules are glob-scoped.

Internal

  • README: .ctxlintignore was named in the Options block and the exclude prose, but its format was documented nowhere. A new "Ignore file" section covers the checkName [fileGlob] [# reason] line format, what the glob is matched against, the space required before #, rule order relative to ignoreRules, the shared report, --no-ignore-file and --watch. ignore-file.test.ts (new) covers the parser and loads the README example expecting real check names and a reason on every line; audit.test.ts gains the first end-to-end .ctxlintignore tests, including both reporting fixes above.