diff --git a/.changeset/anchor-sg-test-summary.md b/.changeset/anchor-sg-test-summary.md deleted file mode 100644 index 0f910986..00000000 --- a/.changeset/anchor-sg-test-summary.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Fix the pass/fail counts reported when a rule's `ast-grep` tests fail. - -`ast-grep test` echoes the source of a failing test case, and `verify` scraped -its counts with unanchored regexes over stdout and stderr combined — so a -fixture containing text like `'7 passed; 0 failed'` was read as the summary and -`verify` reported `✗ failed (7 passed, 0 failed)` for a run that actually had 0 -passed 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 also -now decoded with a `StringDecoder` per stream, so a multi-byte character split -across a chunk boundary is no longer mangled. diff --git a/.changeset/ast-grep-0-45-2.md b/.changeset/ast-grep-0-45-2.md deleted file mode 100644 index 71748330..00000000 --- a/.changeset/ast-grep-0-45-2.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Upgrade the vendored ast-grep from 0.41.0 to 0.45.2, and add `Markdown` and -`Dart` to the languages `sg` rules can target. - -**One breaking change reaches user rules.** A `rewriters:` entry now requires a -`fix:`. It was optional in 0.41.0, and the regenerated rule schema makes -`verify` name the offending rewriter directly. It cannot be migrated for you: -`fix` is replacement text, so a tool can find every affected rewriter but -cannot write one. Nothing shipped here uses `rewriters:`, so this only affects -rules you wrote yourself. Elsewhere the schema barely moves: `matches:` widens -from a plain utility-rule id to also accept a parameterized call object, which -is backward compatible, and the top-level property set is unchanged. - -**`Markdown` is narrower than the name suggests, and the routing recipes now -say so.** tree-sitter-markdown splits its grammar into block and inline halves -and ast-grep exposes only the block tree. `atx_heading`, `setext_heading`, -`fenced_code_block`, `list_item`, `paragraph`, `section` and `document` are -real kinds, and headings discriminate by level. Everything inside a line is one -opaque `inline` node: there is no `link`, `emphasis` or `strong_emphasis`. -Naming one is a config error that exits 8 and takes the whole scan with it; -writing it as a pattern instead matches nothing forever with no error at all. -So "link text must not read click here" is a Vale rule, not an `sg` rule, and -"every doc has exactly one h1" is neither, because ast-grep has no count and no -absence assertion. `.md` is the first extension both static engines claim, so -`route` now says which question each answers rather than leaving it to the file -extension. - -Also: the `sg` alias is deprecated as of ast-grep 0.45.0 and prints a banner to -stderr on every run. On a host where only `sg` resolves, that banner used to be -decoded into user-facing messages as if an engine had reported it. It is now -stripped. diff --git a/.changeset/check-honors-gitignore.md b/.changeset/check-honors-gitignore.md deleted file mode 100644 index 92826b21..00000000 --- a/.changeset/check-honors-gitignore.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Keep a whole-project `check` out of the paths git ignores. - -`check` reported prose findings from inside gitignored directories. The case -that surfaced it was a git worktree at `worktrees//`, which is a complete -second checkout: every Vale rule fired again over another branch's documents, -including code an agent was mid-edit on. That makes the finding count move when -a worktree appears or disappears with nothing in the output explaining why, and -the general shape is the same for `dist/`, vendored trees, and local scratch -directories — a check reporting on files nobody maintains. - -Only one engine was wrong, which is why it was hard to attribute. ast-grep's -walker is the `ignore` crate and `sgWalkArgv` has always passed `--no-ignore -hidden` without `vcs`, so a bare scan already skipped `worktrees/`; measured -against ast-grep 0.41.0, it skips a hidden-_and_-ignored `.turbo/` too. Vale -has no notion of a VCS and walked everything. So the two static engines -disagreed about which files the project contains, and only the prose findings -duplicated. On a fixture repository with a worktree present, a bare `check` -went from 6 findings to 4; the two that left were both Vale, both a second copy -of a finding already reported against the tracked file. - -The set comes from `git ls-files --others --ignored --exclude-standard ---directory -z`, which is the complement of the tracked-plus-untracked set the -question is usually phrased as. The complement is the one that scales: -`--directory` collapses a wholly-ignored directory to a single entry, so -`node_modules/` costs one line rather than forty thousand, and the result is -short enough to hand Vale as `--glob` exclusions without meeting `ARG_MAX`. No -new dependency — `.gitignore` is not one file or one syntax question once -nested ignore files, `.git/info/exclude`, a global `core.excludesFile` and -negation patterns are involved, and git already answers all of it in one call. - -The exclusion belongs to the walk `check` chose for itself. `check -worktrees/probe` names an ignored path deliberately and still checks it, on the -same terms as the existing `.taskless/` exclusion. A directory that is not a -git repository, or a host with no `git` on its `PATH`, gets an empty ignore set -and the walk that shipped before this change. Standing _inside_ an ignored -directory is treated as explicit too: git answers `./` there, meaning -"everything here", and honouring that would return an empty check with nothing -saying why. - -The converter skip notice no longer names files inside ignored paths. An -`.adoc` under `worktrees/` is not a file this run declined to convert; it is a -file this run was never going to open, and naming it would send the reader to -investigate a directory the exclusion is there to keep out. diff --git a/.changeset/cli-readme-rewrite.md b/.changeset/cli-readme-rewrite.md deleted file mode 100644 index 88c549a2..00000000 --- a/.changeset/cli-readme-rewrite.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Rewrite the CLI README around what you actually do with Taskless: installing it, -driving it from your coding agent with the `taskless` skill and `/tskl` command, -running `taskless check` in CI, and where to find the docs. Telemetry — and the -two environment variables that turn it off — is now stated plainly instead of -being left to the source. diff --git a/.changeset/decode-sg-stderr.md b/.changeset/decode-sg-stderr.md deleted file mode 100644 index e881a5d4..00000000 --- a/.changeset/decode-sg-stderr.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Stop corrupting non-ASCII characters in ast-grep's error output. - -`runAstGrepScan` and the runtime narrow both decoded ast-grep's stderr one -chunk at a time with `chunk.toString()`. A multi-byte UTF-8 sequence split -across 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 `verify` already 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. diff --git a/.changeset/engine-partitioned-layout.md b/.changeset/engine-partitioned-layout.md deleted file mode 100644 index 597179b8..00000000 --- a/.changeset/engine-partitioned-layout.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@taskless/cli": minor ---- - -Partition `.taskless/` by rule engine. Migration `0004` moves ast-grep rules to `sg/rules/` and `sg/rule-tests/`, the runtime tree to `runtime/rules/` and `runtime/rule-tests/`, and scaffolds an inert `vale/`. 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. `check` runs ast-grep against the committed `.taskless/sg/sgconfig.yml` instead 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-migration `runtime-rules/`. Migration `0004` moves that tree byte-for-byte, so the signatures the server validates are unchanged. - -`check` and `rule verify` read the committed `.taskless/sg/sgconfig.yml` rather 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 with `SCAFFOLD_CONFLICT` rather than failing part-way. diff --git a/.changeset/engine-reach-in-route.md b/.changeset/engine-reach-in-route.md deleted file mode 100644 index aee7be4f..00000000 --- a/.changeset/engine-reach-in-route.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Tell the routing recipe what the local engines can actually read. - -`route` chose between `sg`, `vale`, and the runtime tier on the shape of the -evidence 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 a -login, 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 -`language` as a bare string with no enum, `verify` never checks the field, and -`detect --json` reports the repo's own languages in a different vocabulary. -Vale self-reports nothing at all. - -`route` now states both engines' reach, and `create-vale-rule` repeats Vale's -where 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 `E100` and abandons the whole run, silencing every -other Vale rule over every other file. `create-vale-rule` had been offering -`[*.{md,mdx}]` as its example of widening a matcher. - -`.xml` is the one entry where naming the converter is not enough. It needs -`xsltproc` **and** an XSLT stylesheet, and a stylesheet is document-specific, so -there is nothing to ship and installing the program does not make `.xml` -lintable — unlike `asciidoctor`, which genuinely fixes `.adoc`. Vale says so -differently depending on the host, too: `xsltproc not found` where the program is -absent, `no XSLT transform provided` where it is present, and macOS ships -`/usr/bin/xsltproc` while a typical Linux CI image does not. The contract test -now asserts Vale's checker tag, which is the same everywhere, rather than a -substring of the converter name. diff --git a/.changeset/export-knowledge-prompts.md b/.changeset/export-knowledge-prompts.md deleted file mode 100644 index 45f9d9f6..00000000 --- a/.changeset/export-knowledge-prompts.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@taskless/cli": minor ---- - -Add a `@taskless/cli/prompts` subpath export exposing the CLI's knowledge prompts as importable, topic-keyed render functions. - -`getPrompt(topic, options?)` and the `PROMPTS` map return fully rendered recipe text, with every `%(KEY)s` placeholder already resolved from values the package holds, so a consumer never handles a template dialect. Topic names are typed as `PromptTopic` and start at `static`, the one recipe a service-side consumer can act on; everything else stays internal until a consumer needs it. `PromptOptions` covers the anonymous variant, a `packageManagerDlx` override, and `header: false` for 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 ` 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. diff --git a/.changeset/full-cli-invocation.md b/.changeset/full-cli-invocation.md deleted file mode 100644 index 818d80f8..00000000 --- a/.changeset/full-cli-invocation.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@taskless/cli": minor ---- - -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 on `PATH` — 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/prompts` gains `getInstructions(topic, options?)` and `getRawInstructions(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; `variables` comes from sprintf-js's own parse rather than a regex over the template. `PromptOptions.invocation` is the only way a consumer sets `TASKLESS_CLI` — the render path stays free of `process` so it remains importable from a Worker. - -Fixes launcher detection in user-facing error messages. `getCliPrefix()` read only `npm_config_user_agent`, which every pnpm entry point sets, so running the CLI from a `package.json` script told the user to run `pnpm dlx @taskless/cli@latest`. Detection now reads the path the binary was launched from, recognizes npx and `pnpm dlx` only, and answers "unknown" for everything else. The package specifier comes from the build target, so a nightly's error messages name `@taskless/cli-nightly` at its own version. diff --git a/.changeset/gh-owner-camelcase.md b/.changeset/gh-owner-camelcase.md deleted file mode 100644 index 5dfbddd7..00000000 --- a/.changeset/gh-owner-camelcase.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Spell the telemetry property `ghOwner` rather than `gh_owner`. - -Telemetry names events in `snake_case` (`cli_run`, `cli_check_completed`) and properties in `camelCase` (`cliVersion`, `durationMs`, `errorCount`). `gh_owner` was added in the previous change with the event convention applied to a property by mistake, and it was the only property in the codebase spelled that way. - -No migration is needed for anyone reading this: the property was introduced in this same unreleased cycle, so no stable build ever emitted `gh_owner` and no saved insight can be filtering on it. - -The convention is now stated normatively in the `analytics` spec, so it can be checked rather than inferred from whichever names happen to exist. diff --git a/.changeset/help-flag-on-subcommands.md b/.changeset/help-flag-on-subcommands.md deleted file mode 100644 index afaa949a..00000000 --- a/.changeset/help-flag-on-subcommands.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Make `--help` work on every command, instead of running the command. - -`taskless check --help` printed no usage — it ran `check`. So did every other -subcommand: `--help` was parsed as an unrecognized flag and the command body -executed anyway, which meant asking `init` how it works installed skills, and -asking `check` how it works migrated the `.taskless/` scaffold. The only place -help worked was the bare `taskless --help`, whose own output tells you to run -`taskless --help`. - -`--help` and `-h` are now recognized at every depth, including nested commands -(`taskless auth login --help` describes `login`, not `auth`), and a working -directory passed before the command (`taskless -d ./repo check --help`) no -longer confuses which command you asked about. The usage text itself is -unchanged, and nothing else about how commands run has changed. diff --git a/.changeset/house-style-in-recipes.md b/.changeset/house-style-in-recipes.md deleted file mode 100644 index 0e5fb09e..00000000 --- a/.changeset/house-style-in-recipes.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Hold the agent-facing recipes to the house writing style. - -`packages/cli/src/agent/*.txt` is bundled into the published CLI and served by `taskless agent `, so it is text users and agents read on every authoring run. It was the largest prose surface the house-style rules did not cover. `no-em-dashes`, `no-blocklist-phrases` and `no-hedging` now reach it, and the 270 existing em and en dashes are rewritten as periods, commas, colons or parentheses depending on what each one was doing. - -No instruction changed meaning. The recipe-content tests, which assert exact phrases from `route.txt`, `create-sg-rule.txt`, `create-vale-rule.txt` and others, all still pass. - -Two scoping notes worth knowing for anyone widening further. These files are `.txt`, which Vale treats as plain text: there is no markdown parser, so fenced blocks and code spans are **not** skipped the way they are in a `.md` file, and command examples are checked as prose. And `create-vale-rule.txt` and `verify-rule.txt` are excluded from `no-hedging`, because both teach rule authoring through a worked example named `no-simply` and the token appears throughout as an identifier rather than as hedging. diff --git a/.changeset/idempotent-rules-partition.md b/.changeset/idempotent-rules-partition.md deleted file mode 100644 index 95349d75..00000000 --- a/.changeset/idempotent-rules-partition.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Stop the engine-partition migration from relocating a rules tree that is already partitioned. - -A `.taskless/` with no `taskless.json` — a manifest that was never committed, or was deleted — reads as version 0, so every migration runs against it. Migration `0004` then applied its `rules/` → `sg/rules/` move to a tree already in the current layout, burying every rule at `.taskless/sg/rules/sg//`; `0005` scaffolded fresh empty engine directories over the gap. Nothing errored. `check` scanned 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. - -`0004` now 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-`0004` tree of flat `rules/.yml` files still moves wholesale, as before. And a tree holding both — an already-partitioned layout with a stray `rules/.yml` beside 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, and `0005` never brings them back, which is the same silent clean pass by another route. diff --git a/.changeset/ledger-baseline.md b/.changeset/ledger-baseline.md deleted file mode 100644 index 3625e874..00000000 --- a/.changeset/ledger-baseline.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@taskless/cli": patch ---- - -A project with no recorded rules marker now walks the ledger from the beginning, and `taskless update --rules` replaces `--reconciledTo=`. - -Previously an absent `rules.reconciledTo` meant "nothing to walk", on the reasoning that a project created at the installed version has no history. That was right about new projects and wrong about every existing one: a project that predates the ledger has had none of its entries applied, so reading absence as up to date silently excused exactly the population the entries were written for. The 0.11.x entry would have reached nobody. - -Absent now means `0.0.0`, so every section applies. New projects stay correct because `init` stamps the marker at creation, which is what makes the two distinguishable: present means accounted for, absent means predates the ledger. The stamp never overwrites an existing marker, so re-running setup cannot reset one a real walk earned. - -`--reconciledTo=` is replaced by the flag `--rules`, which stamps the running CLI's version. The value was never load-bearing: the CLI knows its own version, the only sensible endpoint of a walk is the installed one, and accepting a value only made it possible to claim a walk that did not finish. Removing it removes the two guards that existed to police it and every way of supplying it wrongly. The backwards guard remains, because an older CLI running on the same project would otherwise rewind the marker. - -The ledger heading is now `Migrating to 0.11.x`, since the entry describes the release series rather than one patch. diff --git a/.changeset/migrated-envelope-field.md b/.changeset/migrated-envelope-field.md deleted file mode 100644 index eca4dfa6..00000000 --- a/.changeset/migrated-envelope-field.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Report the `.taskless/` layout migration on the `--json` envelope. - -`check`, `verify`, and `test` migrate the scaffold before they can do their real -work, and that rewrites files in the working tree: rules move into per-rule -directories, configs are deleted, `taskless.json` and `.gitignore` are rewritten. -Until now the only trace was one line of prose on stderr, so a CI script reading -`{"success":true}` had no way to learn that its checkout had changed underneath -it. The migration would then land in an unrelated commit, or run mid-suite and -fail tests that had nothing to do with the change being made. - -Those commands now carry a `migrated` field when, and only when, a migration -ran: - -```json -{ - "success": true, - "results": [], - "migrated": { - "from": 3, - "to": 5, - "applied": [4, 5], - "files": { - "added": [".taskless/rules/sg/no-eval/no-eval.yml"], - "modified": [".taskless/taskless.json"], - "removed": [".taskless/sgconfig.yml"] - } - } -} -``` - -The field is absent when nothing happened, so presence is the signal and no -consumer has to read empty arrays to decide. Paths are relative to the project -root and sorted. - -The migration keeps happening automatically, because these commands need a known -layout to run at all and the alternative is a hard failure on every upgrade. The -human notice improved to match the new field: it names the source and target -versions up front, and prints the files it touched on completion. diff --git a/.changeset/nightly-cli-builds.md b/.changeset/nightly-cli-builds.md deleted file mode 100644 index 9e16c805..00000000 --- a/.changeset/nightly-cli-builds.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Split the release pipeline so each workflow file carries one release design. - -`release.yml` held two jobs with opposite trust properties behind one header. -It is now `release-cli-changeset.yml` — which reads contributor-authored -changesets 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.yml` is renamed `release-vale.yml` to match. - -The build and publish steps themselves are unchanged — same triggers, same -`permissions: {}`, same action pins, same OIDC trusted publishing behind the -same `npm-production` approval. Two operational details do differ: `check` and -`publish` no longer share the `release-*` concurrency group, and the release -now runs as two workflow runs instead of one, so its check contexts are -`Release CLI Version PR / …` and `Release CLI / …` rather than `Release / …`. -Neither is a required check. - -The header comments also get one correction: they claimed `npm-production` had -no 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 `main` as `@taskless/cli-nightly`. - -Every push to `main` that has changesets pending now publishes the CLI under a -second package name, stamped `-x` — so -merged-but-unreleased behavior is installable with `npx @taskless/cli-nightly`. -A nightly is the same build as the release it anticipates and keeps the -`taskless` executable, so it is a drop-in; the rename happens at pack time, so -`@taskless/cli`'s own version history stays releases-only. Installing both -globally 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@` — pinned to the -build being installed — instead of `npx @taskless/cli`. Previously a nightly -carried 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. diff --git a/.changeset/nightly-self-reported-version.md b/.changeset/nightly-self-reported-version.md deleted file mode 100644 index b6e72969..00000000 --- a/.changeset/nightly-self-reported-version.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@taskless/cli": patch ---- - -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 the -same command in the same run, pinned every invocation to -`@taskless/cli-nightly@0.11.0-…`. The manifest attributed the install to a -version that never performed it, which matters because `install.cliVersion` is -what 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.json` is deliberately left untouched — so the build was -reading 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, and -the `cliVersion` telemetry property on nightly builds. Released builds are -unaffected. 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.json` instead. Deriving from one -source is not the same as being checked against it. diff --git a/.changeset/onboard-route-first.md b/.changeset/onboard-route-first.md deleted file mode 100644 index ee6849a0..00000000 --- a/.changeset/onboard-route-first.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Onboarding now reads the routing surface before it proposes rule candidates. - -The `onboard` recipe asked the agent to synthesize its bullet list of -hypothetical rules first and consult `route` only afterwards, once per accepted -bullet. 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 route` and runs `taskless detect --json` -before proposing anything, and each bullet carries the destination it would -route to: `- no-direct-db-access [sg]: …`. The annotation is provisional — -`route` still decides for real at materialization time, when it has the rule's -full 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. diff --git a/.changeset/remote-tier-capability-boundary.md b/.changeset/remote-tier-capability-boundary.md deleted file mode 100644 index cc69e123..00000000 --- a/.changeset/remote-tier-capability-boundary.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@taskless/cli": minor ---- - -Report a missing GitHub remote as a boundary on remote rule generation, not as a broken repository. - -Remote rule generation needs a verifiable org, which comes from a GitHub `origin`. Local rule authoring, `verify`, `test` and `check` do not, and never did. Previously all three no-remote situations failed with one code and a message telling the user to fix their repository, which reads as a setup or auth problem rather than as one tier being unavailable. A project that is not a git repository at all, such as a notes vault, had no obvious path forward. - -Three codes replace the single collapsed one, because the remedies differ and an agent has to pick one: - -| Code | Situation | Remedy | -| ------------------------- | ------------------------------------- | ------------------------------- | -| `NOT_A_GIT_REPOSITORY` | the directory is not a git repository | `git init`, or author locally | -| `NO_ORIGIN_REMOTE` | a git repository with no `origin` | add a remote, or author locally | -| `UNSUPPORTED_REMOTE_HOST` | an `origin` that is not GitHub | author locally | - -`NO_GITHUB_REMOTE` is **retained** and remains a valid member of the error-code contract, so consumers and recipes that branch on it keep working. Error codes are an agent contract: adding one is safe, renaming one is not. - -Each message now names the local authoring path that still works, so the refusal is something a reader can route around rather than a dead end. None of the three is ever reported as an authentication failure, which is pinned by a test: an agent that saw `AUTH_REQUIRED` here would send the user through `auth login`, which cannot fix any of them. - -Telling "not a git repository" from "no `origin`" needs a second question, since both fail the same `git remote get-url origin` call. That probe runs only on the failure path, so an ordinary run still spawns one process rather than two. - -`taskless info --json` now also reports `repositoryUrl` and `ghOwner`, so a caller deciding whether remote generation is available reads the same resolution the CLI enforces instead of shelling out to git and reaching a different answer. `repositoryUrl` is the canonical GitHub URL or `null`; `ghOwner` is the owner segment or the literal `[unknown]`. Both resolve without failing, including on a host with no git installed, and both are present under `--anonymous` because capability state is not auth state. `taskless auth` is unchanged and stays plain text. - -Telemetry now records `gh_owner`, so it is possible to see which GitHub owners use the CLI, including on anonymous runs. It resolves from the git remote rather than from the token, and is the owner segment when one is found or the literal `[unknown]` when not, so runs with no resolvable owner stay countable rather than disappearing from aggregates. `gh_owner` rather than `gh_org` because the first path segment of a GitHub URL is an organization or a user account, and telling them apart needs an authenticated API call an anonymous run cannot make; owner type is never inferred. - -`route` no longer offers remote generation when no GitHub owner is identifiable, reading `ghOwner` from the `info --json` call it already makes rather than re-deriving the remote, and says the tier is unavailable rather than silently dropping it. `create-remote-rule` guards the same constraint itself, before it collects anything, for the times an agent reaches it without going through `route`. Both state that `auth login` does not fix it: no GitHub owner is a property of the project, not the session. diff --git a/.changeset/rename-help-to-agent.md b/.changeset/rename-help-to-agent.md deleted file mode 100644 index 97866ebd..00000000 --- a/.changeset/rename-help-to-agent.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Complete the `help` → `agent` rename. The user-facing command was renamed in -0.10.0, but the internals kept the old name: the recipe directory moved from -`packages/cli/src/help/` to `packages/cli/src/agent/`, the `cli-help` OpenSpec -capability is now `cli-agent`, and the shipped skill and `/tskl` command no -longer tell agents to run the removed `npx @taskless/cli help ` (they -now 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_help` event is renamed -to `cli_agent`. The `topic` property is unchanged. PostHog dashboards keyed on -`cli_help` will need updating — nothing is emitted under the old name. diff --git a/.changeset/rules-reconciliation-ledger.md b/.changeset/rules-reconciliation-ledger.md deleted file mode 100644 index 9a6c3943..00000000 --- a/.changeset/rules-reconciliation-ledger.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@taskless/cli": minor ---- - -`taskless update` now tells an agent what an upgrade changed for the rules already in a project, and records when that work is done. - -**BREAKING for anyone scripting `taskless update`.** It used to mean "reinstall the skills non-interactively", which is what `taskless init --no-interactive` already does through the same code path, and what running `taskless` does on its own. Scripts relying on the old behavior should call `taskless init --no-interactive`. - -The word is reclaimed for the job an agent actually needs. Running the CLI migrates the `.taskless/` layout and refreshes skills: that is the directory, and it is automatic. No migration can rewrite the rules themselves, and a rewriter that now requires a `fix`, or a rule whose matching semantics shifted under a new engine, is a question about content. An agent that has run a migration and watched it succeed will otherwise reasonably conclude the upgrade is finished. - -`taskless update` with no flags serves a ledger: one section per release, in order, saying what that version means for existing rules. Sections are cumulative, and a version with nothing to do says so explicitly, because an agent cannot tell "nothing here" from "nobody wrote this". The first entry covers 0.11.0: the newly required `fix` on rewriters, Markdown's block-only grammar and its two opposite failure shapes, `sg run --lang` accepting alias spellings, and the matching-semantics changes that alter what a valid rule matches with no error at all. - -`taskless update --reconciledTo=` records that the walk finished, in a new `rules` section of `.taskless/taskless.json` alongside the existing `install`. It stores the CLI version and the ast-grep and Vale versions the rules are now valid against. - -The two namespaces are separate because they drift. `install` records how the scaffold got here and moves on a skills refresh; `rules` records what the rules are valid against and moves only on a completed reconciliation. Keying rule work off `install.cliVersion` would let an agent skip entries it never performed, and it would fail quietly: the walk would report nothing to do while the rules stayed wrong. - -The version is validated rather than trusted. A value ahead of the installed CLI is rejected, since this build carries no entries for it, and the marker is never moved backwards. - -`taskless info --json` reports both namespaces, so an agent reads where to start from the same payload it already fetches. diff --git a/.changeset/scan-hidden-directories.md b/.changeset/scan-hidden-directories.md deleted file mode 100644 index 661d9b60..00000000 --- a/.changeset/scan-hidden-directories.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Let ast-grep rules see inside hidden directories such as `.github/`. - -ast-grep's file walker skips dot-directories unless told otherwise, and -`runAstGrepScan` never told it otherwise. No `sg` rule could match anything -under `.github/`, `.circleci/`, `.vscode/` or `.husky/`, so `check` reported -nothing 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 `check` and the runtime engine's ast-grep narrow now pass -`--no-ignore hidden`. - -Only `hidden` is passed, and deliberately not `vcs`: `.gitignore` is still -respected, so the wider walk does not start reporting findings in `dist/` or -anywhere else a project has already said it does not want scanned. Rule -discovery is untouched — `ruleDirs` walks 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 and -reaching it is not a fix. A rule definition is structured YAML full of `id:`, -`language:`, `severity:` and `rule:` keys, so an ordinary user-written Yaml rule -fires 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 `check` walks the whole project on its own; an explicit path stays a -request, which is the rule the Vale runner already follows. - -`.git/` is excluded on the same terms. ast-grep has no exclusion of its own for -it and `.gitignore` does not list it, so the default hidden-directory skip was -the only thing holding it back: without this, a whole-project `check` descended -into `.git/objects` and `.git/logs` on every run, and `.git/hooks/*` scripts -matched 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 -— `check` was clean while `check .` reported findings inside `.taskless/`. Vale -was 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. diff --git a/.changeset/sg-fixture-coverage.md b/.changeset/sg-fixture-coverage.md deleted file mode 100644 index 8fbd0a33..00000000 --- a/.changeset/sg-fixture-coverage.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Fail `test` for an ast-grep rule that never demonstrates it can fire. - -`verify` checked that a rule's `-test.yml` existed and never read what was in -it, and `ast-grep test` reports an empty `invalid:` bucket as `1 passed; 0 -failed` and exits zero. A rule whose fixtures were all `valid:` therefore -reported `ok: true, ran: true` while `check` found nothing anywhere — verified -looking verified, having proved nothing. `test` now counts the `valid:` and -`invalid:` entries across every test file a rule owns and requires both, which -is 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 fails `test` until a fixture is added that the rule -actually 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 `smart` -strictness 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. Upstream -considers this intended and 0.45.2 behaves identically, so there is no version -to upgrade to; write the pattern as an object with `strictness: ast` to ignore -the separator, or use `any:` with one branch per arity. `verify --schema` now -carries a worked example, and the behaviour is pinned against the vendored -binary so a bump that changes it fails loudly. - -`create-sg-rule` states all of this where a pattern is written: the arity table -measured against the pinned binary, both remedies and the fact that -`strictness: ast` moves a trailing `$$$` from two arguments to one rather than -to 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 pinned -constants — nothing local validates that field, an unrecognized spelling takes -the whole scan down, and `Tsx` is a different parser from `TypeScript` rather -than an alias. `improve-rule` gains the two notes that matter when a rule is -rewritten 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. diff --git a/.changeset/sg-rule-tests-directory.md b/.changeset/sg-rule-tests-directory.md deleted file mode 100644 index 238f55a1..00000000 --- a/.changeset/sg-rule-tests-directory.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Stop a rule with no tests from failing every other rule's ast-grep test run. - -Migration `0005` created 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 `-YYYYMMDD-test.yml` shape the migration can attribute to a rule — arrived in the new layout with no tests directory at all. Assembly then named that directory as a `testConfigs` entry anyway, and ast-grep 0.41.0 treats a `testDir` it cannot read as fatal to the whole invocation rather than to the one rule: `taskless test` on _any_ rule died with `Cannot read rule directory .taskless/rules/sg//.tests` and exit 6, naming a rule the author had never touched. `--filter` does not scope that away, so there was no way to run one rule's tests around it. - -`0005` now gives every `rules/sg//` a `.tests/`, holding a committed `.gitkeep` when 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 any `testDir` that 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 amended `0005`, 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 — `verify` still reports "No test file found" and `test` still reports "Skipped: no test file found", both reading the rule directory rather than the generated config. diff --git a/.changeset/typed-identity-error-codes.md b/.changeset/typed-identity-error-codes.md deleted file mode 100644 index 46679461..00000000 --- a/.changeset/typed-identity-error-codes.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Stop deriving `--json` error codes from the text of an error message. - -`rule create` and `rule improve` chose between `AUTH_REQUIRED` and -`NO_GITHUB_REMOTE` by running `/git remote|origin/i` over the human-readable -message that `resolveIdentity` threw. The codes exist so a machine consumer -never parses English, and the code itself was being picked by parsing English. -It happened to be right only because both repository-URL messages contain the -words "git remote"; rewording or translating either one would have silently -told every `--json` consumer to log in when the real problem was the project's -git remote. - -Each failure now throws a `CLIError` carrying its own `CLIErrorCode`, and both -call sites read that field through one shared helper. No new code was added. - -The emitted codes for existing scenarios are unchanged: - -| Condition | Code | -| ------------------------------------------- | ------------------ | -| Not logged in | `AUTH_REQUIRED` | -| Not a git repository, or no `origin` remote | `NO_GITHUB_REMOTE` | -| `origin` remote is not on `github.com` | `NO_GITHUB_REMOTE` | - -One code does change, for a scenario that is unreachable today: an unexpected -throw from the org-resolution step now reports `INTERNAL_ERROR` rather than -whichever of the two codes its wording happened to match. That step swallows -every network and HTTP error and falls back to a nil-UUID org subject, so it -cannot fail in normal operation; anything escaping it is a CLI bug rather than -a state the caller can act on. diff --git a/.changeset/vale-authoring-polish.md b/.changeset/vale-authoring-polish.md deleted file mode 100644 index 7d2bf027..00000000 --- a/.changeset/vale-authoring-polish.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Close the gaps in `create-vale-rule` that produce a rule which is green -everywhere and reports nothing. - -A malformed Vale rule fails loudly. A Vale rule that is merely _wrong_ passes -`verify`, passes `test`, and never fires — and nobody re-checks a green rule. -The recipe now documents each of those failures as an observed behavior of the -pinned Vale binary rather than as a caution in principle: - -- **The measured `scope` vocabulary**, with what each value actually reaches. - `raw` subsumes `code` and `text`; `~` negation and `&` chaining are accepted; - and a negation over a scope Vale does not know (`~fenced`) is a silent no-op - that removes the exclusion you wrote the rule for. -- **`scope` is per-rule.** Taskless assembles one config per run, which invites - the assumption that scopes interact. They do not. -- **A `raw`-scoped rule cannot be suppressed** by ``, - because it reads the unparsed document — so a rule about a shell command needs - `raw` and trades away per-case exemption. -- **A punctuation-only token needs `nonword: true`**, because Vale wraps every - token in word boundaries and an em dash has no word character on either side. -- **How to scope a rule _out_**, with a second matcher assigning `NO`. -- **Collocation guidance** for a banned word, checked by writing the `pass/` - fixture from the literal sense first. -- **Fixture design for a subject that appears in code**: `fail/` must carry it - inline, fenced, and in prose. -- **`limit`** in the common-fields table, **`vocab`** with the per-check fields - it actually belongs to, and that Vale loads only `.yml` — a style file renamed - to `.yaml` is silently not loaded at all. -- **Fixtures run under an isolating config**, so a green `test` is not evidence - the rule's matcher glob reaches any real file. - -Vale's check types are now enumerated from the binary rather than the docs. -There are **twelve**, not eleven: the docs fold `readability` into `metric`. -The per-check field tables are measured the same way, which corrects three -published claims: `capitalization` takes `prefix` (singular) and rejects -`prefixes` and `suffixes`, `capitalization` rejects `ignorecase`, and -`occurrence` rejects `exceptions` and `vocab`. - -`verify` now schema-checks a Vale rule structurally, before Vale is invoked. -It previously validated `level` and the presence of the rule's `.vale.ini`, so -`extends: nonsense` and `scope: fenced` both verified clean. It now also checks: - -- **`extends`** against the twelve check types, naming the accepted set. -- **`scope`** as a grammar over measured operands — a bare value, a list, `~` - negation, `&` chaining — rather than a flat enum, which would have rejected - working rules. It is deliberately stricter than Vale in one place: a negation - over an operand Vale does not know (`~fenced`) fires on everything, having - silently lost its exclusion, and is rejected. -- **Per-check fields**, so a field belonging to another check type is caught - before Vale reports `E201`. `consistency` and `spelling` are exempt because - the binary accepts any key on those two. - -The ordering is the point for two of the three: Vale reads one assembled config -per run, so an unknown `extends` or a foreign field reaching the binary takes -down **every** Vale rule's findings, not just the offending rule's. - -The schema is hand-authored, because Vale publishes no JSON Schema and its -machine-readable field knowledge is behind a paid hosted MCP. What holds it to -the binary is a corpus of 82 minimal rules, each with a document it must flag, -run through both the vendored Vale and the schema, asserting the two agree — -with guards so that a rule which "did not fire" because its fixture was -unreachable cannot pass as a measurement. A Vale upgrade that changes the -vocabulary fails a test that names the value. diff --git a/.changeset/vale-converter-formats.md b/.changeset/vale-converter-formats.md deleted file mode 100644 index 7a91db66..00000000 --- a/.changeset/vale-converter-formats.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -"@taskless/cli": patch ---- - -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`, `dita` or -`mdx2vast`, and the `@taskless/vale-*` packages ship the binary with none of -those alongside it. On a host without the converter Vale does not skip the file: -it prints one `E100 [lintAdoc] Runtime error` on stderr, writes nothing at all to -stdout, 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 `.adoc` took a check that reported five Vale findings across four files -down to zero — reported as a raw JSON blob among the results, and exiting 1 the -same way any ordinary failing check does. - -`runVale` now 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 recipes -render their format lists from — measured against the pinned binary rather than -transcribed from documentation. That is how `.asc` and `.rest`, a third AsciiDoc -spelling 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`, `.mkd` and `.mkdn` are 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. -`.mdx` gains 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. `.typ` moved the opposite way: Typst now -parses through `typst2vast`, an external program this build does not ship, so a -Typst file is excluded from the run rather than read as prose the way 3.17.1 -read it. `.rmd` and the new `.qmd` and `.myst` are parsed as markup, and `.qml`, -`.scss` and the new `.qdoc` are comment-aware where they previously fell through -to plain text. - -The `.typ` move is the one that mattered to get right. An extension missing from -the 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 `-` as `optionalDependencies` pinned -to 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 `vale` found on `PATH`. diff --git a/.changeset/vale-rule-engine.md b/.changeset/vale-rule-engine.md deleted file mode 100644 index 77daab75..00000000 --- a/.changeset/vale-rule-engine.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -"@taskless/cli": minor ---- - -Add Vale as a second static-tier rule engine, give every engine one rule layout, and rename the agent-facing command. - -`check` now dispatches by engine and runs ast-grep, Vale, and runtime rules -concurrently, 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///`, holding -the rule, any per-engine config, and its tests in `.tests/`. Writing a rule -means creating a directory and deleting one means `rm -rf`. Nothing outside it -is touched either way, so concurrent authors never collide on a shared file. - -Vale rules carry their own `.vale.ini` declaring 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`/`ignores` inside the rule and needs no second file. - -**`rule verify` is replaced by two path-addressed commands.** `verify ` -checks that a rule has the components its engine requires and needs no tests, -so it works while you're still authoring. `test ` runs the rule's tests, -after running `verify` and 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 ` is now `taskless agent `.** The -command 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 -create` becomes `taskless agent create-sg-rule`; multiple positionals are no -longer 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: - -| Was | Now | -| ------------------ | --------------------------------------- | -| `rule create` | `create-sg-rule` / `create-remote-rule` | -| `rule improve` | `improve-rule` | -| `rule delete` | `delete-rule` | -| `rule verify` | `verify-rule` | -| `rule meta` | `rule-meta` | -| `static` | `create-sg-rule` | -| `existing` | `create-legacy-rule` | -| `engine-selection` | `route` | - -`route` now applies the engine reasoning itself and names a concrete -`create-*-rule` topic, so `engine-selection` is removed rather than renamed — -its criterion is stated once, in `route`. Every authoring recipe is rewritten -for the rule-directory layout. - -**BREAKING for `@taskless/cli/prompts` consumers.** `engine-selection` is no -longer exported. `TOPICS` is now `create-sg-rule`, `create-vale-rule`, and -`create-runtime-rule`, so a consumer that decides an engine can reach the -procedure 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. diff --git a/.changeset/vale-schema-generation.md b/.changeset/vale-schema-generation.md deleted file mode 100644 index 8b600f66..00000000 --- a/.changeset/vale-schema-generation.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Derive the Vale rule schema's vocabulary from the vendored binary instead of -transcribing it by hand. - -`pnpm generate:vale-schema` runs the pinned Vale against rules it writes itself -and emits `src/generated/vale-vocabulary.ts` — twelve check types, three levels, -ten per-check field tables, twenty-eight scope operands, two open scope -families — plus a divergence report. `src/schemas/vale-rule.ts` imports it and -stays what it was: the zod layer, the scope grammar, and the error messages that -explain blast radius to an author. Every corpus row that predates the generation -passes against the generated schema unmodified, and rows were added to cover -ground the generation newly measured. - -One of those measurements found a real gap. Vale decodes a check's own fields -case-insensitively, so `Tokens:` means `tokens:`, but a few keys are read off -the raw mapping before that decode and are not synonyms of their capitalised -spellings. Assuming that set was the three header keys was wrong: `scope` and -`name` are read literally too, so `Scope: raw` fails the run with -`E201 has invalid keys: 'scope'` while the schema, having lowercased it, was -accepting it. For `scope` that was the worse half of the bug, because -canonicalising the key also routed the value past the scope grammar, which is -the only thing that ever inspects it. The set is now derived as a per-key -two-run differential (the lowercase spelling must run clean; the capitalised one -either runs clean too or draws a diagnostic) and emitted as -`VALE_LITERAL_KEYS`, with corpus rows for `Scope:`, `Name:` and, as the contrast -that keeps them from proving too much, `Tokens:`. - -What a transcription lost was not the answer but the question. Every value in the -previous schema _was_ measured — by a script that was then discarded, leaving the -next person to raise `VALE_VERSION` with a failing test and no way to reproduce -the measurement it was failing against. - -The measuring is also where the errors live, so the generator is built around one -rule: **every verdict comes from the process exit status and the structured JSON -output, never from matching stdout against an error phrase.** A Go panic contains -no `has invalid keys` string, so a phrase-grep scores a crash as a clean run — -which is exactly how a tokenless `sequence` rule once came to look like a check -that validates nothing. A run's outcome is a closed set of `clean`, `diagnostic`, -`panic`, and `unrecognized`, and the last one is fatal at every call site. - -Two of the four vocabularies are self-enumerating: an unknown `extends` or -`level` makes the binary name its own accepted set. If either of those lines -stops matching, generation **fails** rather than emitting a short enum — a -truncated enum is _stricter_ than the binary, which is the direction that blocks -rules that would have worked. - -The other two are honest about their limit, and the artifact says so. `E201` -names the key you got wrong and never the ones you could have used, and an -unrecognized `scope` raises nothing at all — so field tables and scope operands -are **verified, not discovered**, from a candidate list seeded from four sources -with its provenance recorded. A scope verdict is three-valued, and a `scope: raw` -reach probe must fire on every fixture, so an operand cannot be dropped because -its fixture was never linted. - -Where the binary and Vale's documentation disagree, `vale-vocabulary-report.md` -records it rather than either side being quietly dropped: `meta` and -`meta.class.` are documented and never fire; `frontmatter` and -`frontmatter.` fire and are documented nowhere; `consistency` and `spelling` -validate no keys at all. diff --git a/.changeset/verify-sg-language.md b/.changeset/verify-sg-language.md deleted file mode 100644 index f891dd69..00000000 --- a/.changeset/verify-sg-language.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Validate an sg rule's `language:` field in `verify`, instead of leaving it to -ast-grep at `check` time. - -Nothing local had an opinion on the field. The vendored rule schema types it as -a bare string with no enum, so `verify` returned `ok: true` for any spelling and -the binary was the first thing to object — in the two ways it objects, both of -them late: - -- A name ast-grep does not recognize fails `SgLang` deserialization, which - aborts parsing of the single config Taskless assembles per run. One typo takes - every _other_ sg rule down with it. `verify` now fails that rule by name, - prints the accepted spellings, and suggests the obvious canonical one where - there is one (`C#` → `CSharp`). -- A recognized name pointing at the wrong parser reports nothing and reads as a - clean codebase. `Tsx` and `TypeScript` are two parsers, not aliases, so a - `TypeScript` rule scoped to `**/*.tsx` matches nothing and exits zero. - `verify` fails that rule, and notices the half-dead case where a `{ts,tsx}` - glob reaches both. - -Case variants and ast-grep's extension aliases are accepted rather than -rejected, since ast-grep accepts them itself: `typescript`, `TYPESCRIPT` and -`ts` all reach TypeScript. They get a notice naming the canonical spelling, so -rules already written the lowercase way — including the ones in this -repository — keep passing. - -The `files:` scan reads both shapes ast-grep allows for a glob entry, the plain -string and the `{ glob, caseInsensitive }` object, so the wrong-parser check is -not silently skipped for rules written the second way. diff --git a/.changeset/wild-jars-repeat.md b/.changeset/wild-jars-repeat.md deleted file mode 100644 index 2999cb81..00000000 --- a/.changeset/wild-jars-repeat.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@taskless/cli": patch ---- - -Resolve the ast-grep binary without relying on an install-time step, and drop -the `@ast-grep/cli` wrapper from what consumers install. - -- **The wrapper moves to `devDependencies`.** The seven `@ast-grep/cli-` - packages were already declared in `optionalDependencies`, and the CLI already - resolved them by path — the wrapper was a leftover whose only job is a - `postinstall` that hardlinks the binary into itself so its `bin` entries work. - Nothing here invoked those entries. Consumers now install only the platform - package matching their host, and the wrapper's `postinstall` — which leaves a - placeholder text file where the binary should be under `pnpm dlx`'s strict - isolation — is out of the shipped product entirely. It stays as a - `devDependency` because `fetch-ast-grep-schema` reads its version. -- **Platform packages are pinned exactly.** They were carets, and the wrapper - had been enforcing alignment implicitly by pinning its own - `optionalDependencies`; without it, two hosts could resolve different ast-grep - versions against the same rules and disagree about findings. This change makes - the pin explicit without moving the version; the upgrade itself is a separate - change. -- **Binary resolution exhausts every candidate before failing.** It now searches - the platform package, `node_modules/.bin`, then `sg` and `ast-grep` on `PATH`, - and throws naming what it tried. Previously it returned a bare `"sg"` and let - `spawn`'s `ENOENT` be the error, from a caller that could not say where it had - looked. - -Alpine improves as a side effect: upstream publishes no musl build and marks its -Linux packages `libc: ["glibc"]`, so today the wrapper's `postinstall` resolves a -package that does not exist and exits 1, failing the install wherever dependency -scripts run. Installing now succeeds and resolution falls through to `PATH`. diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 22a15c42..250467c8 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "taskless", "description": "Taskless skills for code quality rules, authentication, and project management", - "version": "0.10.2", + "version": "0.11.0", "author": { "name": "Taskless" }, diff --git a/package.json b/package.json index 2e953391..f3d0ef35 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@taskless/skills", - "version": "0.10.2", + "version": "0.11.0", "license": "MIT", "repository": "taskless/cli.git", "scripts": { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index ec084400..f6cd780e 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,859 @@ # @taskless/cli +## 0.11.0 + +### Minor Changes + +- f7ee186: Partition `.taskless/` by rule engine. Migration `0004` moves ast-grep rules to `sg/rules/` and `sg/rule-tests/`, the runtime tree to `runtime/rules/` and `runtime/rule-tests/`, and scaffolds an inert `vale/`. 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. `check` runs ast-grep against the committed `.taskless/sg/sgconfig.yml` instead 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-migration `runtime-rules/`. Migration `0004` moves that tree byte-for-byte, so the signatures the server validates are unchanged. + + `check` and `rule verify` read the committed `.taskless/sg/sgconfig.yml` rather 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 with `SCAFFOLD_CONFLICT` rather than failing part-way. + +- d4fca88: Add a `@taskless/cli/prompts` subpath export exposing the CLI's knowledge prompts as importable, topic-keyed render functions. + + `getPrompt(topic, options?)` and the `PROMPTS` map return fully rendered recipe text, with every `%(KEY)s` placeholder already resolved from values the package holds, so a consumer never handles a template dialect. Topic names are typed as `PromptTopic` and start at `static`, the one recipe a service-side consumer can act on; everything else stays internal until a consumer needs it. `PromptOptions` covers the anonymous variant, a `packageManagerDlx` override, and `header: false` for 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 ` 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 on `PATH` — 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/prompts` gains `getInstructions(topic, options?)` and `getRawInstructions(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; `variables` comes from sprintf-js's own parse rather than a regex over the template. `PromptOptions.invocation` is the only way a consumer sets `TASKLESS_CLI` — the render path stays free of `process` so it remains importable from a Worker. + + Fixes launcher detection in user-facing error messages. `getCliPrefix()` read only `npm_config_user_agent`, which every pnpm entry point sets, so running the CLI from a `package.json` script told the user to run `pnpm dlx @taskless/cli@latest`. Detection now reads the path the binary was launched from, recognizes npx and `pnpm dlx` only, and answers "unknown" for everything else. The package specifier comes from the build target, so a nightly's error messages name `@taskless/cli-nightly` at its own version. + +- a5cff75: Report a missing GitHub remote as a boundary on remote rule generation, not as a broken repository. + + Remote rule generation needs a verifiable org, which comes from a GitHub `origin`. Local rule authoring, `verify`, `test` and `check` do not, and never did. Previously all three no-remote situations failed with one code and a message telling the user to fix their repository, which reads as a setup or auth problem rather than as one tier being unavailable. A project that is not a git repository at all, such as a notes vault, had no obvious path forward. + + Three codes replace the single collapsed one, because the remedies differ and an agent has to pick one: + + | Code | Situation | Remedy | + | ------------------------- | ------------------------------------- | ------------------------------- | + | `NOT_A_GIT_REPOSITORY` | the directory is not a git repository | `git init`, or author locally | + | `NO_ORIGIN_REMOTE` | a git repository with no `origin` | add a remote, or author locally | + | `UNSUPPORTED_REMOTE_HOST` | an `origin` that is not GitHub | author locally | + + `NO_GITHUB_REMOTE` is **retained** and remains a valid member of the error-code contract, so consumers and recipes that branch on it keep working. Error codes are an agent contract: adding one is safe, renaming one is not. + + Each message now names the local authoring path that still works, so the refusal is something a reader can route around rather than a dead end. None of the three is ever reported as an authentication failure, which is pinned by a test: an agent that saw `AUTH_REQUIRED` here would send the user through `auth login`, which cannot fix any of them. + + Telling "not a git repository" from "no `origin`" needs a second question, since both fail the same `git remote get-url origin` call. That probe runs only on the failure path, so an ordinary run still spawns one process rather than two. + + `taskless info --json` now also reports `repositoryUrl` and `ghOwner`, so a caller deciding whether remote generation is available reads the same resolution the CLI enforces instead of shelling out to git and reaching a different answer. `repositoryUrl` is the canonical GitHub URL or `null`; `ghOwner` is the owner segment or the literal `[unknown]`. Both resolve without failing, including on a host with no git installed, and both are present under `--anonymous` because capability state is not auth state. `taskless auth` is unchanged and stays plain text. + + Telemetry now records `gh_owner`, so it is possible to see which GitHub owners use the CLI, including on anonymous runs. It resolves from the git remote rather than from the token, and is the owner segment when one is found or the literal `[unknown]` when not, so runs with no resolvable owner stay countable rather than disappearing from aggregates. `gh_owner` rather than `gh_org` because the first path segment of a GitHub URL is an organization or a user account, and telling them apart needs an authenticated API call an anonymous run cannot make; owner type is never inferred. + + `route` no longer offers remote generation when no GitHub owner is identifiable, reading `ghOwner` from the `info --json` call it already makes rather than re-deriving the remote, and says the tier is unavailable rather than silently dropping it. `create-remote-rule` guards the same constraint itself, before it collects anything, for the times an agent reaches it without going through `route`. Both state that `auth login` does not fix it: no GitHub owner is a property of the project, not the session. + +- 38f7676: `taskless update` now tells an agent what an upgrade changed for the rules already in a project, and records when that work is done. + + **BREAKING for anyone scripting `taskless update`.** It used to mean "reinstall the skills non-interactively", which is what `taskless init --no-interactive` already does through the same code path, and what running `taskless` does on its own. Scripts relying on the old behavior should call `taskless init --no-interactive`. + + The word is reclaimed for the job an agent actually needs. Running the CLI migrates the `.taskless/` layout and refreshes skills: that is the directory, and it is automatic. No migration can rewrite the rules themselves, and a rewriter that now requires a `fix`, or a rule whose matching semantics shifted under a new engine, is a question about content. An agent that has run a migration and watched it succeed will otherwise reasonably conclude the upgrade is finished. + + `taskless update` with no flags serves a ledger: one section per release, in order, saying what that version means for existing rules. Sections are cumulative, and a version with nothing to do says so explicitly, because an agent cannot tell "nothing here" from "nobody wrote this". The first entry covers 0.11.0: the newly required `fix` on rewriters, Markdown's block-only grammar and its two opposite failure shapes, `sg run --lang` accepting alias spellings, and the matching-semantics changes that alter what a valid rule matches with no error at all. + + `taskless update --reconciledTo=` records that the walk finished, in a new `rules` section of `.taskless/taskless.json` alongside the existing `install`. It stores the CLI version and the ast-grep and Vale versions the rules are now valid against. + + The two namespaces are separate because they drift. `install` records how the scaffold got here and moves on a skills refresh; `rules` records what the rules are valid against and moves only on a completed reconciliation. Keying rule work off `install.cliVersion` would let an agent skip entries it never performed, and it would fail quietly: the walk would report nothing to do while the rules stayed wrong. + + The version is validated rather than trusted. A value ahead of the installed CLI is rejected, since this build carries no entries for it, and the marker is never moved backwards. + + `taskless info --json` reports both namespaces, so an agent reads where to start from the same payload it already fetches. + +- 6b07695: Ship Vale as per-platform binary packages. + + The CLI now declares `@taskless/vale--` as `optionalDependencies` pinned + to 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 `vale` found on `PATH`. + +- 0e03ee9: Add Vale as a second static-tier rule engine, give every engine one rule layout, and rename the agent-facing command. + + `check` now dispatches by engine and runs ast-grep, Vale, and runtime rules + concurrently, 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///`, holding + the rule, any per-engine config, and its tests in `.tests/`. Writing a rule + means creating a directory and deleting one means `rm -rf`. Nothing outside it + is touched either way, so concurrent authors never collide on a shared file. + + Vale rules carry their own `.vale.ini` declaring 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`/`ignores` inside the rule and needs no second file. + + **`rule verify` is replaced by two path-addressed commands.** `verify ` + checks that a rule has the components its engine requires and needs no tests, + so it works while you're still authoring. `test ` runs the rule's tests, + after running `verify` and 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 ` is now `taskless agent `.** The + command 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 +create` becomes `taskless agent create-sg-rule`; multiple positionals are no + longer 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: + + | Was | Now | + | ------------------ | --------------------------------------- | + | `rule create` | `create-sg-rule` / `create-remote-rule` | + | `rule improve` | `improve-rule` | + | `rule delete` | `delete-rule` | + | `rule verify` | `verify-rule` | + | `rule meta` | `rule-meta` | + | `static` | `create-sg-rule` | + | `existing` | `create-legacy-rule` | + | `engine-selection` | `route` | + + `route` now applies the engine reasoning itself and names a concrete + `create-*-rule` topic, so `engine-selection` is removed rather than renamed — + its criterion is stated once, in `route`. Every authoring recipe is rewritten + for the rule-directory layout. + + **BREAKING for `@taskless/cli/prompts` consumers.** `engine-selection` is no + longer exported. `TOPICS` is now `create-sg-rule`, `create-vale-rule`, and + `create-runtime-rule`, so a consumer that decides an engine can reach the + procedure 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-grep` tests fail. + + `ast-grep test` echoes the source of a failing test case, and `verify` scraped + its counts with unanchored regexes over stdout and stderr combined — so a + fixture containing text like `'7 passed; 0 failed'` was read as the summary and + `verify` reported `✗ failed (7 passed, 0 failed)` for a run that actually had 0 + passed 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 also + now decoded with a `StringDecoder` per stream, so a multi-byte character split + across a chunk boundary is no longer mangled. + +- 8e4084c: Upgrade the vendored ast-grep from 0.41.0 to 0.45.2, and add `Markdown` and + `Dart` to the languages `sg` rules can target. + + **One breaking change reaches user rules.** A `rewriters:` entry now requires a + `fix:`. It was optional in 0.41.0, and the regenerated rule schema makes + `verify` name the offending rewriter directly. It cannot be migrated for you: + `fix` is replacement text, so a tool can find every affected rewriter but + cannot write one. Nothing shipped here uses `rewriters:`, so this only affects + rules you wrote yourself. Elsewhere the schema barely moves: `matches:` widens + from a plain utility-rule id to also accept a parameterized call object, which + is backward compatible, and the top-level property set is unchanged. + + **`Markdown` is narrower than the name suggests, and the routing recipes now + say so.** tree-sitter-markdown splits its grammar into block and inline halves + and ast-grep exposes only the block tree. `atx_heading`, `setext_heading`, + `fenced_code_block`, `list_item`, `paragraph`, `section` and `document` are + real kinds, and headings discriminate by level. Everything inside a line is one + opaque `inline` node: there is no `link`, `emphasis` or `strong_emphasis`. + Naming one is a config error that exits 8 and takes the whole scan with it; + writing it as a pattern instead matches nothing forever with no error at all. + So "link text must not read click here" is a Vale rule, not an `sg` rule, and + "every doc has exactly one h1" is neither, because ast-grep has no count and no + absence assertion. `.md` is the first extension both static engines claim, so + `route` now says which question each answers rather than leaving it to the file + extension. + + Also: the `sg` alias is deprecated as of ast-grep 0.45.0 and prints a banner to + stderr on every run. On a host where only `sg` resolves, that banner used to be + decoded into user-facing messages as if an engine had reported it. It is now + stripped. + +- 307fb3a: Keep a whole-project `check` out of the paths git ignores. + + `check` reported prose findings from inside gitignored directories. The case + that surfaced it was a git worktree at `worktrees//`, which is a complete + second checkout: every Vale rule fired again over another branch's documents, + including code an agent was mid-edit on. That makes the finding count move when + a worktree appears or disappears with nothing in the output explaining why, and + the general shape is the same for `dist/`, vendored trees, and local scratch + directories — a check reporting on files nobody maintains. + + Only one engine was wrong, which is why it was hard to attribute. ast-grep's + walker is the `ignore` crate and `sgWalkArgv` has always passed `--no-ignore +hidden` without `vcs`, so a bare scan already skipped `worktrees/`; measured + against ast-grep 0.41.0, it skips a hidden-_and_-ignored `.turbo/` too. Vale + has no notion of a VCS and walked everything. So the two static engines + disagreed about which files the project contains, and only the prose findings + duplicated. On a fixture repository with a worktree present, a bare `check` + went from 6 findings to 4; the two that left were both Vale, both a second copy + of a finding already reported against the tracked file. + + The set comes from `git ls-files --others --ignored --exclude-standard +--directory -z`, which is the complement of the tracked-plus-untracked set the + question is usually phrased as. The complement is the one that scales: + `--directory` collapses a wholly-ignored directory to a single entry, so + `node_modules/` costs one line rather than forty thousand, and the result is + short enough to hand Vale as `--glob` exclusions without meeting `ARG_MAX`. No + new dependency — `.gitignore` is not one file or one syntax question once + nested ignore files, `.git/info/exclude`, a global `core.excludesFile` and + negation patterns are involved, and git already answers all of it in one call. + + The exclusion belongs to the walk `check` chose for itself. `check +worktrees/probe` names an ignored path deliberately and still checks it, on the + same terms as the existing `.taskless/` exclusion. A directory that is not a + git repository, or a host with no `git` on its `PATH`, gets an empty ignore set + and the walk that shipped before this change. Standing _inside_ an ignored + directory is treated as explicit too: git answers `./` there, meaning + "everything here", and honouring that would return an empty check with nothing + saying why. + + The converter skip notice no longer names files inside ignored paths. An + `.adoc` under `worktrees/` is not a file this run declined to convert; it is a + file this run was never going to open, and naming it would send the reader to + investigate a directory the exclusion is there to keep out. + +- 1fb9dda: Rewrite the CLI README around what you actually do with Taskless: installing it, + driving it from your coding agent with the `taskless` skill and `/tskl` command, + running `taskless check` in CI, and where to find the docs. Telemetry — and the + two 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. + + `runAstGrepScan` and the runtime narrow both decoded ast-grep's stderr one + chunk at a time with `chunk.toString()`. A multi-byte UTF-8 sequence split + across 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 `verify` already 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. + + `route` chose between `sg`, `vale`, and the runtime tier on the shape of the + evidence 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 a + login, 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 + `language` as a bare string with no enum, `verify` never checks the field, and + `detect --json` reports the repo's own languages in a different vocabulary. + Vale self-reports nothing at all. + + `route` now states both engines' reach, and `create-vale-rule` repeats Vale's + where 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 `E100` and abandons the whole run, silencing every + other Vale rule over every other file. `create-vale-rule` had been offering + `[*.{md,mdx}]` as its example of widening a matcher. + + `.xml` is the one entry where naming the converter is not enough. It needs + `xsltproc` **and** an XSLT stylesheet, and a stylesheet is document-specific, so + there is nothing to ship and installing the program does not make `.xml` + lintable — unlike `asciidoctor`, which genuinely fixes `.adoc`. Vale says so + differently depending on the host, too: `xsltproc not found` where the program is + absent, `no XSLT transform provided` where it is present, and macOS ships + `/usr/bin/xsltproc` while a typical Linux CI image does not. The contract test + now asserts Vale's checker tag, which is the same everywhere, rather than a + substring of the converter name. + +- 9e886b8: Spell the telemetry property `ghOwner` rather than `gh_owner`. + + Telemetry names events in `snake_case` (`cli_run`, `cli_check_completed`) and properties in `camelCase` (`cliVersion`, `durationMs`, `errorCount`). `gh_owner` was added in the previous change with the event convention applied to a property by mistake, and it was the only property in the codebase spelled that way. + + No migration is needed for anyone reading this: the property was introduced in this same unreleased cycle, so no stable build ever emitted `gh_owner` and no saved insight can be filtering on it. + + The convention is now stated normatively in the `analytics` spec, so it can be checked rather than inferred from whichever names happen to exist. + +- 87392fa: Make `--help` work on every command, instead of running the command. + + `taskless check --help` printed no usage — it ran `check`. So did every other + subcommand: `--help` was parsed as an unrecognized flag and the command body + executed anyway, which meant asking `init` how it works installed skills, and + asking `check` how it works migrated the `.taskless/` scaffold. The only place + help worked was the bare `taskless --help`, whose own output tells you to run + `taskless --help`. + + `--help` and `-h` are now recognized at every depth, including nested commands + (`taskless auth login --help` describes `login`, not `auth`), and a working + directory passed before the command (`taskless -d ./repo check --help`) no + longer confuses which command you asked about. The usage text itself is + unchanged, and nothing else about how commands run has changed. + +- ac83a00: Hold the agent-facing recipes to the house writing style. + + `packages/cli/src/agent/*.txt` is bundled into the published CLI and served by `taskless agent `, so it is text users and agents read on every authoring run. It was the largest prose surface the house-style rules did not cover. `no-em-dashes`, `no-blocklist-phrases` and `no-hedging` now reach it, and the 270 existing em and en dashes are rewritten as periods, commas, colons or parentheses depending on what each one was doing. + + No instruction changed meaning. The recipe-content tests, which assert exact phrases from `route.txt`, `create-sg-rule.txt`, `create-vale-rule.txt` and others, all still pass. + + Two scoping notes worth knowing for anyone widening further. These files are `.txt`, which Vale treats as plain text: there is no markdown parser, so fenced blocks and code spans are **not** skipped the way they are in a `.md` file, and command examples are checked as prose. And `create-vale-rule.txt` and `verify-rule.txt` are excluded from `no-hedging`, because both teach rule authoring through a worked example named `no-simply` and the token appears throughout as an identifier rather than as hedging. + +- 32da4f9: Stop the engine-partition migration from relocating a rules tree that is already partitioned. + + A `.taskless/` with no `taskless.json` — a manifest that was never committed, or was deleted — reads as version 0, so every migration runs against it. Migration `0004` then applied its `rules/` → `sg/rules/` move to a tree already in the current layout, burying every rule at `.taskless/sg/rules/sg//`; `0005` scaffolded fresh empty engine directories over the gap. Nothing errored. `check` scanned 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. + + `0004` now 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-`0004` tree of flat `rules/.yml` files still moves wholesale, as before. And a tree holding both — an already-partitioned layout with a stray `rules/.yml` beside 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, and `0005` never brings them back, which is the same silent clean pass by another route. + +- 8d9d9cf: A project with no recorded rules marker now walks the ledger from the beginning, and `taskless update --rules` replaces `--reconciledTo=`. + + Previously an absent `rules.reconciledTo` meant "nothing to walk", on the reasoning that a project created at the installed version has no history. That was right about new projects and wrong about every existing one: a project that predates the ledger has had none of its entries applied, so reading absence as up to date silently excused exactly the population the entries were written for. The 0.11.x entry would have reached nobody. + + Absent now means `0.0.0`, so every section applies. New projects stay correct because `init` stamps the marker at creation, which is what makes the two distinguishable: present means accounted for, absent means predates the ledger. The stamp never overwrites an existing marker, so re-running setup cannot reset one a real walk earned. + + `--reconciledTo=` is replaced by the flag `--rules`, which stamps the running CLI's version. The value was never load-bearing: the CLI knows its own version, the only sensible endpoint of a walk is the installed one, and accepting a value only made it possible to claim a walk that did not finish. Removing it removes the two guards that existed to police it and every way of supplying it wrongly. The backwards guard remains, because an older CLI running on the same project would otherwise rewind the marker. + + The ledger heading is now `Migrating to 0.11.x`, since the entry describes the release series rather than one patch. + +- e25117b: Report the `.taskless/` layout migration on the `--json` envelope. + + `check`, `verify`, and `test` migrate the scaffold before they can do their real + work, and that rewrites files in the working tree: rules move into per-rule + directories, configs are deleted, `taskless.json` and `.gitignore` are rewritten. + Until now the only trace was one line of prose on stderr, so a CI script reading + `{"success":true}` had no way to learn that its checkout had changed underneath + it. The migration would then land in an unrelated commit, or run mid-suite and + fail tests that had nothing to do with the change being made. + + Those commands now carry a `migrated` field when, and only when, a migration + ran: + + ```json + { + "success": true, + "results": [], + "migrated": { + "from": 3, + "to": 5, + "applied": [4, 5], + "files": { + "added": [".taskless/rules/sg/no-eval/no-eval.yml"], + "modified": [".taskless/taskless.json"], + "removed": [".taskless/sgconfig.yml"] + } + } + } + ``` + + The field is absent when nothing happened, so presence is the signal and no + consumer has to read empty arrays to decide. Paths are relative to the project + root and sorted. + + The migration keeps happening automatically, because these commands need a known + layout to run at all and the alternative is a hard failure on every upgrade. The + human notice improved to match the new field: it names the source and target + versions up front, and prints the files it touched on completion. + +- 0cc713e: Split the release pipeline so each workflow file carries one release design. + + `release.yml` held two jobs with opposite trust properties behind one header. + It is now `release-cli-changeset.yml` — which reads contributor-authored + changesets 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.yml` is renamed `release-vale.yml` to match. + + The build and publish steps themselves are unchanged — same triggers, same + `permissions: {}`, same action pins, same OIDC trusted publishing behind the + same `npm-production` approval. Two operational details do differ: `check` and + `publish` no longer share the `release-*` concurrency group, and the release + now runs as two workflow runs instead of one, so its check contexts are + `Release CLI Version PR / …` and `Release CLI / …` rather than `Release / …`. + Neither is a required check. + + The header comments also get one correction: they claimed `npm-production` had + no 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 `main` as `@taskless/cli-nightly`. + + Every push to `main` that has changesets pending now publishes the CLI under a + second package name, stamped `-x` — so + merged-but-unreleased behavior is installable with `npx @taskless/cli-nightly`. + A nightly is the same build as the release it anticipates and keeps the + `taskless` executable, so it is a drop-in; the rename happens at pack time, so + `@taskless/cli`'s own version history stays releases-only. Installing both + globally 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@` — pinned to the + build being installed — instead of `npx @taskless/cli`. Previously a nightly + carried 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 the + same command in the same run, pinned every invocation to + `@taskless/cli-nightly@0.11.0-…`. The manifest attributed the install to a + version that never performed it, which matters because `install.cliVersion` is + what 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.json` is deliberately left untouched — so the build was + reading 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, and + the `cliVersion` telemetry property on nightly builds. Released builds are + unaffected. 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.json` instead. Deriving from one + source is not the same as being checked against it. + +- c4a252b: Onboarding now reads the routing surface before it proposes rule candidates. + + The `onboard` recipe asked the agent to synthesize its bullet list of + hypothetical rules first and consult `route` only afterwards, once per accepted + bullet. 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 route` and runs `taskless detect --json` + before proposing anything, and each bullet carries the destination it would + route to: `- no-direct-db-access [sg]: …`. The annotation is provisional — + `route` still decides for real at materialization time, when it has the rule's + full 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` → `agent` rename. The user-facing command was renamed in + 0.10.0, but the internals kept the old name: the recipe directory moved from + `packages/cli/src/help/` to `packages/cli/src/agent/`, the `cli-help` OpenSpec + capability is now `cli-agent`, and the shipped skill and `/tskl` command no + longer tell agents to run the removed `npx @taskless/cli help ` (they + now 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_help` event is renamed + to `cli_agent`. The `topic` property is unchanged. PostHog dashboards keyed on + `cli_help` will 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 + `runAstGrepScan` never told it otherwise. No `sg` rule could match anything + under `.github/`, `.circleci/`, `.vscode/` or `.husky/`, so `check` reported + nothing 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 `check` and the runtime engine's ast-grep narrow now pass + `--no-ignore hidden`. + + Only `hidden` is passed, and deliberately not `vcs`: `.gitignore` is still + respected, so the wider walk does not start reporting findings in `dist/` or + anywhere else a project has already said it does not want scanned. Rule + discovery is untouched — `ruleDirs` walks 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 and + reaching it is not a fix. A rule definition is structured YAML full of `id:`, + `language:`, `severity:` and `rule:` keys, so an ordinary user-written Yaml rule + fires 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 `check` walks the whole project on its own; an explicit path stays a + request, which is the rule the Vale runner already follows. + + `.git/` is excluded on the same terms. ast-grep has no exclusion of its own for + it and `.gitignore` does not list it, so the default hidden-directory skip was + the only thing holding it back: without this, a whole-project `check` descended + into `.git/objects` and `.git/logs` on every run, and `.git/hooks/*` scripts + matched 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 + — `check` was clean while `check .` reported findings inside `.taskless/`. Vale + was 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 `test` for an ast-grep rule that never demonstrates it can fire. + + `verify` checked that a rule's `-test.yml` existed and never read what was in + it, and `ast-grep test` reports an empty `invalid:` bucket as `1 passed; 0 +failed` and exits zero. A rule whose fixtures were all `valid:` therefore + reported `ok: true, ran: true` while `check` found nothing anywhere — verified + looking verified, having proved nothing. `test` now counts the `valid:` and + `invalid:` entries across every test file a rule owns and requires both, which + is 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 fails `test` until a fixture is added that the rule + actually 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 `smart` + strictness 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. Upstream + considers this intended and 0.45.2 behaves identically, so there is no version + to upgrade to; write the pattern as an object with `strictness: ast` to ignore + the separator, or use `any:` with one branch per arity. `verify --schema` now + carries a worked example, and the behaviour is pinned against the vendored + binary so a bump that changes it fails loudly. + + `create-sg-rule` states all of this where a pattern is written: the arity table + measured against the pinned binary, both remedies and the fact that + `strictness: ast` moves a trailing `$$$` from two arguments to one rather than + to 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 pinned + constants — nothing local validates that field, an unrecognized spelling takes + the whole scan down, and `Tsx` is a different parser from `TypeScript` rather + than an alias. `improve-rule` gains the two notes that matter when a rule is + rewritten 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 `0005` created 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 `-YYYYMMDD-test.yml` shape the migration can attribute to a rule — arrived in the new layout with no tests directory at all. Assembly then named that directory as a `testConfigs` entry anyway, and ast-grep 0.41.0 treats a `testDir` it cannot read as fatal to the whole invocation rather than to the one rule: `taskless test` on _any_ rule died with `Cannot read rule directory .taskless/rules/sg//.tests` and exit 6, naming a rule the author had never touched. `--filter` does not scope that away, so there was no way to run one rule's tests around it. + + `0005` now gives every `rules/sg//` a `.tests/`, holding a committed `.gitkeep` when 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 any `testDir` that 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 amended `0005`, 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 — `verify` still reports "No test file found" and `test` still reports "Skipped: no test file found", both reading the rule directory rather than the generated config. + +- 5031fbd: Stop deriving `--json` error codes from the text of an error message. + + `rule create` and `rule improve` chose between `AUTH_REQUIRED` and + `NO_GITHUB_REMOTE` by running `/git remote|origin/i` over the human-readable + message that `resolveIdentity` threw. The codes exist so a machine consumer + never parses English, and the code itself was being picked by parsing English. + It happened to be right only because both repository-URL messages contain the + words "git remote"; rewording or translating either one would have silently + told every `--json` consumer to log in when the real problem was the project's + git remote. + + Each failure now throws a `CLIError` carrying its own `CLIErrorCode`, and both + call sites read that field through one shared helper. No new code was added. + + The emitted codes for existing scenarios are unchanged: + + | Condition | Code | + | ------------------------------------------- | ------------------ | + | Not logged in | `AUTH_REQUIRED` | + | Not a git repository, or no `origin` remote | `NO_GITHUB_REMOTE` | + | `origin` remote is not on `github.com` | `NO_GITHUB_REMOTE` | + + One code does change, for a scenario that is unreachable today: an unexpected + throw from the org-resolution step now reports `INTERNAL_ERROR` rather than + whichever of the two codes its wording happened to match. That step swallows + every network and HTTP error and falls back to a nil-UUID org subject, so it + cannot fail in normal operation; anything escaping it is a CLI bug rather than + a state the caller can act on. + +- a596e54: Close the gaps in `create-vale-rule` that produce a rule which is green + everywhere and reports nothing. + + A malformed Vale rule fails loudly. A Vale rule that is merely _wrong_ passes + `verify`, passes `test`, and never fires — and nobody re-checks a green rule. + The recipe now documents each of those failures as an observed behavior of the + pinned Vale binary rather than as a caution in principle: + - **The measured `scope` vocabulary**, with what each value actually reaches. + `raw` subsumes `code` and `text`; `~` negation and `&` chaining are accepted; + and a negation over a scope Vale does not know (`~fenced`) is a silent no-op + that removes the exclusion you wrote the rule for. + - **`scope` is per-rule.** Taskless assembles one config per run, which invites + the assumption that scopes interact. They do not. + - **A `raw`-scoped rule cannot be suppressed** by ``, + because it reads the unparsed document — so a rule about a shell command needs + `raw` and trades away per-case exemption. + - **A punctuation-only token needs `nonword: true`**, because Vale wraps every + token in word boundaries and an em dash has no word character on either side. + - **How to scope a rule _out_**, with a second matcher assigning `NO`. + - **Collocation guidance** for a banned word, checked by writing the `pass/` + fixture from the literal sense first. + - **Fixture design for a subject that appears in code**: `fail/` must carry it + inline, fenced, and in prose. + - **`limit`** in the common-fields table, **`vocab`** with the per-check fields + it actually belongs to, and that Vale loads only `.yml` — a style file renamed + to `.yaml` is silently not loaded at all. + - **Fixtures run under an isolating config**, so a green `test` is not evidence + the rule's matcher glob reaches any real file. + + Vale's check types are now enumerated from the binary rather than the docs. + There are **twelve**, not eleven: the docs fold `readability` into `metric`. + The per-check field tables are measured the same way, which corrects three + published claims: `capitalization` takes `prefix` (singular) and rejects + `prefixes` and `suffixes`, `capitalization` rejects `ignorecase`, and + `occurrence` rejects `exceptions` and `vocab`. + + `verify` now schema-checks a Vale rule structurally, before Vale is invoked. + It previously validated `level` and the presence of the rule's `.vale.ini`, so + `extends: nonsense` and `scope: fenced` both verified clean. It now also checks: + - **`extends`** against the twelve check types, naming the accepted set. + - **`scope`** as a grammar over measured operands — a bare value, a list, `~` + negation, `&` chaining — rather than a flat enum, which would have rejected + working rules. It is deliberately stricter than Vale in one place: a negation + over an operand Vale does not know (`~fenced`) fires on everything, having + silently lost its exclusion, and is rejected. + - **Per-check fields**, so a field belonging to another check type is caught + before Vale reports `E201`. `consistency` and `spelling` are exempt because + the binary accepts any key on those two. + + The ordering is the point for two of the three: Vale reads one assembled config + per run, so an unknown `extends` or a foreign field reaching the binary takes + down **every** Vale rule's findings, not just the offending rule's. + + The schema is hand-authored, because Vale publishes no JSON Schema and its + machine-readable field knowledge is behind a paid hosted MCP. What holds it to + the binary is a corpus of 82 minimal rules, each with a document it must flag, + run through both the vendored Vale and the schema, asserting the two agree — + with guards so that a rule which "did not fire" because its fixture was + unreachable cannot pass as a measurement. A Vale upgrade that changes the + vocabulary fails a test that names the value. + +- 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`, `dita` or + `mdx2vast`, and the `@taskless/vale-*` packages ship the binary with none of + those alongside it. On a host without the converter Vale does not skip the file: + it prints one `E100 [lintAdoc] Runtime error` on stderr, writes nothing at all to + stdout, 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 `.adoc` took a check that reported five Vale findings across four files + down to zero — reported as a raw JSON blob among the results, and exiting 1 the + same way any ordinary failing check does. + + `runVale` now 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 recipes + render their format lists from — measured against the pinned binary rather than + transcribed from documentation. That is how `.asc` and `.rest`, a third AsciiDoc + spelling 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`, `.mkd` and `.mkdn` are 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. + `.mdx` gains 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. `.typ` moved the opposite way: Typst now + parses through `typst2vast`, an external program this build does not ship, so a + Typst file is excluded from the run rather than read as prose the way 3.17.1 + read it. `.rmd` and the new `.qmd` and `.myst` are parsed as markup, and `.qml`, + `.scss` and the new `.qdoc` are comment-aware where they previously fell through + to plain text. + + The `.typ` move is the one that mattered to get right. An extension missing from + the 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 `` are documented and never fire; `frontmatter` and + `frontmatter.` fire and are documented nowhere; `consistency` and `spelling` + validate no keys at all. + +- 520a382: Validate an sg rule's `language:` field in `verify`, instead of leaving it to + ast-grep at `check` time. + + Nothing local had an opinion on the field. The vendored rule schema types it as + a bare string with no enum, so `verify` returned `ok: true` for any spelling and + the binary was the first thing to object — in the two ways it objects, both of + them late: + - A name ast-grep does not recognize fails `SgLang` deserialization, which + aborts parsing of the single config Taskless assembles per run. One typo takes + every _other_ sg rule down with it. `verify` now fails that rule by name, + prints the accepted spellings, and suggests the obvious canonical one where + there is one (`C#` → `CSharp`). + - A recognized name pointing at the wrong parser reports nothing and reads as a + clean codebase. `Tsx` and `TypeScript` are two parsers, not aliases, so a + `TypeScript` rule scoped to `**/*.tsx` matches nothing and exits zero. + `verify` fails that rule, and notices the half-dead case where a `{ts,tsx}` + glob reaches both. + + Case variants and ast-grep's extension aliases are accepted rather than + rejected, since ast-grep accepts them itself: `typescript`, `TYPESCRIPT` and + `ts` all reach TypeScript. They get a notice naming the canonical spelling, so + rules already written the lowercase way — including the ones in this + repository — keep passing. + + The `files:` scan reads both shapes ast-grep allows for a glob entry, the plain + string and the `{ glob, caseInsensitive }` object, so the wrong-parser check is + not silently skipped for rules written the second way. + +- db8adfa: Resolve the ast-grep binary without relying on an install-time step, and drop + the `@ast-grep/cli` wrapper from what consumers install. + - **The wrapper moves to `devDependencies`.** The seven `@ast-grep/cli-` + packages were already declared in `optionalDependencies`, and the CLI already + resolved them by path — the wrapper was a leftover whose only job is a + `postinstall` that hardlinks the binary into itself so its `bin` entries work. + Nothing here invoked those entries. Consumers now install only the platform + package matching their host, and the wrapper's `postinstall` — which leaves a + placeholder text file where the binary should be under `pnpm dlx`'s strict + isolation — is out of the shipped product entirely. It stays as a + `devDependency` because `fetch-ast-grep-schema` reads its version. + - **Platform packages are pinned exactly.** They were carets, and the wrapper + had been enforcing alignment implicitly by pinning its own + `optionalDependencies`; without it, two hosts could resolve different ast-grep + versions against the same rules and disagree about findings. This change makes + the pin explicit without moving the version; the upgrade itself is a separate + change. + - **Binary resolution exhausts every candidate before failing.** It now searches + the platform package, `node_modules/.bin`, then `sg` and `ast-grep` on `PATH`, + and throws naming what it tried. Previously it returned a bare `"sg"` and let + `spawn`'s `ENOENT` be the error, from a caller that could not say where it had + looked. + + Alpine improves as a side effect: upstream publishes no musl build and marks its + Linux packages `libc: ["glibc"]`, so today the wrapper's `postinstall` resolves a + package that does not exist and exits 1, failing the install wherever dependency + scripts run. Installing now succeeds and resolution falls through to `PATH`. + ## 0.10.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index f99938a0..69b42e87 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@taskless/cli", - "version": "0.10.2", + "version": "0.11.0", "license": "MIT", "repository": { "type": "git", diff --git a/skills/taskless/SKILL.md b/skills/taskless/SKILL.md index c48ce5ca..ac234307 100644 --- a/skills/taskless/SKILL.md +++ b/skills/taskless/SKILL.md @@ -20,7 +20,7 @@ description: | `npx @taskless/cli agent route`; it does NOT suppress the skill. metadata: author: taskless - version: 0.10.2 + version: 0.11.0 commandName: tskl compatibility: Designed for Agents implementing the Agent Skills specification. ---