chore: version packages - #74
Open
github-actions[bot] wants to merge 1 commit into
Open
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
7 times, most recently
from
August 4, 2026 15:08
6083aac to
28c11bf
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
15 times, most recently
from
August 12, 2026 02:45
8d07d51 to
850c944
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
8 times, most recently
from
August 20, 2026 22:57
b263ba8 to
60a7925
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
August 21, 2026 02:52
6941b6e to
fd18682
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
August 21, 2026 17:22
ef2479b to
0a7bbe9
Compare
Closed
13 tasks
github-actions
Bot
force-pushed
the
changeset-release/main
branch
16 times, most recently
from
August 24, 2026 21:37
d400ea0 to
90d6cd4
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 24, 2026 22:12
90d6cd4 to
e788c93
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@taskless/cli@0.11.0
Minor Changes
f7ee186: Partition
.taskless/by rule engine. Migration0004moves ast-grep rules tosg/rules/andsg/rule-tests/, the runtime tree toruntime/rules/andruntime/rule-tests/, and scaffolds an inertvale/. Files move byte-for-byte, so runtime rule signatures survive.The directory a rule sits in now is its engine: dispatch reads the path and never parses a rule file to decide who owns it.
checkruns ast-grep against the committed.taskless/sg/sgconfig.ymlinstead of generating an ephemeral config each run.A rule engine the CLI does not recognize is now rejected with a message instead of failing silently: an unsupported engine from the server previously exited 0 with no output, which read as success.
Runtime rules are discovered under
runtime/rules/rather than the pre-migrationruntime-rules/. Migration0004moves that tree byte-for-byte, so the signatures the server validates are unchanged.checkandrule verifyread the committed.taskless/sg/sgconfig.ymlrather than writing an ephemeral config on every run, so the config ast-grep uses is the one you can edit and review. A pre-migration rule set still gets a generated config, so an unmigrated project keeps running.Existing projects keep working without action. The pre-
0004.taskless/rules/still runs as ast-grep, and a delivered rule that names no engine is still treated as ast-grep — a rule engine this CLI does not recognize is rejected rather than guessed at. A migration that would have to merge a file into an engine directory now refuses up front withSCAFFOLD_CONFLICTrather than failing part-way.d4fca88: Add a
@taskless/cli/promptssubpath export exposing the CLI's knowledge prompts as importable, topic-keyed render functions.getPrompt(topic, options?)and thePROMPTSmap return fully rendered recipe text, with every%(KEY)splaceholder already resolved from values the package holds, so a consumer never handles a template dialect. Topic names are typed asPromptTopicand start atstatic, the one recipe a service-side consumer can act on; everything else stays internal until a consumer needs it.PromptOptionscovers the anonymous variant, apackageManagerDlxoverride, andheader: falsefor callers placing the text in an LLM system prompt, where the CLI version in the header would otherwise churn the prompt-cache key on every publish.The export is sourced from the same embedded recipes and the same render path
taskless help <topic>serves, so the two surfaces cannot drift, and it carries no CLI runtime, so a Worker can import it without pulling in the command tree.8c91857: Name the CLI by its full invocation everywhere an agent is told to run it.
Agent recipes said
taskless agent route— a binary almost nobody has onPATH— in 114 places,npx @taskless/cli …in 40 more, and only the second form was rewritten for non-prod builds. A nightly's recipes therefore sent readers to the released package. All of it now renders through one new sprintf variable,%(TASKLESS_CLI)s, which resolves to a caller-supplied invocation, else the build's own invocation when that build is not prod, else the agent-fill marker<taskless-cli>.@taskless/cli/promptsgainsgetInstructions(topic, options?)andgetRawInstructions(topic, options?), both returning{ text, variables }. The raw form hands back the unrendered template and the list of variables it contains, so a host that knows its own launcher can render the text itself;variablescomes from sprintf-js's own parse rather than a regex over the template.PromptOptions.invocationis the only way a consumer setsTASKLESS_CLI— the render path stays free ofprocessso it remains importable from a Worker.Fixes launcher detection in user-facing error messages.
getCliPrefix()read onlynpm_config_user_agent, which every pnpm entry point sets, so running the CLI from apackage.jsonscript told the user to runpnpm dlx @taskless/cli@latest. Detection now reads the path the binary was launched from, recognizes npx andpnpm dlxonly, and answers "unknown" for everything else. The package specifier comes from the build target, so a nightly's error messages name@taskless/cli-nightlyat its own version.6b07695: Ship Vale as per-platform binary packages.
The CLI now declares
@taskless/vale-<os>-<cpu>asoptionalDependenciespinnedto an exact version, so installing it also brings down a verified Vale binary for
the host platform — no lifecycle script, and nothing to download at runtime. Only
the matching platform installs; unsupported hosts install cleanly with none
present and continue to fall back to a
valefound onPATH.0e03ee9: Add Vale as a second static-tier rule engine, give every engine one rule layout, and rename the agent-facing command.
checknow dispatches by engine and runs ast-grep, Vale, and runtime rulesconcurrently, merging their findings into one result set. An unavailable Vale
reports itself and the other engines still return. A Vale that times out or
rejects its config fails the check rather than passing as a clean run.
Every rule is now one directory,
.taskless/rules/<engine>/<id>/, holdingthe rule, any per-engine config, and its tests in
.tests/. Writing a rulemeans creating a directory and deleting one means
rm -rf. Nothing outside itis touched either way, so concurrent authors never collide on a shared file.
Vale rules carry their own
.vale.inideclaring which files they apply to.The single config Vale reads is assembled from those per-rule files on each
run, gitignored, and regenerated, so hand edits to it have no effect. ast-grep
keeps its
files/ignoresinside the rule and needs no second file.rule verifyis replaced by two path-addressed commands.verify <path>checks that a rule has the components its engine requires and needs no tests,
so it works while you're still authoring.
test <path>runs the rule's tests,after running
verifyand stopping if that fails. Both take a rule directory,an engine directory, or nothing at all for the whole project, and both report
one result per rule. Addressing by path rather than id removes the ambiguity
that arose when two engines held the same rule id.
Projects on an older layout migrate automatically on the next command.
BREAKING:
taskless help <topic>is nowtaskless agent <topic>. Thecommand is named for who reads it. Agents fetching a procedure are not asking
for help, and the old name is gone rather than aliased.
BREAKING: topics are addressed by a single token.
taskless help rule createbecomestaskless agent create-sg-rule; multiple positionals are nolonger joined into a topic key. A topic name is now a literal string an agent
copies rather than a phrase it can reorder. The renames:
rule createcreate-sg-rule/create-remote-rulerule improveimprove-rulerule deletedelete-rulerule verifyverify-rulerule metarule-metastaticcreate-sg-ruleexistingcreate-legacy-ruleengine-selectionrouteroutenow applies the engine reasoning itself and names a concretecreate-*-ruletopic, soengine-selectionis removed rather than renamed —its criterion is stated once, in
route. Every authoring recipe is rewrittenfor the rule-directory layout.
BREAKING for
@taskless/cli/promptsconsumers.engine-selectionis nolonger exported.
TOPICSis nowcreate-sg-rule,create-vale-rule, andcreate-runtime-rule, so a consumer that decides an engine can reach theprocedure for each destination. Because the export is a string union, a
consumer passing the removed name dynamically breaks on upgrade rather than at
build time.
Patch Changes
87abaf3: Fix the pass/fail counts reported when a rule's
ast-greptests fail.ast-grep testechoes the source of a failing test case, andverifyscrapedits counts with unanchored regexes over stdout and stderr combined — so a
fixture containing text like
'7 passed; 0 failed'was read as the summary andverifyreported✗ failed (7 passed, 0 failed)for a run that actually had 0passed and 1 failed. The counts are now read from the summary line itself
(
test result: ok./Error: test failed.), with ANSI colors stripped first.This only affected the reported numbers, never the pass/fail verdict, which
comes from the exit code — but those numbers are handed to the agent driving
improve-rule, where a wrong count can steer the next edit. Test output is alsonow decoded with a
StringDecoderper stream, so a multi-byte character splitacross a chunk boundary is no longer mangled.
1fb9dda: Rewrite the CLI README around what you actually do with Taskless: installing it,
driving it from your coding agent with the
tasklessskill and/tsklcommand,running
taskless checkin CI, and where to find the docs. Telemetry — and thetwo environment variables that turn it off — is now stated plainly instead of
being left to the source.
f13d501: Stop corrupting non-ASCII characters in ast-grep's error output.
runAstGrepScanand the runtime narrow both decoded ast-grep's stderr onechunk at a time with
chunk.toString(). A multi-byte UTF-8 sequence splitacross a chunk boundary was decoded as two invalid sequences, and both halves
became replacement characters before the pieces were joined — the original
bytes unrecoverable by then. Each stream now uses a single
StringDecoder,flushed on close, matching what the Vale runner and
verifyalready do.The corrupted text only ever reached an error message, so no scan result was
ever wrong. But that message is the one a user reads when ast-grep rejects a
rule file, naming a rule id or a path — which is exactly where a non-ASCII
character turns up.
71f4394: Tell the routing recipe what the local engines can actually read.
routechose betweensg,vale, and the runtime tier on the shape of theevidence alone, and had nothing to say about language reach — so a rule over a
GitHub Actions workflow was escalated to
create-runtime-rule, which needs alogin, because nothing stated that ast-grep parses YAML. It does. Nothing in
the repository could have said so either: the vendored ast-grep schema types
languageas a bare string with no enum,verifynever checks the field, anddetect --jsonreports the repo's own languages in a different vocabulary.Vale self-reports nothing at all.
routenow states both engines' reach, andcreate-vale-rulerepeats Vale'swhere a matcher is written. Both read the lists from constants pinned to the
engine versions this CLI ships, rather than from prose typed into the recipe —
an engine bump that changes what a binary parses now fails a vendor-contract
test instead of leaving a confident, wrong sentence in front of an agent.
Vale's reach was measured by probing the shipped binary, tier by tier, since it
publishes no capability listing.
The Vale half carries a hazard worth naming on its own. Vale supports
reStructuredText, AsciiDoc, XML, DITA, and MDX by shelling out to an external
converter, and this CLI ships none of them — so one such file caught by a
rule's glob exits 2 with an
E100and abandons the whole run, silencing everyother Vale rule over every other file.
create-vale-rulehad been offering[*.{md,mdx}]as its example of widening a matcher..xmlis the one entry where naming the converter is not enough. It needsxsltprocand an XSLT stylesheet, and a stylesheet is document-specific, sothere is nothing to ship and installing the program does not make
.xmllintable — unlike
asciidoctor, which genuinely fixes.adoc. Vale says sodifferently depending on the host, too:
xsltproc not foundwhere the program isabsent,
no XSLT transform providedwhere it is present, and macOS ships/usr/bin/xsltprocwhile a typical Linux CI image does not. The contract testnow asserts Vale's checker tag, which is the same everywhere, rather than a
substring of the converter name.
87392fa: Make
--helpwork on every command, instead of running the command.taskless check --helpprinted no usage — it rancheck. So did every othersubcommand:
--helpwas parsed as an unrecognized flag and the command bodyexecuted anyway, which meant asking
inithow it works installed skills, andasking
checkhow it works migrated the.taskless/scaffold. The only placehelp worked was the bare
taskless --help, whose own output tells you to runtaskless <command> --help.--helpand-hare now recognized at every depth, including nested commands(
taskless auth login --helpdescribeslogin, notauth), and a workingdirectory passed before the command (
taskless -d ./repo check --help) nolonger confuses which command you asked about. The usage text itself is
unchanged, and nothing else about how commands run has changed.
32da4f9: Stop the engine-partition migration from relocating a rules tree that is already partitioned.
A
.taskless/with notaskless.json— a manifest that was never committed, or was deleted — reads as version 0, so every migration runs against it. Migration0004then applied itsrules/→sg/rules/move to a tree already in the current layout, burying every rule at.taskless/sg/rules/sg/<id>/;0005scaffolded fresh empty engine directories over the gap. Nothing errored.checkscanned a tree with no rules in it and exited 0 on a clean report, so a project that had silently stopped being checked was indistinguishable from one that passes.0004now reads the shape of.taskless/rules/before moving it. A tree holding engine directories and no loose rule files is newer than the migration, not older, so it is left alone. A genuinely pre-0004tree of flatrules/<id>.ymlfiles still moves wholesale, as before. And a tree holding both — an already-partitioned layout with a strayrules/<id>.ymlbeside it, as a merge-conflict leftover produces — migrates only the stray files: moving the directory to collect them would carry the partitioned rules down with it, and0005never brings them back, which is the same silent clean pass by another route.0cc713e: Split the release pipeline so each workflow file carries one release design.
release.ymlheld two jobs with opposite trust properties behind one header.It is now
release-cli-changeset.yml— which reads contributor-authoredchangesets and opens the Version Packages PR holding no npm credential and no
OIDC identity — and
release-cli.yml, which keeps the credential-free"is this version already on npm?" gate together with the publish job it
protects, so an OIDC-capable job is never instantiated on an ordinary merge.
vale-binaries.ymlis renamedrelease-vale.ymlto match.The build and publish steps themselves are unchanged — same triggers, same
permissions: {}, same action pins, same OIDC trusted publishing behind thesame
npm-productionapproval. Two operational details do differ:checkandpublishno longer share therelease-*concurrency group, and the releasenow runs as two workflow runs instead of one, so its check contexts are
Release CLI Version PR / …andRelease CLI / …rather thanRelease / ….Neither is a required check.
The header comments also get one correction: they claimed
npm-productionhadno required reviewers, and it has had one all along, so a release has always
waited for a human approval that the file said was not there.
Publish unreleased work on
mainas@taskless/cli-nightly.Every push to
mainthat has changesets pending now publishes the CLI under asecond package name, stamped
<next-version>-<yyyymmddhhmmss>x<short-sha>— somerged-but-unreleased behavior is installable with
npx @taskless/cli-nightly.A nightly is the same build as the release it anticipates and keeps the
tasklessexecutable, so it is a drop-in; the rename happens at pack time, so@taskless/cli's own version history stays releases-only. Installing bothglobally collides on the binary and is unsupported.
Two credential-free gates decide whether anything is built — pending changesets
first (before any install), then whether the commit already has a nightly — so
the publishing job is never instantiated on an ordinary push, and the merge of a
Version Packages PR publishes the real release and no nightly with no rule
special-casing it.
A nightly now ships instructions for itself. The skills, commands, and recipes
a nightly installs name
npx @taskless/cli-nightly@<version>— pinned to thebuild being installed — instead of
npx @taskless/cli. Previously a nightlycarried the released CLI's text verbatim, so an agent following it ran the
released binary: no error, just instructions for a different package, on a
build installed precisely to exercise unreleased behavior. The version is
stamped once and passed to both the build and the pack, so the version the
instructions name is always the version on npm, and a nightly build without a
valid version fails rather than falling back.
The nightly's duplicate-suppression gate also now fails closed. An unreadable
registry response used to read as "this commit has no nightly", and since each
build stamps a fresh timestamp, a re-run after one would have published a
second nightly for the same commit successfully and silently.
226061d: A nightly now reports the version it is, not the release it anticipates.
Installing a nightly wrote the previous release into
.taskless/taskless.json—
install.cliVersion: "0.10.2"— while the skills written beside it, by thesame command in the same run, pinned every invocation to
@taskless/cli-nightly@0.11.0-…. The manifest attributed the install to aversion that never performed it, which matters because
install.cliVersioniswhat answers "what installed this?", and that question gets asked precisely
when someone is running a nightly to reproduce unreleased behavior.
A nightly's version is stamped when the publishable artifact is produced, and
the committed
package.jsonis deliberately left untouched — so the build wasreading a file that could not know the answer. It now takes the same stamp that
names the published package, so the version a nightly reports and the version
it sends an agent to are the same string by construction.
This also corrects
taskless --version, the CLI version in recipe headers, andthe
cliVersiontelemetry property on nightly builds. Released builds areunaffected. A nightly that cannot determine its own version now fails the build
rather than quietly reporting the released one.
The build now also refuses to emit a nightly whose reported version and
embedded invocation disagree. Both derive from the same stamp, so they cannot
diverge today — but that was true of the two values in this bug as well, right
up until one of them started reading
package.jsoninstead. Deriving from onesource is not the same as being checked against it.
c4a252b: Onboarding now reads the routing surface before it proposes rule candidates.
The
onboardrecipe asked the agent to synthesize its bullet list ofhypothetical rules first and consult
routeonly afterwards, once per acceptedbullet. So the list a user picked from was written without knowing what kind of
rule anything would be, or what the repository already lints — and a candidate
with nowhere to go looked exactly like a good one until the user had already
chosen it.
The recipe now fetches
taskless agent routeand runstaskless detect --jsonbefore proposing anything, and each bullet carries the destination it would
route to:
- no-direct-db-access [sg]: …. The annotation is provisional —routestill decides for real at materialization time, when it has the rule'sfull description — but an unroutable candidate is now visible while it is still
cheap to drop.
The destination criterion itself has not moved. It is still defined once, in
route; onboarding reads it rather than carrying a copy that would drift.a7ec7a1: Complete the
help→agentrename. The user-facing command was renamed in0.10.0, but the internals kept the old name: the recipe directory moved from
packages/cli/src/help/topackages/cli/src/agent/, thecli-helpOpenSpeccapability is now
cli-agent, and the shipped skill and/tsklcommand nolonger tell agents to run the removed
npx @taskless/cli help <topic>(theynow use
agent, with the single-token topic names —route,improve-rule,delete-rule,create-sg-rule, and siblings).Telemetry rename (hard cut, no dual-emit). The
cli_helpevent is renamedto
cli_agent. Thetopicproperty is unchanged. PostHog dashboards keyed oncli_helpwill need updating — nothing is emitted under the old name.afb4831: Let ast-grep rules see inside hidden directories such as
.github/.ast-grep's file walker skips dot-directories unless told otherwise, and
runAstGrepScannever told it otherwise. Nosgrule could match anythingunder
.github/,.circleci/,.vscode/or.husky/, socheckreportednothing and exited 0 on a workflow file it flags correctly the moment the same
bytes live in a non-hidden directory. Vale has no such blind spot, which left
the two static engines disagreeing about whether
.github/existed at all.Both
checkand the runtime engine's ast-grep narrow now pass--no-ignore hidden.Only
hiddenis passed, and deliberately notvcs:.gitignoreis stillrespected, so the wider walk does not start reporting findings in
dist/oranywhere else a project has already said it does not want scanned. Rule
discovery is untouched —
ruleDirswalks by its own rules, so a rule's.tests/directory is still skipped rather than parsed as a rule..taskless/is excluded from the wider walk, because it is hidden too andreaching it is not a fix. A rule definition is structured YAML full of
id:,language:,severity:andrule:keys, so an ordinary user-written Yaml rulefires on the CLI's own rule files — a finding in a directory the user did not
author and cannot edit without disabling their rule. The exclusion applies only
when
checkwalks the whole project on its own; an explicit path stays arequest, which is the rule the Vale runner already follows.
.git/is excluded on the same terms. ast-grep has no exclusion of its own forit and
.gitignoredoes not list it, so the default hidden-directory skip wasthe only thing holding it back: without this, a whole-project
checkdescendedinto
.git/objectsand.git/logson every run, and.git/hooks/*scriptsmatched language rules never meant to lint VCS internals.
Both engines now decide "whole project" the same way, and it is no longer
paths.length === 0. An explicit.is normalized to the literal path"."before it reaches either runner, so a length test read the most ordinary way of
asking for a whole-project check as a user-named path and skipped the exclusions
—
checkwas clean whilecheck .reported findings inside.taskless/. Valewas already wrong in the same way and for the same reason, independently of the
hidden-directory change, so the predicate is now shared rather than written
twice.
73cdc45: Fail
testfor an ast-grep rule that never demonstrates it can fire.verifychecked that a rule's-test.ymlexisted and never read what was init, and
ast-grep testreports an emptyinvalid:bucket as1 passed; 0 failedand exits zero. A rule whose fixtures were allvalid:thereforereported
ok: true, ran: truewhilecheckfound nothing anywhere — verifiedlooking verified, having proved nothing.
testnow counts thevalid:andinvalid:entries across every test file a rule owns and requires both, whichis the rule Vale fixtures have always been held to.
This rejects rules that passed before. Any sg rule with an empty or absent
invalid:bucket now failstestuntil a fixture is added that the ruleactually matches. That is the intended effect: adding one is how the underlying
mistake surfaces.
The mistake that prompted this is worth knowing about, because the pattern
looks correct. A trailing
$$$next to a comma does not mean "zero or more" —the comma is itself an AST node, and under ast-grep's default
smartstrictness every node in the pattern must match, so
fetch($URL, $$$REST)never matches
fetch(url)and silently starts at two arguments. A leading$$$is worse:foo($$$, $A)collapses to exactly one argument. Upstreamconsiders this intended and 0.45.2 behaves identically, so there is no version
to upgrade to; write the pattern as an object with
strictness: astto ignorethe separator, or use
any:with one branch per arity.verify --schemanowcarries a worked example, and the behaviour is pinned against the vendored
binary so a bump that changes it fails loudly.
create-sg-rulestates all of this where a pattern is written: the arity tablemeasured against the pinned binary, both remedies and the fact that
strictness: astmoves a trailing$$$from two arguments to one rather thanto zero, and the fixture requirement with a case on each side of an arity
boundary. It also names ast-grep's
language:vocabulary from the same pinnedconstants — nothing local validates that field, an unrecognized spelling takes
the whole scan down, and
Tsxis a different parser fromTypeScriptratherthan an alias.
improve-rulegains the two notes that matter when a rule isrewritten rather than written: read the pattern for a comma-adjacent
$$$before reporting it as too narrow, and re-check both fixture buckets after the
service returns a narrowed rule.
9e87aa6: Stop a rule with no tests from failing every other rule's ast-grep test run.
Migration
0005created a rule's.tests/only as a side effect of moving a test file into it, so an ast-grep rule that had no test at version 3 — or one whose test file did not match the<id>-YYYYMMDD-test.ymlshape the migration can attribute to a rule — arrived in the new layout with no tests directory at all. Assembly then named that directory as atestConfigsentry anyway, and ast-grep 0.41.0 treats atestDirit cannot read as fatal to the whole invocation rather than to the one rule:taskless teston any rule died withCannot read rule directory .taskless/rules/sg/<other-id>/.testsand exit 6, naming a rule the author had never touched.--filterdoes not scope that away, so there was no way to run one rule's tests around it.0005now gives everyrules/sg/<id>/a.tests/, holding a committed.gitkeepwhen it would otherwise be empty — git does not track empty directories, so without one the repair would not survive a commit and the failure would come back in CI. Assembly separately omits anytestDirthat is not on disk, which is what rescues a project a nightly already stamped at version 5: migrations short-circuit once the manifest is at the latest version, so those installs never re-run the amended0005, and the same state is reachable at any version by creating a rule directory by hand. Neither change turns a missing test into a pass —verifystill reports "No test file found" andteststill reports "Skipped: no test file found", both reading the rule directory rather than the generated config.4960987: Stop one AsciiDoc file from disabling every Vale rule in the project.
Vale supports AsciiDoc, reStructuredText, XML/DITA and MDX, but it parses none
of them by itself — it shells out to
asciidoctor,rst2html,ditaormdx2vast, and the@taskless/vale-*packages ship the binary with none ofthose alongside it. On a host without the converter Vale does not skip the file:
it prints one
E100 [lintAdoc] Runtime erroron stderr, writes nothing at all tostdout, and exits 2. The abort is Vale's own and it is not scoped to the file
that caused it, so every finding from every other file in the run was destroyed
before it was ever serialized. Measured against the example project, adding a
single
.adoctook a check that reported five Vale findings across four filesdown to zero — reported as a raw JSON blob among the results, and exiting 1 the
same way any ordinary failing check does.
runValenow excludes the converter-dependent extensions from Vale's own walk,so the rest of the project is checked normally and the skipped files are named
in a notice that says which converter would put them back in scope. The tiers
live in one table in
rules/capabilities.ts— the same record the agent recipesrender their format lists from — measured against the pinned binary rather than
transcribed from documentation. That is how
.ascand.rest, a third AsciiDocspelling and a second reStructuredText one that crash identically and were in
neither bug report, ended up covered. Measurement also corrected four
extensions that a documentation reading had put in the wrong tier:
.tex,.rmd,.mkdand.mkdnare all read as plain text by this Vale, not parsed,so excluding them would have dropped files Vale lints perfectly well. A
per-extension test re-measures every row against the real Vale — each tier by
the property only that tier has, since ordinary prose fires in all of them — so
a version bump that moves a format between tiers fails there instead of silently
turning the engine off again.
The engine moves to Vale 3.18.0 in the same release, and the table carries a
standing instruction to re-measure every row on a version bump — so every row
was re-probed against the new binary rather than carried over. Eight moved.
.mdxgains a native parser and leaves the unsupported tier, so a matcher like[*.{md,mdx}]— the worked example above — is legitimate again, and[*.{md,typ}]takes its place as the broken one.
.typmoved the opposite way: Typst nowparses through
typst2vast, an external program this build does not ship, so aTypst file is excluded from the run rather than read as prose the way 3.17.1
read it.
.rmdand the new.qmdand.mystare parsed as markup, and.qml,.scssand the new.qdocare comment-aware where they previously fell throughto plain text.
The
.typmove is the one that mattered to get right. An extension missing fromthe table is read as prose, which is harmless — but the moment Vale routes it to
a converter, that same omission is a crash that takes down every Vale rule in
the run. Bumping the binary without re-measuring would have introduced exactly
the failure this table exists to prevent, under an extension nobody was
watching. Re-probing also caught one change the release notes do not mention:
PHP comment extraction now requires a real
<?phptag, where 3.17.1 linted abare
//comment without one.Two details are load-bearing and were both wrong on the first attempt. Vale
honours exactly one
--globand keeps the last, so the.taskless/exclusionand the format exclusions have to travel as one negated alternation or the first
is silently discarded. And Vale matches a
--globagainst the basename onlywhen the pattern contains no
/— combined with.taskless/**the wholeexpression goes path-wise, at which point a bare
*.adocstops matchingdocs/guide.adocand the crash survives one directory down from wherever it wastested. Vale's error output is also decoded now rather than forwarded verbatim,
so a failure reads as a sentence naming the missing program instead of a
five-field JSON object.
These formats are now stated as unsupported rather than as needing a tool.
The notice used to end "Install it and put it on your PATH to have these files
checked", which offered a path this build does not ship, does not test, and for
.xmlcannot deliver — an XSLT stylesheet is specific to the document, so noinstall makes it lintable. It also made behaviour host-dependent: macOS ships
/usr/bin/xsltprocand typical Linux CI images do not, so the same repositorychecked differently depending on the machine. The exclusion is unconditional
for that reason, and the programs are still named as the reason rather than as
a remedy.
The comment tier was reconciled against Vale's own documentation at
docs.vale.sh/formats/code, which adds
.bsh,.csx,.pod,.py3and.sbtonce measured. It also documents
.pyi,.qmland.scssas comment-aware,and on 3.17.1 a bare non-comment line in each of them lints. 3.18.0 makes the
claim true for
.qmland.scssand still not for.pyi, which stays in theplaintext tier. That divergence is the argument for probing rather than
transcribing: the docs describe whatever Vale is current, and copying the list
would have shipped
.pyias comment-aware and been wrong for both builds..podis a reminder of how easily this is misread — it lints Perlcomments but not POD blocks, so probing it with
=head1looks like no supportat all.
db8adfa: Resolve the ast-grep binary without relying on an install-time step, and drop
the
@ast-grep/cliwrapper from what consumers install.devDependencies. The seven@ast-grep/cli-<platform>packages were already declared in
optionalDependencies, and the CLI alreadyresolved them by path — the wrapper was a leftover whose only job is a
postinstallthat hardlinks the binary into itself so itsbinentries work.Nothing here invoked those entries. Consumers now install only the platform
package matching their host, and the wrapper's
postinstall— which leaves aplaceholder text file where the binary should be under
pnpm dlx's strictisolation — is out of the shipped product entirely. It stays as a
devDependencybecausefetch-ast-grep-schemareads its version.0.41.0. They were carets, and thewrapper had been enforcing alignment implicitly by pinning its own
optionalDependencies; without it, two hosts could resolve different ast-grepversions against the same rules and disagree about findings. Held at
0.41.0rather than taking upstream's
0.45.0, so this change stays structural.the platform package,
node_modules/.bin, thensgandast-greponPATH,and throws naming what it tried. Previously it returned a bare
"sg"and letspawn'sENOENTbe the error, from a caller that could not say where it hadlooked.
Alpine improves as a side effect: upstream publishes no musl build and marks its
Linux packages
libc: ["glibc"], so today the wrapper'spostinstallresolves apackage that does not exist and exits 1, failing the install wherever dependency
scripts run. Installing now succeeds and resolution falls through to
PATH.Build Info
npx @taskless/cli-nightly@0.11.0-20260824221400x9cd9d99Built from: 9cd9d99
Built at: 2026-08-24 22:14:00