From 0fa878e819534ac0000a04be88552efdcc8acb36 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 29 Sep 2026 22:35:28 -0700 Subject: [PATCH 1/5] revert: undo the unpublished 0.11.3 version bump #358 (chore: version packages) consumed 17 changesets into a 0.11.3 CHANGELOG section and bumped to 0.11.3, but the publish job was never approved, so 0.11.3 never reached npm. Its changes first ship in 0.12.0, so their notes belong there. This reverts a1ad90f: the changesets come back, the 0.11.3 section goes, and the version returns to 0.11.2, which the release PR then bumps to 0.12.0 with every change listed. This reverts commit a1ad90f. --- .changeset/check-rule-filter.md | 5 + .changeset/detect-oxlint.md | 5 + .changeset/feedback-survey-0-11-3.md | 5 + .changeset/migration-9-atomicity.md | 15 ++ .changeset/migration-failure-reporting.md | 7 + .changeset/notice-list-contract.md | 11 ++ .changeset/onboard-host-tool-detection.md | 38 +++++ .changeset/rule-id-uniqueness.md | 15 ++ .changeset/sg-fixture-findings.md | 20 +++ .changeset/test-show-fixture-findings.md | 17 ++ .changeset/vale-3-22-0.md | 22 +++ .changeset/vale-config-schema.md | 9 + .changeset/vale-raw-authoring.md | 5 + .changeset/vale-recipe-regex-and-fixtures.md | 7 + .changeset/vale-size-guard.md | 29 ++++ .changeset/vale-swap-backreference.md | 5 + .changeset/version-flag.md | 5 + .claude-plugin/plugin.json | 2 +- package.json | 2 +- packages/cli/CHANGELOG.md | 168 ------------------- packages/cli/package.json | 2 +- skills/taskless/SKILL.md | 2 +- 22 files changed, 224 insertions(+), 172 deletions(-) create mode 100644 .changeset/check-rule-filter.md create mode 100644 .changeset/detect-oxlint.md create mode 100644 .changeset/feedback-survey-0-11-3.md create mode 100644 .changeset/migration-9-atomicity.md create mode 100644 .changeset/migration-failure-reporting.md create mode 100644 .changeset/notice-list-contract.md create mode 100644 .changeset/onboard-host-tool-detection.md create mode 100644 .changeset/rule-id-uniqueness.md create mode 100644 .changeset/sg-fixture-findings.md create mode 100644 .changeset/test-show-fixture-findings.md create mode 100644 .changeset/vale-3-22-0.md create mode 100644 .changeset/vale-config-schema.md create mode 100644 .changeset/vale-raw-authoring.md create mode 100644 .changeset/vale-recipe-regex-and-fixtures.md create mode 100644 .changeset/vale-size-guard.md create mode 100644 .changeset/vale-swap-backreference.md create mode 100644 .changeset/version-flag.md diff --git a/.changeset/check-rule-filter.md b/.changeset/check-rule-filter.md new file mode 100644 index 00000000..5dd79af5 --- /dev/null +++ b/.changeset/check-rule-filter.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`taskless check --rule ` (repeatable) restricts a run to the named rules, so a rule can be measured over the whole project without running every other rule and filtering the JSON afterwards. The filter applies to both static engines and to runtime rules, keeps every exclusion a whole-project run applies, and refuses an id no rule directory has. diff --git a/.changeset/detect-oxlint.md b/.changeset/detect-oxlint.md new file mode 100644 index 00000000..ae91bc52 --- /dev/null +++ b/.changeset/detect-oxlint.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`taskless detect` recognizes oxlint alongside eslint, biome and stylelint, from `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, `oxlint.config.mts`, or an `oxlint` entry in `package.json`. diff --git a/.changeset/feedback-survey-0-11-3.md b/.changeset/feedback-survey-0-11-3.md new file mode 100644 index 00000000..78d837f9 --- /dev/null +++ b/.changeset/feedback-survey-0-11-3.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +The feedback survey is replaced for 0.11.3. Only the kind of rule the user was trying to create is required now; the user's own words, the completion verdict, what worked, what did not, the agent in use, and the most valuable rule so far are all optional. A `skip` at the invite no longer dismisses the survey: the agent sends its own account of the session and leaves the user's words out. `feedback dismiss` is reserved for a user who asks that nothing be sent. Every install is invited once more, since the new survey keeps its own cadence. diff --git a/.changeset/migration-9-atomicity.md b/.changeset/migration-9-atomicity.md new file mode 100644 index 00000000..f46e932b --- /dev/null +++ b/.changeset/migration-9-atomicity.md @@ -0,0 +1,15 @@ +--- +"@taskless/cli": patch +--- + +Scaffold migration `9` — the one that renames a rule id held by more than one engine — now survives being interrupted, and no longer double-suffixes a fixture you had already named `-sg-…`. + +Migration 9 has not been in a released version, so nothing on disk anywhere was produced by the old behaviour and there is no repair step to run. `latest` is `0.11.2`, tagged 2026-09-19; the migration landed 2026-09-22. + +**It could not resume.** It renamed the rule directory first and then chased the files inside it, but renaming the directory is what resolves the collision, and the migration returns early when no collision is left. So a run that died in between — a `Ctrl-C`, a full disk, an editor holding a file open — left `sg/no-eval-sg/` containing `no-eval.yml` with `id: no-eval` and fixtures still under the `no-eval-` prefix. `verify` reported that as broken, and running `taskless init` again fixed nothing, because every later run found no collision and returned. + +The order is reversed: the rule file, its `id:` field, the `.tests/` fixtures and a Vale rule's `.vale.ini` are all rewritten under the old directory name, and the directory rename is the last thing to happen. A directory rename is a single atomic operation, so it is the moment a rule is done. A rule interrupted before it still collides and is picked up by the next run; a rule interrupted after it is already whole. Each inner step also tolerates having already run, and every file rewrite is committed by renaming a temporary sibling, so an interrupted write cannot truncate a rule file. + +One asymmetry can survive an interruption. Where `sg` and `vale` both hold an id, a complete run moves both and neither keeps the bare id. If a run is interrupted between the two halves, the half that finished keeps its suffix and the other keeps the bare id, because the collision it would have been renamed for is gone. The tree is collision-free and every rule is internally consistent; only the symmetry is lost. Rename it yourself if you want the pair to match. + +**A fixture already named `-sg-…` is no longer renamed again.** The predicate picking fixtures to rename matched every name it produced, so `no-eval-sg-basic-test.yml` in `sg/no-eval/.tests/` came out as `no-eval-sg-sg-basic-test.yml` on the first run. Such a fixture is already at the right prefix, so it now keeps its name and only its `id:` field follows — which still matters, since ast-grep attributes cases by the `id:` inside the file and a stale one reads as a rule that shipped no cases. diff --git a/.changeset/migration-failure-reporting.md b/.changeset/migration-failure-reporting.md new file mode 100644 index 00000000..8aa0ed44 --- /dev/null +++ b/.changeset/migration-failure-reporting.md @@ -0,0 +1,7 @@ +--- +"@taskless/cli": patch +--- + +A failing schema migration is now reported once instead of twice, and the message names the migration that refused. `runMigrations` used to print `Migration N failed: ` itself and then rethrow the original, so the caller printed the same text again without the migration number — the longer and more useful the refusal, the worse it read. The prefix now rides on the rethrown error, so there is one string and one printer, and the original `CLIError` code (for example `SCAFFOLD_CONFLICT`) is preserved. + +`taskless init --json` also emits an error envelope when a migration fails. It previously wrote nothing at all to stdout and put prose on stderr, leaving a machine consumer with only the exit code; it now writes the standard `{ ok: false, code, message }` envelope, with the migration number in `message`. diff --git a/.changeset/notice-list-contract.md b/.changeset/notice-list-contract.md new file mode 100644 index 00000000..e34c64d1 --- /dev/null +++ b/.changeset/notice-list-contract.md @@ -0,0 +1,11 @@ +--- +"@taskless/cli": patch +--- + +`taskless check` now marks every notice it prints. A run with more than one advisory used to print the first behind a `Notice: ` marker and the rest as bare, unindented lines with nothing identifying them as notices — so a Vale config advisory sitting beside Vale's own diagnostic read as stray output. `verify` had the same defect and it was fixed earlier; `check` did not get the fix until now. + +A second, related gap in the same output: the notices from runtime rule planning — what was repaired, what could not be, and why — were printed by their own loop with no `Notice: ` marker at all, while engine notices were marked. `check --json` merges both into one `notices` array, so the same message looked like two different kinds of thing depending on which list it arrived on. Every notice `check` prints is now marked, and every line of one is. + +The cause was that notices were joined into one string before they reached the renderer, so `check --json` also published array elements that were several notices glued together, with no separator a consumer could rely on to split them back apart. Notices are now carried as a list from producer to output: in `check --json` the `notices` array keeps its name and type, and only its element boundaries change — one element is now exactly one notice. + +**What a consumer crosses:** the optional `notice` string on `verify --json` and `test --json` per-rule results is now a `notices` array of strings, present and empty rather than absent when there is nothing to say. The same replacement applies to the exported `verifyOutputSchema` (its `schema` layer) and `valeVerifyOutputSchema`. Read `notices` where you read `notice`, and render one marker per element instead of splitting on a separator. It was replaced rather than mirrored because a joined `notice` kept alongside would preserve the convention this change removes, and nothing ever published the separator that would have made splitting it safe. `check --json` consumers need change nothing. diff --git a/.changeset/onboard-host-tool-detection.md b/.changeset/onboard-host-tool-detection.md new file mode 100644 index 00000000..56565dd3 --- /dev/null +++ b/.changeset/onboard-host-tool-detection.md @@ -0,0 +1,38 @@ +--- +"@taskless/cli": patch +--- + +`taskless info --json` renames its `tools` key to `harnesses`, and gives +`tools` to the command-line binaries found on `PATH`. + +**This is the consumer-visible part.** The array that lists Claude Code, +Codex, Cursor and OpenCode, with each one's installed skills and their +staleness, is unchanged in shape — it now lives under `harnesses`. Anything +reading `.tools[].skills` from `info --json` reads `.harnesses[].skills` +instead. The new `tools` array carries `{ name, present, applicable, path? }` +for `gh`, `git` and `jq`. + +`present` is established by looking for a file of that name on `PATH`. +Taskless does not spawn a detected binary, does not read its version, and does +not hash it, so `present: true` is presence and not a working install. A +sha256-against-published-releases tier was considered and dropped on +measurement: GitHub publishes digests for `gh`'s release archives and +installers rather than for the extracted binary, and a Homebrew-installed `gh` +2.97.0 matched 0 of the 21 official digests — a tier that reports "unverified" +for the ordinary macOS install path is worse than no tier at all. + +`applicable` is the separate question of whether a tool could accomplish +anything where it is being asked to. `gh` in a repository with no GitHub +`origin` is present and inapplicable, and reporting that as "missing" would +produce the one instruction that cannot help — "install `gh`". + +The `onboard` recipe (topic v4) uses both. Its source menu now states what was +found rather than telling the agent to run `command -v gh`, says in one line +why a source is not offered instead of dropping it silently, and no longer +names Linear as the expected issue tracker: a bug-tracker scan is offered when +the agent has an MCP that reaches one, with Jira and Linear as examples of the +class. `@taskless/cli/prompts` is unaffected — with no host state supplied the +recipe renders its full menu, unchanged. + +`patch` rather than `minor`: the package is `0.y.z`, where semver puts added +surface outside the stability guarantee. diff --git a/.changeset/rule-id-uniqueness.md b/.changeset/rule-id-uniqueness.md new file mode 100644 index 00000000..f4e329cf --- /dev/null +++ b/.changeset/rule-id-uniqueness.md @@ -0,0 +1,15 @@ +--- +"@taskless/cli": patch +--- + +Two rules can no longer share an id across engines. `verify` fails a rule whose id is also a directory name under another engine, and a new scaffold migration (`9`) renames the ones that already exist. + +`.taskless/rules/sg/no-eval/` beside `.taskless/rules/vale/no-eval/` was silent: `check`'s human output prints `error[no-eval]` with no engine, so a collision shows two identical lines, and every id-addressed command had two answers to choose between. + +**Your rule ids may change on upgrade, and `check` output changes with them.** The first `taskless init` after upgrading renames the colliding `sg` and `vale` copies to `-` — `sg/no-eval` becomes `sg/no-eval-sg`, `vale/no-eval` becomes `vale/no-eval-vale`. Where both move, neither keeps the bare id, so nobody has to work out which of their two rules kept the name. If `-` is already taken it uses the next free `--2`, `-3`, … and never overwrites an existing rule. + +**A `runtime` rule is never renamed** and keeps the bare id, so a collision between `runtime` and another engine moves only the other one. Runtime rules are the signed and blessed tier, and this keeps the upgrade clear of that machinery. Nothing is left colliding either way, because one engine can only hold one directory per id. + +Everything the rename touches is inside the rule's own directory: the directory name, the rule file, its `id:` field, an sg rule's `.tests/` fixtures and their `id:` fields, and a Vale rule's `.vale.ini` breadcrumb and both segments of its `.` assignment. Every rename is printed — old path, new path, and each file rewritten — as is any runtime rule that kept its id, so you can see exactly what moved before committing it. Update any CI config, baseline file or suppression comment that names an old id — including `check --rule `, which errors with `RULE_NOT_FOUND` rather than reporting zero findings when the id it names has been renamed out from under it. + +`.taskless/rule-metadata/.yml` is left where it is rather than following either rule, since a symmetric rename gives it no owner. In practice there is nothing there: this CLI has never written a sidecar, because the service does not return the metadata block they are written from. diff --git a/.changeset/sg-fixture-findings.md b/.changeset/sg-fixture-findings.md new file mode 100644 index 00000000..154574ba --- /dev/null +++ b/.changeset/sg-fixture-findings.md @@ -0,0 +1,20 @@ +--- +"@taskless/cli": patch +--- + +`test` now reports the findings an ast-grep rule's fixtures produced, with the +message as ast-grep rendered it. Previously only Vale and runtime rules carried +`findings`; an ast-grep rule reported an empty array, so a rule whose message +interpolated its metavariables in the wrong order fired in exactly the right +places and was reported green. + +Each snippet is replayed through `ast-grep scan --stdin`, so the language comes +from the rule's own `language:` key and no temporary file is written. A finding +names the test YAML that declares the snippet, at the snippet's real line and +column in that file. + +Additive, and `patch` under the pre-1.0 rule: the `findings` array was already +present on every rule result and documented as possibly empty, so nothing a +consumer reads changes shape. Vale and runtime behaviour is untouched, and the +verdict `ast-grep test` decides is unchanged — findings are gathered after it +and cannot alter it. diff --git a/.changeset/test-show-fixture-findings.md b/.changeset/test-show-fixture-findings.md new file mode 100644 index 00000000..7e94cbe3 --- /dev/null +++ b/.changeset/test-show-fixture-findings.md @@ -0,0 +1,17 @@ +--- +"@taskless/cli": patch +--- + +`test --json` now reports the findings a rule's fixtures produced. + +Each rule result carries a `findings` array: the same finding shape `check --json` prints — `source`, `ruleId`, `severity`, `message`, `file`, `range`, `matchedText`, and the optional `note` and `fix` — plus a `bucket` of `"pass"` or `"fail"` naming the fixture that produced it. The rendered `message` is the point. A rule whose message interpolates its captures can have the slots in the wrong order, fire on every `fail/` fixture, stay quiet on every `pass/` one, and be reported as a rule that passed; the rendered message is the only evidence otherwise, and until now the only way to see it was a second `check` run against a fixture path you had to construct yourself. + +The array is **always present and empty rather than absent** — for a rule that produced nothing, one whose verification failed before its fixtures ran, a refused runtime rule, `verify` (which runs no fixtures at all), and ast-grep rules, whose findings are not surfaced yet. Fail-bucket findings are reported on a passing run too, since that is the only run that produces them. + +On the human path a passing rule still prints one line. A **failing** rule now prints the findings that bear on the failure beneath it, labelled by bucket and rendered the way `check` renders a finding. That includes pass-bucket findings: `pass fixture wrongly fired: ` said _that_ it happened and never what matched. + +Vale and runtime rules only. ast-grep follows separately: the vendored binary's `sg test` has no `--json` and no output-format flag, and its fixtures are inline YAML scalars rather than files. + +No flag was added, and nothing new is executed: the findings were already in hand and were being discarded. + +**If you consume `@taskless/cli/schemas`:** `zod`'s `.parse()` strips keys the schema does not declare, so a consumer still on the previously published `verifyTestOutputSchema` will silently drop `findings` from the payload it returns until the dependency is upgraded. Nothing breaks — but the field simply not being there, on a CLI that is emitting it, is the kind of thing that generates a bug report. diff --git a/.changeset/vale-3-22-0.md b/.changeset/vale-3-22-0.md new file mode 100644 index 00000000..442de221 --- /dev/null +++ b/.changeset/vale-3-22-0.md @@ -0,0 +1,22 @@ +--- +"@taskless/cli": patch +--- + +Update the bundled Vale to 3.22.0. + +For a rule under `.taskless/rules/vale/`, what you can now write: + +- `scope: text & ~link` (and `~strong`, `~emphasis`, `~code`) runs on the paragraph and blanks the element's text out of it before the rule sees it, so a wording or casing rule can leave link text and bold terms alone without giving up the sentence around them. Positions after the blanked element do not move. The release note's `text.raw` and `paragraph.link` spellings are not scopes; `verify` rejects them, so write the bare inline name. +- `split: true` on a `spelling` rule checks the parts of an identifier (`getHTTPResponsze_v2` reports `Responsze`) and places each at its own position. 3.21.0 accepted the key and did neither. +- A `frontmatter` or `frontmatter.` rule reports every occurrence in a field at the field's own position; a token appearing twice in one field was reported once. +- A one-line MDX element's text (`...`) is linted. + +What changes for a rule you already have: + +- **`BasedOnStyles =` is removed from every rule's `.vale.ini`, by migration 0008 on the next `init`, and `verify` now rejects the key with any value (`vale-config-no-based-on-styles`).** Every earlier version of the `create-vale-rule` recipe wrote the line into every matcher, where it was inert: no bundled style loads unless a run-level `BasedOnStyles` names one, and the assembled header names none. On 3.22.0 an empty value clears every setting a file inherited from an earlier matcher, and the assembled run config is every rule's matchers in id order, so a rule writing the line under `[docs/**]` silenced every alphabetically earlier rule under `docs/`, and `[*.md]` beside another rule's `[*.{md,markdown}]` silenced the first on every `.md` file, with nothing reported. `check` and `verify` refuse to run until `init` has migrated the configs, and name it; commit the rewritten files. A migrated config enables exactly what the old one did on 3.21.0. +- A rule whose `scope` negates `link`, `strong`, `emphasis` or `code` no longer sees that element's text inside a paragraph. Through 3.21.0 the paragraph still carried it, so findings inside those elements disappear. Drop the negation if the rule was meant to reach them. +- The isolating config `test` and `verify` build no longer writes `BasedOnStyles =`. Measured on both binaries, nothing fires without it and `Vale.Spelling` never could, so fixtures behave as before. + +Not changed for a rule this CLI assembles: a `[formats]` key may now be a file name or a glob, but no rule config can carry one and the assembled header writes none, so the extension still decides the parser. `UNSET` as a rule's value behaves as `NO` and is not accepted; `YES` and `NO` remain the two values. + +`taskless agent update` carries the same list, with what to do about each. diff --git a/.changeset/vale-config-schema.md b/.changeset/vale-config-schema.md new file mode 100644 index 00000000..24f7ac37 --- /dev/null +++ b/.changeset/vale-config-schema.md @@ -0,0 +1,9 @@ +--- +"@taskless/cli": patch +--- + +`verify` validates a Vale rule's `.vale.ini` against a schema and names the constraint each rejection violates. The config is parsed into an ordered structure and checked there: an assignment above the first matcher, a matcher without its `tskl) rule` breadcrumb, a key naming another rule, a value other than `YES`/`NO`, a `BasedOnStyles` assignment with any value, a config with no matcher or no `YES`, and a `NO` matcher that precedes every `YES` (both judged by each matcher's final verdict, so a `YES` a later `NO` in the same matcher overrides does not count) are each rejected under a `vale-config-*` constraint that `verify --json` reports in `violations[]` and `reference.json` publishes. A repeated key, a `[*]` matcher, and a `.taskless/**` matcher are reported as a notice without failing the rule. + +`check` runs the same schema before assembling the Vale run config, and a rejected config refuses the Vale engine for that run: the failure names the rule and the line, reaches the exit code, and ast-grep still runs. A rule is never silently left out of the assembled config. Accepted configs are written verbatim under their breadcrumb, so the one string edit assembly used to make (dropping a copied-in `StylesPath`) is gone; that line is now a rejection. Advisories reach `check`'s notices. To find every rejected line at once, run `taskless verify`. + +This is still `patch`. The package is `0.y.z`, and every config the schema refuses was already being misread by Vale: a rule enabled nowhere with a `W101` on stderr, a rule silently overriding a neighbour, a disable the following enable cancelled. The release surfaces a defect the consumer already had rather than introducing one, the same call as linting files over 128 KB again in 0.11.3. diff --git a/.changeset/vale-raw-authoring.md b/.changeset/vale-raw-authoring.md new file mode 100644 index 00000000..7d21f986 --- /dev/null +++ b/.changeset/vale-raw-authoring.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`verify` warns when a Vale rule's `raw` list has more than one entry, since Vale concatenates them into one pattern rather than alternating them; the `create-vale-rule` recipe explains the `(a|b)` form. The warning rides on `notice` and does not fail the rule. diff --git a/.changeset/vale-recipe-regex-and-fixtures.md b/.changeset/vale-recipe-regex-and-fixtures.md new file mode 100644 index 00000000..220bb018 --- /dev/null +++ b/.changeset/vale-recipe-regex-and-fixtures.md @@ -0,0 +1,7 @@ +--- +"@taskless/cli": patch +--- + +`agent create-vale-rule` (topic v13) corrects two claims that cost rule authors work. Vale patterns are not RE2-only: Vale compiles with Go's `regexp` and falls back to `regexp2`, so lookahead, lookbehind and backreferences work, and the recipe no longer tells you to split a rule that one pattern expresses. Two silent limits are measured and documented alongside it: a backreference does nothing as a `swap` key, and the implicit word boundary on `tokens`/`swap` lands after a trailing lookahead, so that lookahead has to peek at a non-word character. `check` on a rule's `.tests/fail` bucket is a supported way to read a rendered message, because `.taskless/` is excluded from the whole-project walk only; when that bucket comes back empty, the recipe now sends you to the rule's own config, specifically a `[.taskless/**]` matcher, before the pattern. + +`verify` carried the same imprecision and now states both halves: a `[.taskless/**]` matcher is unnecessary on a whole-project check, AND it silences the rule on a path you name, such as the rule's own fixture bucket. It was previously described as acting only under a bare `vale` invocation, which read as harmless. `agent update` (topic v10) is corrected to match. diff --git a/.changeset/vale-size-guard.md b/.changeset/vale-size-guard.md new file mode 100644 index 00000000..b81c82cc --- /dev/null +++ b/.changeset/vale-size-guard.md @@ -0,0 +1,29 @@ +--- +"@taskless/cli": patch +--- + +`check` no longer skips Vale target files over 128 KB. The guard +(`VALE_MAX_FILE_BYTES`, added in 0.11.2) was written against Vale 3.20.0, +whose lint time grew superlinearly with the size of a single Markdown block, +so one large file could consume the run's whole timeout and, because Vale +writes nothing until the run finishes, cost every other file its findings. +The same release moved the vendored Vale to 3.21.0, whose perf work makes that +cost linear regardless of block structure, so the cap no longer separates a +cheap file from an expensive one. Re-measured on the reproduction from +taskless/cli#325 against the vendored 3.21.0 binary (darwin/arm64, warm, median +of three): + +| fixture | Vale 3.20.0 | Vale 3.21.0 | +| ------------------------------- | ----------- | ----------- | +| 3.2 MB, one block (`huge.md`) | ~81,000 ms | ~230 ms | +| 3.2 MB, blank-line separated | ~4,400 ms | ~290 ms | +| 128 KB, one block (the old cap) | ~770 ms | ~26 ms | +| 25 MB, one block | — | ~2,200 ms | + +What a user sees: a file that 0.11.2 named in a `Vale did not check N file(s) +over 131072 bytes` notice is linted again and produces findings; the notice is +gone. The per-file retry for a target whose front matter Vale cannot parse +(taskless/cli#300) is unchanged, as is the 60 s run timeout. Vale still emits +nothing until the run completes, so a run killed by an external time limit +still loses every finding; with linear cost that takes a file in the hundreds +of megabytes rather than the hundreds of kilobytes. diff --git a/.changeset/vale-swap-backreference.md b/.changeset/vale-swap-backreference.md new file mode 100644 index 00000000..d6b47744 --- /dev/null +++ b/.changeset/vale-swap-backreference.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`verify` now rejects a Vale `substitution` rule whose `swap` key carries a backreference. No capture group survives a swap key: Vale compiles a rule's keys into one alternation, wrapping each in a capture group of its own and rewriting the author's groups to non-capturing, so `\1` refers to Vale's wrapper and matches nothing. Vale reports none of this, so the rule loads, runs and silently never fires. The detector is character-class aware, since `\1` inside `[…]` is an octal escape and works. `$1` in the swap _value_ is unaffected and still works. The `create-vale-rule` topic goes to v14: the claim that Vale tries Go's `regexp` before falling back to `regexp2` is removed (there is no fallback; it compiles with `regexp2` unconditionally), the `swap` constraint is generalised, and the leading-lookbehind mirror of the trailing-lookahead limit is documented. diff --git a/.changeset/version-flag.md b/.changeset/version-flag.md new file mode 100644 index 00000000..554623a1 --- /dev/null +++ b/.changeset/version-flag.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`taskless --version` and `-v` print the version. Before, both fell through to the full usage banner, and `-v` was not recognised at all. diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5590eaf1..df2e100b 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.11.3", + "version": "0.11.2", "author": { "name": "Taskless" }, diff --git a/package.json b/package.json index 33df544a..bb0fbb8e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@taskless/skills", - "version": "0.11.3", + "version": "0.11.2", "license": "MIT", "repository": "taskless/cli.git", "scripts": { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index baec666f..d6c02c9a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,173 +1,5 @@ # @taskless/cli -## 0.11.3 - -[Compare with v0.11.2](https://github.com/taskless/cli/compare/v0.11.2...v0.11.3) - -### Patch Changes - -- aa49d45: `taskless check --rule ` (repeatable) restricts a run to the named rules, so a rule can be measured over the whole project without running every other rule and filtering the JSON afterwards. The filter applies to both static engines and to runtime rules, keeps every exclusion a whole-project run applies, and refuses an id no rule directory has. -- d7b33ab: `taskless detect` recognizes oxlint alongside eslint, biome and stylelint, from `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, `oxlint.config.mts`, or an `oxlint` entry in `package.json`. -- 5047148: The feedback survey is replaced for 0.11.3. Only the kind of rule the user was trying to create is required now; the user's own words, the completion verdict, what worked, what did not, the agent in use, and the most valuable rule so far are all optional. A `skip` at the invite no longer dismisses the survey: the agent sends its own account of the session and leaves the user's words out. `feedback dismiss` is reserved for a user who asks that nothing be sent. Every install is invited once more, since the new survey keeps its own cadence. -- dcfb546: Scaffold migration `9` — the one that renames a rule id held by more than one engine — now survives being interrupted, and no longer double-suffixes a fixture you had already named `-sg-…`. - - Migration 9 has not been in a released version, so nothing on disk anywhere was produced by the old behaviour and there is no repair step to run. `latest` is `0.11.2`, tagged 2026-09-19; the migration landed 2026-09-22. - - **It could not resume.** It renamed the rule directory first and then chased the files inside it, but renaming the directory is what resolves the collision, and the migration returns early when no collision is left. So a run that died in between — a `Ctrl-C`, a full disk, an editor holding a file open — left `sg/no-eval-sg/` containing `no-eval.yml` with `id: no-eval` and fixtures still under the `no-eval-` prefix. `verify` reported that as broken, and running `taskless init` again fixed nothing, because every later run found no collision and returned. - - The order is reversed: the rule file, its `id:` field, the `.tests/` fixtures and a Vale rule's `.vale.ini` are all rewritten under the old directory name, and the directory rename is the last thing to happen. A directory rename is a single atomic operation, so it is the moment a rule is done. A rule interrupted before it still collides and is picked up by the next run; a rule interrupted after it is already whole. Each inner step also tolerates having already run, and every file rewrite is committed by renaming a temporary sibling, so an interrupted write cannot truncate a rule file. - - One asymmetry can survive an interruption. Where `sg` and `vale` both hold an id, a complete run moves both and neither keeps the bare id. If a run is interrupted between the two halves, the half that finished keeps its suffix and the other keeps the bare id, because the collision it would have been renamed for is gone. The tree is collision-free and every rule is internally consistent; only the symmetry is lost. Rename it yourself if you want the pair to match. - - **A fixture already named `-sg-…` is no longer renamed again.** The predicate picking fixtures to rename matched every name it produced, so `no-eval-sg-basic-test.yml` in `sg/no-eval/.tests/` came out as `no-eval-sg-sg-basic-test.yml` on the first run. Such a fixture is already at the right prefix, so it now keeps its name and only its `id:` field follows — which still matters, since ast-grep attributes cases by the `id:` inside the file and a stale one reads as a rule that shipped no cases. - -- 5ac99d2: A failing schema migration is now reported once instead of twice, and the message names the migration that refused. `runMigrations` used to print `Migration N failed: ` itself and then rethrow the original, so the caller printed the same text again without the migration number — the longer and more useful the refusal, the worse it read. The prefix now rides on the rethrown error, so there is one string and one printer, and the original `CLIError` code (for example `SCAFFOLD_CONFLICT`) is preserved. - - `taskless init --json` also emits an error envelope when a migration fails. It previously wrote nothing at all to stdout and put prose on stderr, leaving a machine consumer with only the exit code; it now writes the standard `{ ok: false, code, message }` envelope, with the migration number in `message`. - -- 1800521: `taskless check` now marks every notice it prints. A run with more than one advisory used to print the first behind a `Notice: ` marker and the rest as bare, unindented lines with nothing identifying them as notices — so a Vale config advisory sitting beside Vale's own diagnostic read as stray output. `verify` had the same defect and it was fixed earlier; `check` did not get the fix until now. - - A second, related gap in the same output: the notices from runtime rule planning — what was repaired, what could not be, and why — were printed by their own loop with no `Notice: ` marker at all, while engine notices were marked. `check --json` merges both into one `notices` array, so the same message looked like two different kinds of thing depending on which list it arrived on. Every notice `check` prints is now marked, and every line of one is. - - The cause was that notices were joined into one string before they reached the renderer, so `check --json` also published array elements that were several notices glued together, with no separator a consumer could rely on to split them back apart. Notices are now carried as a list from producer to output: in `check --json` the `notices` array keeps its name and type, and only its element boundaries change — one element is now exactly one notice. - - **What a consumer crosses:** the optional `notice` string on `verify --json` and `test --json` per-rule results is now a `notices` array of strings, present and empty rather than absent when there is nothing to say. The same replacement applies to the exported `verifyOutputSchema` (its `schema` layer) and `valeVerifyOutputSchema`. Read `notices` where you read `notice`, and render one marker per element instead of splitting on a separator. It was replaced rather than mirrored because a joined `notice` kept alongside would preserve the convention this change removes, and nothing ever published the separator that would have made splitting it safe. `check --json` consumers need change nothing. - -- 6c3eff7: `taskless info --json` renames its `tools` key to `harnesses`, and gives - `tools` to the command-line binaries found on `PATH`. - - **This is the consumer-visible part.** The array that lists Claude Code, - Codex, Cursor and OpenCode, with each one's installed skills and their - staleness, is unchanged in shape — it now lives under `harnesses`. Anything - reading `.tools[].skills` from `info --json` reads `.harnesses[].skills` - instead. The new `tools` array carries `{ name, present, applicable, path? }` - for `gh`, `git` and `jq`. - - `present` is established by looking for a file of that name on `PATH`. - Taskless does not spawn a detected binary, does not read its version, and does - not hash it, so `present: true` is presence and not a working install. A - sha256-against-published-releases tier was considered and dropped on - measurement: GitHub publishes digests for `gh`'s release archives and - installers rather than for the extracted binary, and a Homebrew-installed `gh` - 2.97.0 matched 0 of the 21 official digests — a tier that reports "unverified" - for the ordinary macOS install path is worse than no tier at all. - - `applicable` is the separate question of whether a tool could accomplish - anything where it is being asked to. `gh` in a repository with no GitHub - `origin` is present and inapplicable, and reporting that as "missing" would - produce the one instruction that cannot help — "install `gh`". - - The `onboard` recipe (topic v4) uses both. Its source menu now states what was - found rather than telling the agent to run `command -v gh`, says in one line - why a source is not offered instead of dropping it silently, and no longer - names Linear as the expected issue tracker: a bug-tracker scan is offered when - the agent has an MCP that reaches one, with Jira and Linear as examples of the - class. `@taskless/cli/prompts` is unaffected — with no host state supplied the - recipe renders its full menu, unchanged. - - `patch` rather than `minor`: the package is `0.y.z`, where semver puts added - surface outside the stability guarantee. - -- d41576f: Two rules can no longer share an id across engines. `verify` fails a rule whose id is also a directory name under another engine, and a new scaffold migration (`9`) renames the ones that already exist. - - `.taskless/rules/sg/no-eval/` beside `.taskless/rules/vale/no-eval/` was silent: `check`'s human output prints `error[no-eval]` with no engine, so a collision shows two identical lines, and every id-addressed command had two answers to choose between. - - **Your rule ids may change on upgrade, and `check` output changes with them.** The first `taskless init` after upgrading renames the colliding `sg` and `vale` copies to `-` — `sg/no-eval` becomes `sg/no-eval-sg`, `vale/no-eval` becomes `vale/no-eval-vale`. Where both move, neither keeps the bare id, so nobody has to work out which of their two rules kept the name. If `-` is already taken it uses the next free `--2`, `-3`, … and never overwrites an existing rule. - - **A `runtime` rule is never renamed** and keeps the bare id, so a collision between `runtime` and another engine moves only the other one. Runtime rules are the signed and blessed tier, and this keeps the upgrade clear of that machinery. Nothing is left colliding either way, because one engine can only hold one directory per id. - - Everything the rename touches is inside the rule's own directory: the directory name, the rule file, its `id:` field, an sg rule's `.tests/` fixtures and their `id:` fields, and a Vale rule's `.vale.ini` breadcrumb and both segments of its `.` assignment. Every rename is printed — old path, new path, and each file rewritten — as is any runtime rule that kept its id, so you can see exactly what moved before committing it. Update any CI config, baseline file or suppression comment that names an old id — including `check --rule `, which errors with `RULE_NOT_FOUND` rather than reporting zero findings when the id it names has been renamed out from under it. - - `.taskless/rule-metadata/.yml` is left where it is rather than following either rule, since a symmetric rename gives it no owner. In practice there is nothing there: this CLI has never written a sidecar, because the service does not return the metadata block they are written from. - -- 8178d47: `test` now reports the findings an ast-grep rule's fixtures produced, with the - message as ast-grep rendered it. Previously only Vale and runtime rules carried - `findings`; an ast-grep rule reported an empty array, so a rule whose message - interpolated its metavariables in the wrong order fired in exactly the right - places and was reported green. - - Each snippet is replayed through `ast-grep scan --stdin`, so the language comes - from the rule's own `language:` key and no temporary file is written. A finding - names the test YAML that declares the snippet, at the snippet's real line and - column in that file. - - Additive, and `patch` under the pre-1.0 rule: the `findings` array was already - present on every rule result and documented as possibly empty, so nothing a - consumer reads changes shape. Vale and runtime behaviour is untouched, and the - verdict `ast-grep test` decides is unchanged — findings are gathered after it - and cannot alter it. - -- c000762: `test --json` now reports the findings a rule's fixtures produced. - - Each rule result carries a `findings` array: the same finding shape `check --json` prints — `source`, `ruleId`, `severity`, `message`, `file`, `range`, `matchedText`, and the optional `note` and `fix` — plus a `bucket` of `"pass"` or `"fail"` naming the fixture that produced it. The rendered `message` is the point. A rule whose message interpolates its captures can have the slots in the wrong order, fire on every `fail/` fixture, stay quiet on every `pass/` one, and be reported as a rule that passed; the rendered message is the only evidence otherwise, and until now the only way to see it was a second `check` run against a fixture path you had to construct yourself. - - The array is **always present and empty rather than absent** — for a rule that produced nothing, one whose verification failed before its fixtures ran, a refused runtime rule, `verify` (which runs no fixtures at all), and ast-grep rules, whose findings are not surfaced yet. Fail-bucket findings are reported on a passing run too, since that is the only run that produces them. - - On the human path a passing rule still prints one line. A **failing** rule now prints the findings that bear on the failure beneath it, labelled by bucket and rendered the way `check` renders a finding. That includes pass-bucket findings: `pass fixture wrongly fired: ` said _that_ it happened and never what matched. - - Vale and runtime rules only. ast-grep follows separately: the vendored binary's `sg test` has no `--json` and no output-format flag, and its fixtures are inline YAML scalars rather than files. - - No flag was added, and nothing new is executed: the findings were already in hand and were being discarded. - - **If you consume `@taskless/cli/schemas`:** `zod`'s `.parse()` strips keys the schema does not declare, so a consumer still on the previously published `verifyTestOutputSchema` will silently drop `findings` from the payload it returns until the dependency is upgraded. Nothing breaks — but the field simply not being there, on a CLI that is emitting it, is the kind of thing that generates a bug report. - -- d3befa0: Update the bundled Vale to 3.22.0. - - For a rule under `.taskless/rules/vale/`, what you can now write: - - `scope: text & ~link` (and `~strong`, `~emphasis`, `~code`) runs on the paragraph and blanks the element's text out of it before the rule sees it, so a wording or casing rule can leave link text and bold terms alone without giving up the sentence around them. Positions after the blanked element do not move. The release note's `text.raw` and `paragraph.link` spellings are not scopes; `verify` rejects them, so write the bare inline name. - - `split: true` on a `spelling` rule checks the parts of an identifier (`getHTTPResponsze_v2` reports `Responsze`) and places each at its own position. 3.21.0 accepted the key and did neither. - - A `frontmatter` or `frontmatter.` rule reports every occurrence in a field at the field's own position; a token appearing twice in one field was reported once. - - A one-line MDX element's text (`...`) is linted. - - What changes for a rule you already have: - - **`BasedOnStyles =` is removed from every rule's `.vale.ini`, by migration 0008 on the next `init`, and `verify` now rejects the key with any value (`vale-config-no-based-on-styles`).** Every earlier version of the `create-vale-rule` recipe wrote the line into every matcher, where it was inert: no bundled style loads unless a run-level `BasedOnStyles` names one, and the assembled header names none. On 3.22.0 an empty value clears every setting a file inherited from an earlier matcher, and the assembled run config is every rule's matchers in id order, so a rule writing the line under `[docs/**]` silenced every alphabetically earlier rule under `docs/`, and `[*.md]` beside another rule's `[*.{md,markdown}]` silenced the first on every `.md` file, with nothing reported. `check` and `verify` refuse to run until `init` has migrated the configs, and name it; commit the rewritten files. A migrated config enables exactly what the old one did on 3.21.0. - - A rule whose `scope` negates `link`, `strong`, `emphasis` or `code` no longer sees that element's text inside a paragraph. Through 3.21.0 the paragraph still carried it, so findings inside those elements disappear. Drop the negation if the rule was meant to reach them. - - The isolating config `test` and `verify` build no longer writes `BasedOnStyles =`. Measured on both binaries, nothing fires without it and `Vale.Spelling` never could, so fixtures behave as before. - - Not changed for a rule this CLI assembles: a `[formats]` key may now be a file name or a glob, but no rule config can carry one and the assembled header writes none, so the extension still decides the parser. `UNSET` as a rule's value behaves as `NO` and is not accepted; `YES` and `NO` remain the two values. - - `taskless agent update` carries the same list, with what to do about each. - -- 620865e: `verify` validates a Vale rule's `.vale.ini` against a schema and names the constraint each rejection violates. The config is parsed into an ordered structure and checked there: an assignment above the first matcher, a matcher without its `tskl) rule` breadcrumb, a key naming another rule, a value other than `YES`/`NO`, a `BasedOnStyles` assignment with any value, a config with no matcher or no `YES`, and a `NO` matcher that precedes every `YES` (both judged by each matcher's final verdict, so a `YES` a later `NO` in the same matcher overrides does not count) are each rejected under a `vale-config-*` constraint that `verify --json` reports in `violations[]` and `reference.json` publishes. A repeated key, a `[*]` matcher, and a `.taskless/**` matcher are reported as a notice without failing the rule. - - `check` runs the same schema before assembling the Vale run config, and a rejected config refuses the Vale engine for that run: the failure names the rule and the line, reaches the exit code, and ast-grep still runs. A rule is never silently left out of the assembled config. Accepted configs are written verbatim under their breadcrumb, so the one string edit assembly used to make (dropping a copied-in `StylesPath`) is gone; that line is now a rejection. Advisories reach `check`'s notices. To find every rejected line at once, run `taskless verify`. - - This is still `patch`. The package is `0.y.z`, and every config the schema refuses was already being misread by Vale: a rule enabled nowhere with a `W101` on stderr, a rule silently overriding a neighbour, a disable the following enable cancelled. The release surfaces a defect the consumer already had rather than introducing one, the same call as linting files over 128 KB again in 0.11.3. - -- 78dc643: `verify` warns when a Vale rule's `raw` list has more than one entry, since Vale concatenates them into one pattern rather than alternating them; the `create-vale-rule` recipe explains the `(a|b)` form. The warning rides on `notice` and does not fail the rule. -- 3acb5a1: `agent create-vale-rule` (topic v13) corrects two claims that cost rule authors work. Vale patterns are not RE2-only: Vale compiles with Go's `regexp` and falls back to `regexp2`, so lookahead, lookbehind and backreferences work, and the recipe no longer tells you to split a rule that one pattern expresses. Two silent limits are measured and documented alongside it: a backreference does nothing as a `swap` key, and the implicit word boundary on `tokens`/`swap` lands after a trailing lookahead, so that lookahead has to peek at a non-word character. `check` on a rule's `.tests/fail` bucket is a supported way to read a rendered message, because `.taskless/` is excluded from the whole-project walk only; when that bucket comes back empty, the recipe now sends you to the rule's own config, specifically a `[.taskless/**]` matcher, before the pattern. - - `verify` carried the same imprecision and now states both halves: a `[.taskless/**]` matcher is unnecessary on a whole-project check, AND it silences the rule on a path you name, such as the rule's own fixture bucket. It was previously described as acting only under a bare `vale` invocation, which read as harmless. `agent update` (topic v10) is corrected to match. - -- b2e2662: `check` no longer skips Vale target files over 128 KB. The guard - (`VALE_MAX_FILE_BYTES`, added in 0.11.2) was written against Vale 3.20.0, - whose lint time grew superlinearly with the size of a single Markdown block, - so one large file could consume the run's whole timeout and, because Vale - writes nothing until the run finishes, cost every other file its findings. - The same release moved the vendored Vale to 3.21.0, whose perf work makes that - cost linear regardless of block structure, so the cap no longer separates a - cheap file from an expensive one. Re-measured on the reproduction from - taskless/cli#325 against the vendored 3.21.0 binary (darwin/arm64, warm, median - of three): - - | fixture | Vale 3.20.0 | Vale 3.21.0 | - | ------------------------------- | ----------- | ----------- | - | 3.2 MB, one block (`huge.md`) | ~81,000 ms | ~230 ms | - | 3.2 MB, blank-line separated | ~4,400 ms | ~290 ms | - | 128 KB, one block (the old cap) | ~770 ms | ~26 ms | - | 25 MB, one block | — | ~2,200 ms | - - What a user sees: a file that 0.11.2 named in a `Vale did not check N file(s) -over 131072 bytes` notice is linted again and produces findings; the notice is - gone. The per-file retry for a target whose front matter Vale cannot parse - (taskless/cli#300) is unchanged, as is the 60 s run timeout. Vale still emits - nothing until the run completes, so a run killed by an external time limit - still loses every finding; with linear cost that takes a file in the hundreds - of megabytes rather than the hundreds of kilobytes. - -- 59e1163: `verify` now rejects a Vale `substitution` rule whose `swap` key carries a backreference. No capture group survives a swap key: Vale compiles a rule's keys into one alternation, wrapping each in a capture group of its own and rewriting the author's groups to non-capturing, so `\1` refers to Vale's wrapper and matches nothing. Vale reports none of this, so the rule loads, runs and silently never fires. The detector is character-class aware, since `\1` inside `[…]` is an octal escape and works. `$1` in the swap _value_ is unaffected and still works. The `create-vale-rule` topic goes to v14: the claim that Vale tries Go's `regexp` before falling back to `regexp2` is removed (there is no fallback; it compiles with `regexp2` unconditionally), the `swap` constraint is generalised, and the leading-lookbehind mirror of the trailing-lookahead limit is documented. -- f97365d: `taskless --version` and `-v` print the version. Before, both fell through to the full usage banner, and `-v` was not recognised at all. - ## 0.11.2 [Compare with v0.11.1](https://github.com/taskless/cli/compare/v0.11.1...v0.11.2) diff --git a/packages/cli/package.json b/packages/cli/package.json index d98470a7..96acc692 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@taskless/cli", - "version": "0.11.3", + "version": "0.11.2", "license": "MIT", "repository": { "type": "git", diff --git a/skills/taskless/SKILL.md b/skills/taskless/SKILL.md index ab6cc0da..2ca96e35 100644 --- a/skills/taskless/SKILL.md +++ b/skills/taskless/SKILL.md @@ -20,7 +20,7 @@ description: | `agent route`; it does NOT suppress the skill. metadata: author: taskless - version: 0.11.3 + version: 0.11.2 commandName: tskl compatibility: Designed for Agents implementing the Agent Skills specification. --- From 682ae650892af6610a0f4449653ef5e52e9187d6 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 29 Sep 2026 22:37:58 -0700 Subject: [PATCH 2/5] docs(changeset): describe the restored 0.11.3 changes as shipping in 0.12.0 The feedback-survey note named 0.11.3 and is renamed to match; the Vale config schema note drops a sentence justifying its patch bump against 0.11.3, moot now that the release is minor. Two survey comments follow. --- .../{feedback-survey-0-11-3.md => feedback-survey-0-12-0.md} | 2 +- .changeset/vale-config-schema.md | 2 -- packages/cli/src/survey/constants.ts | 2 +- packages/cli/test/survey-cadence.test.ts | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) rename .changeset/{feedback-survey-0-11-3.md => feedback-survey-0-12-0.md} (88%) diff --git a/.changeset/feedback-survey-0-11-3.md b/.changeset/feedback-survey-0-12-0.md similarity index 88% rename from .changeset/feedback-survey-0-11-3.md rename to .changeset/feedback-survey-0-12-0.md index 78d837f9..07d0730a 100644 --- a/.changeset/feedback-survey-0-11-3.md +++ b/.changeset/feedback-survey-0-12-0.md @@ -2,4 +2,4 @@ "@taskless/cli": patch --- -The feedback survey is replaced for 0.11.3. Only the kind of rule the user was trying to create is required now; the user's own words, the completion verdict, what worked, what did not, the agent in use, and the most valuable rule so far are all optional. A `skip` at the invite no longer dismisses the survey: the agent sends its own account of the session and leaves the user's words out. `feedback dismiss` is reserved for a user who asks that nothing be sent. Every install is invited once more, since the new survey keeps its own cadence. +The feedback survey is replaced for 0.12.0. Only the kind of rule the user was trying to create is required now; the user's own words, the completion verdict, what worked, what did not, the agent in use, and the most valuable rule so far are all optional. A `skip` at the invite no longer dismisses the survey: the agent sends its own account of the session and leaves the user's words out. `feedback dismiss` is reserved for a user who asks that nothing be sent. Every install is invited once more, since the new survey keeps its own cadence. diff --git a/.changeset/vale-config-schema.md b/.changeset/vale-config-schema.md index 24f7ac37..07d05dc0 100644 --- a/.changeset/vale-config-schema.md +++ b/.changeset/vale-config-schema.md @@ -5,5 +5,3 @@ `verify` validates a Vale rule's `.vale.ini` against a schema and names the constraint each rejection violates. The config is parsed into an ordered structure and checked there: an assignment above the first matcher, a matcher without its `tskl) rule` breadcrumb, a key naming another rule, a value other than `YES`/`NO`, a `BasedOnStyles` assignment with any value, a config with no matcher or no `YES`, and a `NO` matcher that precedes every `YES` (both judged by each matcher's final verdict, so a `YES` a later `NO` in the same matcher overrides does not count) are each rejected under a `vale-config-*` constraint that `verify --json` reports in `violations[]` and `reference.json` publishes. A repeated key, a `[*]` matcher, and a `.taskless/**` matcher are reported as a notice without failing the rule. `check` runs the same schema before assembling the Vale run config, and a rejected config refuses the Vale engine for that run: the failure names the rule and the line, reaches the exit code, and ast-grep still runs. A rule is never silently left out of the assembled config. Accepted configs are written verbatim under their breadcrumb, so the one string edit assembly used to make (dropping a copied-in `StylesPath`) is gone; that line is now a rejection. Advisories reach `check`'s notices. To find every rejected line at once, run `taskless verify`. - -This is still `patch`. The package is `0.y.z`, and every config the schema refuses was already being misread by Vale: a rule enabled nowhere with a `W101` on stderr, a rule silently overriding a neighbour, a disable the following enable cancelled. The release surfaces a defect the consumer already had rather than introducing one, the same call as linting files over 128 KB again in 0.11.3. diff --git a/packages/cli/src/survey/constants.ts b/packages/cli/src/survey/constants.ts index 5faa7560..d426fc35 100644 --- a/packages/cli/src/survey/constants.ts +++ b/packages/cli/src/survey/constants.ts @@ -18,7 +18,7 @@ * reads shown ≫ sent by design, and nothing here pretends otherwise. * * A question's id is PostHog's and changes whenever the question does. The - * survey below replaced `01a0b1a0-80fb-0000-5dc1-baa4ec44e619` for 0.11.3: + * survey below replaced `01a0b1a0-80fb-0000-5dc1-baa4ec44e619` for 0.12.0: * seven questions, only the first required, every id new. The cadence store * is keyed by survey id, so every install is invited once more. */ diff --git a/packages/cli/test/survey-cadence.test.ts b/packages/cli/test/survey-cadence.test.ts index 9bc474ac..52d824f4 100644 --- a/packages/cli/test/survey-cadence.test.ts +++ b/packages/cli/test/survey-cadence.test.ts @@ -17,7 +17,7 @@ import { describe("survey constants", () => { // The identifiers are PostHog's, transcribed once. A question's id changes // whenever the question does, which is exactly the kind of drift this pins: - // the values here are what the 0.11.3 survey holds as of 2026-09-21. + // the values here are what the 0.12.0 survey holds as of 2026-09-21. it("carries the live survey's question ids in question order", () => { expect(SURVEY_ID).toBe("01a0c7b9-dfe4-0000-d05e-ce253e90a68c"); expect(SURVEY_QUESTIONS.map(({ key, id }) => [key, id])).toEqual([ From e2eb31598482b2a7517502526b458aa7ad59e1f8 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 29 Sep 2026 22:38:05 -0700 Subject: [PATCH 3/5] docs(agent): give the update ledger a 0.12.0 section The ledger's "Migrating to 0.11.3" section named a version that never shipped; its entries ship in 0.12.0 and move under that heading. 0.12.0 also had no ledger entry for the v2 API: pre-v2 runtime rules must be regenerated, an edited or copied issued rule fails check and is repaired with rule restore, rule create --json prints requestId, and init applies migration 10. --- packages/cli/src/agent/update.md | 47 ++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/agent/update.md b/packages/cli/src/agent/update.md index 195fbd24..ebcd1213 100644 --- a/packages/cli/src/agent/update.md +++ b/packages/cli/src/agent/update.md @@ -1,4 +1,4 @@ -# Topic: update (CLI v%(CLI_VERSION)s / topic v10) +# Topic: update (CLI v%(CLI_VERSION)s / topic v11) ## You are here This is `update`. It tells you what an upgrade changed for the rules @@ -293,7 +293,50 @@ one trap (a leaf element on its own, `doc(h2)`, is inert; chain it). No existing rule changes; this is a reason to revisit one that was narrowed by hand. -### Migrating to 0.11.3 +### Migrating to 0.12.0 + +**The CLI speaks the Taskless v2 rule API, and a rule is addressed by +its own id: its directory name under `.taskless/rules//`.** +Five things follow for existing rules. + +**Rules issued before 0.12.0 are treated as locally written.** They +came through the v1 API, which v2 does not know, so reconcile answers +them `unknown` and nothing can restore them. An ast-grep or Vale rule +keeps running, as any rule you wrote yourself. A runtime rule does not: +an `unknown` runtime rule never executes, so `check` skips it and names +it. Regenerate each such runtime rule with +`%(TASKLESS_CLI)s agent create-remote-rule`, then delete the old +directory. List them with `%(TASKLESS_CLI)s check --json` and read +`skipped`. + +**An edited issued rule fails `check`.** Logged in, `check` compares +every file of every issued rule, of every engine, with what Taskless +issued. An edited ast-grep or Vale rule does not run and fails the run, +naming each changed, removed or added file; an edited runtime rule does +not run. Do not edit a rule back by hand: run +`%(TASKLESS_CLI)s rule restore `, which writes only bytes whose +signatures verify. On a plan without rule recovery it prints how to +recover the rule from git instead. `check` itself never rewrites a rule. + +**A copied or renamed issued rule fails `check` too.** A rule under a +new id that still carries a file Taskless issued to another rule does +not run, and the failure names the rule it came from. If the original +was deleted, the pair is reported as one rename: restore the original +with `rule restore `, then delete the copy. Never restore +the copy's own id. + +**`rule create --json` prints `requestId`, not `ruleId`.** The old +field always held the request id. The ids of the rules written are in +`rules`, and those are what `rule improve`, `rule restore`, +`rule revisions` and `rule rollback` take. Update anything that read +`ruleId` from `rule create`. + +**Run `%(TASKLESS_CLI)s init` once.** Migration 10 moves test files an +earlier migration left in `.taskless/sg/rule-tests/` (from rules whose +ids ended in a timestamp) into each rule's `.tests/`, and removes the +emptied legacy directory. A test that matches no rule is left where it +is. `check` refuses to run until the project is migrated, and names +`init`. Commit the moved files. **Files over 128KB are linted again.** 0.11.2 skipped any target file over 128KB that a matcher's section reached, naming it in a `notices` From 2e347ac10b3fae021fc45d0842043afe2d69b69f Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 29 Sep 2026 22:42:44 -0700 Subject: [PATCH 4/5] docs(agent): scope the 0.12.0 ledger's v2 entries to logged-in projects Only migration 10 applies to every project; check refuses to run until it is applied, logged in or not. The v2 entries (pre-v2 runtime rules, edited and copied rules, rule create's requestId) matter only when info --json reports loggedIn: logged out, nothing is reconciled and runtime rules are skipped as before. --- packages/cli/src/agent/update.md | 82 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/cli/src/agent/update.md b/packages/cli/src/agent/update.md index ebcd1213..f2ba1c7a 100644 --- a/packages/cli/src/agent/update.md +++ b/packages/cli/src/agent/update.md @@ -295,48 +295,46 @@ narrowed by hand. ### Migrating to 0.12.0 -**The CLI speaks the Taskless v2 rule API, and a rule is addressed by -its own id: its directory name under `.taskless/rules//`.** -Five things follow for existing rules. - -**Rules issued before 0.12.0 are treated as locally written.** They -came through the v1 API, which v2 does not know, so reconcile answers -them `unknown` and nothing can restore them. An ast-grep or Vale rule -keeps running, as any rule you wrote yourself. A runtime rule does not: -an `unknown` runtime rule never executes, so `check` skips it and names -it. Regenerate each such runtime rule with -`%(TASKLESS_CLI)s agent create-remote-rule`, then delete the old -directory. List them with `%(TASKLESS_CLI)s check --json` and read -`skipped`. - -**An edited issued rule fails `check`.** Logged in, `check` compares -every file of every issued rule, of every engine, with what Taskless -issued. An edited ast-grep or Vale rule does not run and fails the run, -naming each changed, removed or added file; an edited runtime rule does -not run. Do not edit a rule back by hand: run -`%(TASKLESS_CLI)s rule restore `, which writes only bytes whose -signatures verify. On a plan without rule recovery it prints how to -recover the rule from git instead. `check` itself never rewrites a rule. - -**A copied or renamed issued rule fails `check` too.** A rule under a -new id that still carries a file Taskless issued to another rule does -not run, and the failure names the rule it came from. If the original -was deleted, the pair is reported as one rename: restore the original -with `rule restore `, then delete the copy. Never restore -the copy's own id. - -**`rule create --json` prints `requestId`, not `ruleId`.** The old -field always held the request id. The ids of the rules written are in -`rules`, and those are what `rule improve`, `rule restore`, -`rule revisions` and `rule rollback` take. Update anything that read -`ruleId` from `rule create`. - -**Run `%(TASKLESS_CLI)s init` once.** Migration 10 moves test files an -earlier migration left in `.taskless/sg/rule-tests/` (from rules whose -ids ended in a timestamp) into each rule's `.tests/`, and removes the -emptied legacy directory. A test that matches no rule is left where it -is. `check` refuses to run until the project is migrated, and names -`init`. Commit the moved files. +**Every project: run `%(TASKLESS_CLI)s init` once.** Migration 10 moves +test files an earlier migration left in `.taskless/sg/rule-tests/` (from +rules whose ids ended in a timestamp) into each rule's `.tests/`, and +removes the emptied legacy directory. A test that matches no rule is +left where it is. `check` refuses to run until the project is migrated, +logged in or not, and names `init`. Commit the moved files. + +**Only if `loggedIn` is `true` in `%(TASKLESS_CLI)s info --json`:** the +CLI now speaks the Taskless v2 rule API, which addresses a rule by its +own id (its directory name under `.taskless/rules//`) and checks +every issued rule against what Taskless issued. Logged out, nothing is +checked, runtime rules are skipped as before, and none of the four +entries below applies. + +- **Rules issued before 0.12.0 are treated as locally written.** They + came through the v1 API, which v2 does not know, so reconcile answers + them `unknown` and nothing can restore them. An ast-grep or Vale rule + keeps running, as any rule you wrote yourself. A runtime rule does + not: an `unknown` runtime rule never executes, so `check` skips it and + names it in `skipped` under `--json`. Regenerate each one with + `%(TASKLESS_CLI)s agent create-remote-rule`, then delete the old + directory. +- **An edited issued rule fails `check`.** An edited ast-grep or Vale + rule does not run and fails the run, naming each changed, removed or + added file; an edited runtime rule does not run. Do not edit a rule + back by hand: run `%(TASKLESS_CLI)s rule restore `, which + writes only bytes whose signatures verify. On a plan without rule + recovery it prints how to recover the rule from git instead. `check` + itself never rewrites a rule. +- **A copied or renamed issued rule fails `check` too.** A rule under a + new id that still carries a file Taskless issued to another rule does + not run, and the failure names the rule it came from. If the original + was deleted, the pair is reported as one rename: restore the original + with `rule restore `, then delete the copy. Never restore + the copy's own id. +- **`rule create --json` prints `requestId`, not `ruleId`.** The old + field always held the request id. The ids of the rules written are in + `rules`, and those are what `rule improve`, `rule restore`, + `rule revisions` and `rule rollback` take. Update anything that read + `ruleId` from `rule create`. **Files over 128KB are linted again.** 0.11.2 skipped any target file over 128KB that a matcher's section reached, naming it in a `notices` From 36439b0186c6fc7630b3f85d43f03e2b341fc7c9 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 29 Sep 2026 22:53:35 -0700 Subject: [PATCH 5/5] docs(agent): let check's own output decide the 0.12.0 ledger's v2 steps Instead of preconditions on login state, run check --json once after init and act on the messages it prints: a copy of an issued rule, an edited rule, a pre-v2 runtime rule, a missing rule. A run where signatures do not apply (logged out, an expired token, an uncovered repository) prints none of them, so there is nothing to decide up front. --- packages/cli/src/agent/update.md | 64 ++++++++++++++++---------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/cli/src/agent/update.md b/packages/cli/src/agent/update.md index f2ba1c7a..93bccb27 100644 --- a/packages/cli/src/agent/update.md +++ b/packages/cli/src/agent/update.md @@ -302,39 +302,39 @@ removes the emptied legacy directory. A test that matches no rule is left where it is. `check` refuses to run until the project is migrated, logged in or not, and names `init`. Commit the moved files. -**Only if `loggedIn` is `true` in `%(TASKLESS_CLI)s info --json`:** the -CLI now speaks the Taskless v2 rule API, which addresses a rule by its -own id (its directory name under `.taskless/rules//`) and checks -every issued rule against what Taskless issued. Logged out, nothing is -checked, runtime rules are skipped as before, and none of the four -entries below applies. - -- **Rules issued before 0.12.0 are treated as locally written.** They - came through the v1 API, which v2 does not know, so reconcile answers - them `unknown` and nothing can restore them. An ast-grep or Vale rule - keeps running, as any rule you wrote yourself. A runtime rule does - not: an `unknown` runtime rule never executes, so `check` skips it and - names it in `skipped` under `--json`. Regenerate each one with +**Then run `%(TASKLESS_CLI)s check --json` once, and act on what it +reports.** The CLI now speaks the Taskless v2 rule API, which checks +every rule Taskless issued against what it issued, so `check` itself +says whether any rule needs attention. If none of the following appears +in its output, there is nothing to do. That includes every run that is +logged out, where nothing is checked. + +- **"is a copy of Taskless rule ``"** (in `failures`, `notices`, + or a `skipped` reason): the rule carries a file Taskless issued to + another rule, so it did not run. If the message says the source "was + deleted", run `%(TASKLESS_CLI)s rule restore `; either way, + delete the copy. Never restore the copy's own id. +- **"was edited since Taskless issued it"**: run + `%(TASKLESS_CLI)s rule restore ` rather than editing it back + by hand. It writes only bytes whose signatures verify, and on a plan + without rule recovery it prints how to recover the rule from git + instead. +- **A runtime rule in `skipped` with "not issued by the rule service"**: + it was generated before 0.12.0, through the v1 API, and v2 cannot + vouch for it, so it never runs. Regenerate it with `%(TASKLESS_CLI)s agent create-remote-rule`, then delete the old - directory. -- **An edited issued rule fails `check`.** An edited ast-grep or Vale - rule does not run and fails the run, naming each changed, removed or - added file; an edited runtime rule does not run. Do not edit a rule - back by hand: run `%(TASKLESS_CLI)s rule restore `, which - writes only bytes whose signatures verify. On a plan without rule - recovery it prints how to recover the rule from git instead. `check` - itself never rewrites a rule. -- **A copied or renamed issued rule fails `check` too.** A rule under a - new id that still carries a file Taskless issued to another rule does - not run, and the failure names the rule it came from. If the original - was deleted, the pair is reported as one rename: restore the original - with `rule restore `, then delete the copy. Never restore - the copy's own id. -- **`rule create --json` prints `requestId`, not `ruleId`.** The old - field always held the request id. The ids of the rules written are in - `rules`, and those are what `rule improve`, `rule restore`, - `rule revisions` and `rule rollback` take. Update anything that read - `ruleId` from `rule create`. + directory. An ast-grep or Vale rule from before 0.12.0 keeps running, + as a rule you wrote yourself, and needs nothing. +- **"was issued for this repository but is not in .taskless/rules/"**: + run `%(TASKLESS_CLI)s rule restore ` to bring it back, or + ignore it if the rule was removed on purpose. + +`check` never rewrites a rule itself; each fix above is a command you +run. Separately, **`rule create --json` prints `requestId`, not +`ruleId`**: the old field always held the request id. The ids of the +rules written are in `rules`, and those are what `rule improve`, +`rule restore`, `rule revisions` and `rule rollback` take. Update any +script that read `ruleId` from `rule create`. **Files over 128KB are linted again.** 0.11.2 skipped any target file over 128KB that a matcher's section reached, naming it in a `notices`