From e788c93643e3164add5ac3e5ea15acd3834e5bdb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:12:08 +0000 Subject: [PATCH] chore: version packages --- .changeset/anchor-sg-test-summary.md | 18 - .changeset/cli-readme-rewrite.md | 9 - .changeset/decode-sg-stderr.md | 17 - .changeset/engine-partitioned-layout.md | 15 - .changeset/engine-reach-in-route.md | 39 -- .changeset/export-knowledge-prompts.md | 9 - .changeset/full-cli-invocation.md | 11 - .changeset/help-flag-on-subcommands.md | 18 - .changeset/idempotent-rules-partition.md | 9 - .changeset/nightly-cli-builds.md | 56 --- .changeset/nightly-self-reported-version.md | 30 -- .changeset/onboard-route-first.md | 22 - .changeset/rename-help-to-agent.md | 15 - .changeset/scan-hidden-directories.md | 43 -- .changeset/sg-fixture-coverage.md | 42 -- .changeset/sg-rule-tests-directory.md | 9 - .changeset/vale-converter-formats.md | 86 ---- .changeset/vale-platform-packages.md | 11 - .changeset/vale-rule-engine.md | 62 --- .changeset/wild-jars-repeat.md | 31 -- .claude-plugin/plugin.json | 2 +- package.json | 2 +- packages/cli/CHANGELOG.md | 496 ++++++++++++++++++++ packages/cli/package.json | 2 +- skills/taskless/SKILL.md | 2 +- 25 files changed, 500 insertions(+), 556 deletions(-) delete mode 100644 .changeset/anchor-sg-test-summary.md delete mode 100644 .changeset/cli-readme-rewrite.md delete mode 100644 .changeset/decode-sg-stderr.md delete mode 100644 .changeset/engine-partitioned-layout.md delete mode 100644 .changeset/engine-reach-in-route.md delete mode 100644 .changeset/export-knowledge-prompts.md delete mode 100644 .changeset/full-cli-invocation.md delete mode 100644 .changeset/help-flag-on-subcommands.md delete mode 100644 .changeset/idempotent-rules-partition.md delete mode 100644 .changeset/nightly-cli-builds.md delete mode 100644 .changeset/nightly-self-reported-version.md delete mode 100644 .changeset/onboard-route-first.md delete mode 100644 .changeset/rename-help-to-agent.md delete mode 100644 .changeset/scan-hidden-directories.md delete mode 100644 .changeset/sg-fixture-coverage.md delete mode 100644 .changeset/sg-rule-tests-directory.md delete mode 100644 .changeset/vale-converter-formats.md delete mode 100644 .changeset/vale-platform-packages.md delete mode 100644 .changeset/vale-rule-engine.md delete mode 100644 .changeset/wild-jars-repeat.md 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/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/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/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/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/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/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/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/wild-jars-repeat.md b/.changeset/wild-jars-repeat.md deleted file mode 100644 index 488fdb1e..00000000 --- a/.changeset/wild-jars-repeat.md +++ /dev/null @@ -1,31 +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 at `0.41.0`.** 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. Held at `0.41.0` - rather than taking upstream's `0.45.0`, so this change stays structural. -- **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..f8174968 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,501 @@ # @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. + +- 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. + +- 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. + +- 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. + +- 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. + +- 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. + +- 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 `` + 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 at `0.41.0`.** 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. Held at `0.41.0` + rather than taking upstream's `0.45.0`, so this change stays structural. + - **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 990d2d27..693fab3b 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 ab04b829..16cf8f30 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. ---