@taskless/cli 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,19 @@
1
- import { o as f, a as g, s as o, l as K, t as q } from "./schemas-OhdGAhu_.js";
2
- const Pe = "npx @taskless/cli";
3
- function Ve() {
1
+ import { o as f, a as m, s as i, l as j, _ as F, t as $ } from "./schemas-OhdGAhu_.js";
2
+ const ze = "npx @taskless/cli";
3
+ function Ye() {
4
4
  return "npx @taskless/cli";
5
5
  }
6
- function De(t) {
7
- return t;
6
+ const z = "%(TASKLESS_CLI)s";
7
+ function He(e) {
8
+ return e.replaceAll(z, "npx @taskless/cli");
8
9
  }
9
- function Me(t) {
10
- return t;
10
+ function Be(e) {
11
+ return e;
11
12
  }
12
- const U = `# Topic: auth (CLI v%(CLI_VERSION)s / topic v1)
13
+ function Xe(e) {
14
+ return e;
15
+ }
16
+ const Y = `# Topic: auth (CLI v%(CLI_VERSION)s / topic v1)
13
17
 
14
18
  ## Goal
15
19
  Manage Taskless authentication. Three branches:
@@ -86,7 +90,7 @@ report.
86
90
 
87
91
  - \`%(TASKLESS_CLI)s agent info\`: see auth state and skill versions
88
92
  - \`%(TASKLESS_CLI)s agent route\`: first action that requires auth
89
- `, F = `# Topic: check (CLI v%(CLI_VERSION)s / topic v2)
93
+ `, H = `# Topic: check (CLI v%(CLI_VERSION)s / topic v2)
90
94
 
91
95
  ## Goal
92
96
  Run the applicable rules against the codebase and report matches. Two
@@ -215,7 +219,7 @@ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
215
219
 
216
220
  - \`%(TASKLESS_CLI)s agent route\`: add a rule if none exist
217
221
  - \`%(TASKLESS_CLI)s agent ci\`: wire \`check\` into a CI pipeline
218
- `, W = `# Topic: ci (CLI v%(CLI_VERSION)s / topic v1)
222
+ `, B = `# Topic: ci (CLI v%(CLI_VERSION)s / topic v1)
219
223
 
220
224
  ## Goal
221
225
  Wire \`%(TASKLESS_CLI)s check\` into the user's existing CI so rules run
@@ -443,7 +447,7 @@ Show:
443
447
 
444
448
  - \`%(TASKLESS_CLI)s agent check\`: the command being wired into CI
445
449
  - \`%(TASKLESS_CLI)s agent route\`: required if no rules exist yet
446
- `, $ = `# Topic: create-legacy-rule (CLI v%(CLI_VERSION)s / topic v1)
450
+ `, X = `# Topic: create-legacy-rule (CLI v%(CLI_VERSION)s / topic v1)
447
451
 
448
452
  ## You are here
449
453
  This is \`create-legacy-rule\`. It helps you write a rule for a linter the
@@ -507,7 +511,7 @@ then write the rule where that tool expects it.
507
511
  - \`%(TASKLESS_CLI)s agent route\`: re-decide the destination if this no longer fits
508
512
  - \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a local ast-grep rule instead
509
513
  - \`%(TASKLESS_CLI)s agent create-remote-rule\`: generate via the service (login)
510
- `, z = `# Topic: create-remote-rule (CLI v%(CLI_VERSION)s / topic v3)
514
+ `, J = `# Topic: create-remote-rule (CLI v%(CLI_VERSION)s / topic v3)
511
515
 
512
516
  ## You are here
513
517
  This is \`create-remote-rule\`. It helps you have the Taskless service
@@ -698,7 +702,7 @@ With \`--json\`, failures emit \`{ ok: false, code, message }\`:
698
702
  - \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a rule locally instead
699
703
  - \`%(TASKLESS_CLI)s agent improve-rule\`: iterate on a rule that already exists
700
704
  - \`%(TASKLESS_CLI)s agent check\`: validate the generated rule
701
- `, H = `# Topic: create-runtime-rule (CLI v%(CLI_VERSION)s / topic v2)
705
+ `, Q = `# Topic: create-runtime-rule (CLI v%(CLI_VERSION)s / topic v2)
702
706
 
703
707
  ## You are here
704
708
  This is \`create-runtime-rule\`. It helps you write a runtime rule: a
@@ -878,7 +882,7 @@ capture did not match, which is a fact about the fixture.
878
882
  - \`%(TASKLESS_CLI)s agent auth\`: log in, log out, check status
879
883
  - \`%(TASKLESS_CLI)s agent route\`: re-decide once the login state changes
880
884
  - \`%(TASKLESS_CLI)s agent check\`: see which rules ran and which were skipped
881
- `, Y = '# Topic: create-sg-rule (CLI v%(CLI_VERSION)s / topic v5)\n\n## You are here\nThis is `create-sg-rule`. It helps you write an ast-grep rule: a check\nover the structure of a single source file, authored on this machine.\nIf that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route`\nand follow its decision rather than adapting this recipe.\n\n## Goal\nProduce a verified ast-grep rule and its test file, locally, without\ncontacting the Taskless service. You derive the rule yourself, write it\nin the canonical on-disk shape, and validate it with `verify` and\n`test` in a feedback loop. The files you produce match exactly what the\nservice writes, so `check`, `improve-rule`, `verify`, and `test` treat\nthem identically.\n\n## Preconditions\n- `.taskless/` directory exists.\n- The agent can read/write files and run shell commands.\n- No auth required.\n\n## One rule, one directory\n\nEverything defining a rule lives in `.taskless/rules/sg/<id>/`. Writing\na rule means creating that directory; deleting a rule means deleting\nit. No file outside it is touched either way, so two agents authoring\ntwo rules never collide.\n\n```\n.taskless/rules/sg/no-eval/\n no-eval.yml the rule\n .tests/no-eval-YYYYMMDD-test.yml its test cases\n```\n\n**The leading dot on `.tests/` is required.** ast-grep walks the rules\ntree and parses every `.yml` it reaches as a rule, so a plain `tests/`\ndirectory makes it read your test file as a rule and fail the scan for\nthe whole project. A dot-directory is skipped by that walk, and the\ntest runner still finds it. Do not rename it.\n\nUnlike Vale, an `sg` rule needs no second config file. It carries its\nown `files` and `ignores` inside the rule, so the rule file is the\nwhole rule.\n\n## Steps\n\n1. **Learn the ast-grep rule format.** Consult the ast-grep rule\n reference at https://ast-grep.github.io/guide/rule-config.html for\n valid fields and operators (`pattern`, `kind`, `regex`,\n `any`/`all`/`has`/`inside`/`not`) and meta-variable syntax. This\n recipe does not embed the schema. Read it from upstream rather than\n writing a rule from memory.\n\n2. **Gather and confirm the pattern.** Make sure you have concrete\n success cases (code that should pass) and failure cases (code that\n should be flagged), the target language, and any exceptions. Search\n the codebase for real instances and confirm exclusions with the user.\n\n3. **Check for an existing rule that already covers this.** Scan\n `.taskless/rules/sg/` and read each rule\'s `message`, `note`, and\n `rule` fields. If one overlaps, show the user and ask whether they\n would rather improve it, `%(TASKLESS_CLI)s agent improve-rule --anonymous`\n iterates a rule locally.\n\n4. **Author the rule in the canonical shape.** Write the rule to\n `.taskless/rules/sg/<id>/<id>.yml`, where `<id>` is kebab-case and\n names both the directory and the file. At minimum:\n - `id`: kebab-case, matching the filename (e.g. `no-eval`)\n - `language`: the target language, in ast-grep\'s spelling (below)\n - `severity`: `error`, `warning`, `info`, or `hint`\n - `message`: a concise single-line explanation\n - `rule`: the ast-grep rule object\n\n Optional but useful: `note` (multi-line guidance, supports markdown),\n `fix` (auto-fix pattern), `ignores` (file patterns to skip).\n\n **`language` is ast-grep\'s vocabulary.** The vendored rule schema\n types the field as a bare string with no enum, so the schema layer\n accepts anything. `verify` checks it separately: an off-list spelling\n is an error naming the accepted list, and a spelling ast-grep resolves\n but does not itself use (`typescript` for `TypeScript`) is a notice.\n Get it right anyway rather than leaning on that check. An unrecognized\n name aborts config parsing, which takes every other sg rule in the\n project down with it and reports nothing.\n ast-grep (v%(AST_GREP_VERSION)s) parses:\n\n %(AST_GREP_LANGUAGES)s\n\n Copy a spelling from that list rather than typing one that looks\n right. Off-list spellings fail two different ways and neither is\n caught locally: one ast-grep does not recognize at all takes the\n whole scan down (`did not match any variant of untagged enum\n SgLang`, so every other rule goes unreported too), and one it\n recognizes but that names the wrong parser reports nothing and looks\n like a clean codebase.\n\n Two specific traps:\n - **Do not copy from `detect --json`.** It reports the\n *repository\'s* languages in a different vocabulary. It says\n `C++` where the list above says `Cpp`.\n - **`Tsx` and `TypeScript` are two parsers, not aliases.** A rule\n over `.tsx` files that declares `TypeScript` does not match JSX\n syntax; it does not read those files at all.\n - **`Markdown` sees blocks only.** tree-sitter-markdown splits\n block and inline grammars and ast-grep exposes only the block\n tree, so `atx_heading`, `setext_heading`, `fenced_code_block`,\n `list_item`, `paragraph`, `section` and `document` are real kinds\n and everything inside a line is one opaque `inline` node. There is\n no `link`, `emphasis` or `strong_emphasis` kind; naming one is a\n config error that exits 8 and takes the whole scan with it, and\n writing it as a pattern (`[$T]($U)`) matches nothing forever with\n no error at all. A rule about the words inside a line belongs to\n Vale, not here. ast-grep also has no count and no absence\n assertion, so "every doc has an h1" is not expressible as an `sg`\n rule at any severity.\n\n5. **Check any variadic pattern against the separator trap.** A `$$$`\n next to a comma does not mean "zero or more". The `,` in the pattern\n is itself an AST node, and under ast-grep\'s default `smart`\n strictness every node in the pattern must match, so a call with no\n comma cannot match a pattern that has one. Measured against the\n ast-grep this CLI ships (v%(AST_GREP_VERSION)s), given the four calls\n `foo()`, `foo(1)`, `foo(1,2)`, and `foo(1,2,3)`:\n\n| pattern | what it matches |\n|--------------------|----------------------------------------------|\n| `foo($$$)` | all four, `foo()` included |\n| `foo($A, $$$)` | `foo(1,2)` and `foo(1,2,3)`, never `foo(1)` |\n| `foo($$$, $A)` | `foo(1)` alone |\n| `foo($A, $$$, $B)` | `foo(1,2)` alone |\n\n A standalone `$$$` needs none of this. It is the comma beside it\n that narrows the pattern. The two remedies are not the same:\n\n - **Trailing `$$$`**: write the pattern as an object with\n `strictness: ast`, which compares named AST nodes and ignores the\n separator. An object pattern also requires `context` and\n `selector`:\n ```yaml\n rule:\n pattern:\n context: foo($A, $$$)\n selector: call_expression\n strictness: ast\n ```\n This moves the boundary from two arguments to one, **not to\n zero**, `$A` still has to bind something, so `foo()` is still\n unmatched. And `strictness` is valid only inside the pattern\n object: at rule level ast-grep rejects it as an unknown field and\n fails the whole scan.\n - **Leading `$$$`**: `strictness: ast` does not rescue it. Use\n `any` with one branch per arity you mean to cover.\n\n This is upstream\'s intended behaviour (ast-grep/ast-grep#1365,\n closed as working-as-intended), not a bug waiting on a release:\n 0.45.2 behaves identically, so there is no version to wait for.\n\n6. **Write the tests.** Write\n `.taskless/rules/sg/<id>/.tests/<id>-YYYYMMDD-test.yml` with the matching\n `id` field plus `valid` and `invalid` arrays, at least two of each,\n drawn from real code where you can. The `id` must match the rule\'s\n `id` so ast-grep test filtering pairs them. These paths and this\n shape are the same ones the service writes; do not invent a different\n layout.\n\n **Both arrays must be non-empty, and `test` fails the rule if either\n one is.** An empty `invalid:` is not a neutral starting point to fill\n in later: ast-grep reports `1 passed; 0 failed` and exits zero over\n no cases at all, so a rule that matches nothing anywhere looks\n exactly like a rule that works. The `invalid:` bucket is the only\n thing that demonstrates the rule can fire.\n\n Where the rule has an arity boundary (anything from step 5), put a\n case on each side of it. A pattern that starts at two arguments when\n it was meant to start at one passes a test suite whose fixtures all\n have two.\n\n7. **Run the verify feedback loop.** Both commands take the rule\'s\n directory as their argument:\n ```\n %(TASKLESS_CLI)s verify .taskless/rules/sg/<id> --json\n %(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json\n ```\n `verify` asks whether the rule is well-formed: the YAML matches the\n ast-grep schema and every Taskless-required field is present. It\n needs no test file, so run it the moment the rule exists. `test` runs\n the cases, after running `verify` and stopping if that fails, so a\n malformed rule reports the malformation rather than a test complaint.\n\n Both answer in the same shape:\n ```json\n {"ok":true,"rules":[{"engine":"sg","ruleId":"no-eval",\n "ok":true,"errors":[],"ran":true}]}\n ```\n - `ok: true` → go to step 8.\n - `ok: false` → read `errors` and fix. Repeat up to 3 times.\n\n| what `errors` says | fix |\n|--------------------------------------|-------------------------------------------------|\n| the YAML doesn\'t match the schema | check field types against the upstream schema |\n| a Taskless-required field is missing | add `id`/`language`/`severity`/`message`/`rule` |\n| a case didn\'t behave as expected | fix the rule pattern OR the test case |\n\n A `regex` without an accompanying `kind` fails verification, the two\n always travel together.\n\n Pass a directory above a rule and every rule beneath it is checked,\n one entry per rule in the report. `.taskless/rules/sg` covers every\n ast-grep rule; no argument at all covers the project.\n\n8. **On success, report.** Show the rule directory and what is in it,\n plus a one-line summary of what the rule detects. Suggest\n `%(TASKLESS_CLI)s agent check` to validate against the broader codebase.\n\n9. **On failure, escalate, with confirmation.** If after the feedback\n loop the rule still cannot capture the user\'s cases:\n - Delete the candidate `.taskless/rules/sg/<id>/` directory so the\n repo is not left with a broken rule. One `rm -rf` removes the rule\n and its tests together.\n - Tell the user the local rule could not capture the cases, and that\n generating via the Taskless service uses a generation and requires\n login.\n - Only after the user confirms, fetch `%(TASKLESS_CLI)s agent\n create-remote-rule` and follow it. Do not call the service silently.\n\n## Two ways a relational rule matches nothing\n\nBoth of these parse, verify, run, and exit 0. Neither reports anything\never, which reads exactly like a clean codebase.\n\n**`follows` and `precedes` need `stopBy: end` to cross punctuation.**\nA sibling relation walks the immediate siblings, and in most grammars\nthe separators are nodes too. Between two array elements sits a `,`, so\nthe previous element is not the previous SIBLING. Measured on\nTypeScript, `[10, 10, 30]` with\n\n```\nrule:\n kind: number\n follows: { kind: number }\n```\n\nmatches nothing, and the same rule with `stopBy: end` inside `follows`\nmatches. If a sibling relation reports zero on input you believe should\nmatch, add `stopBy: end` before doubting the rest of the rule.\n\n**A `not` containing a bare metavariable excludes everything.** Writing\n"the name is not used in the initializer" as\n\n```\nrule:\n kind: variable_declarator\n has: { field: name, pattern: $A }\n not:\n has: { pattern: $A, stopBy: end }\n```\n\nmatches nothing at all, because `$A` is bound to the name node and that\nnode is its own descendant: the `not` finds it inside every declarator\nand rejects all of them. Scope the negation to the part you meant, in\nthis case the value:\n\n```\n not:\n has:\n field: value\n has: { pattern: $A, stopBy: end }\n```\n\nThe general shape: when a `not` searches the same subtree that bound the\nmetavariable, it always finds it.\n\n## Important Notes\n\n- Do NOT make any HTTP requests to taskless.io on this path.\n- Do NOT write to `.taskless/rule-metadata/`: no rule has a metadata\n sidecar, service-generated ones included, because the CLI never\n writes that directory. Locally authored rules iterate via file edits.\n- The verify loop is the quality gate. A clean failure is a legitimate\n reason to escalate, but only with the user\'s confirmation (step 9).\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent route`: re-decide the destination\n- `%(TASKLESS_CLI)s agent verify-rule`: the `verify` and `test` commands step 7 calls\n- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists\n- `%(TASKLESS_CLI)s agent create-remote-rule`: generate via the service (login)\n- `%(TASKLESS_CLI)s agent check`: validate the new rule against the codebase\n', B = '# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v6)\n\n## You are here\nThis is `create-vale-rule`. It helps you write a Vale rule: a check over\nthe words of a document, prose, markup, and the prose parts of code.\nIf that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route`\nand follow its decision rather than adapting this recipe.\n\n## Goal\nProduce a Vale rule that fires on the prose it should flag and stays\nquiet on the prose it should not, scoped to the files it is about.\n\n## Preconditions\n- `.taskless/` directory exists.\n- The agent can read/write files and run shell commands.\n- No auth required.\n\n## One rule, one directory\n\nEverything that defines a Vale rule lives in\n`.taskless/rules/vale/<id>/`. Writing a rule means creating that\ndirectory. Deleting a rule means deleting it. No file outside it is\ntouched either way, so two agents authoring two rules never collide.\n\n```\n.taskless/rules/vale/no-simply/\n no-simply.yml the style: what the rule looks for\n .vale.ini the scope: which files it applies to\n .tests/fail/bad.md prose it must flag\n .tests/pass/ok.md prose it must leave alone\n```\n\nThe rule is **incomplete until the style and the config both exist**,\nand skipping the config fails silently rather than loudly: the rule\nparses, the check passes, and nothing is ever reported. `verify` exists\nto catch exactly that, so run it (step 6) before you believe a rule\nworks.\n\n**The id appears four times and all four must agree:**\n\n```\n.taskless/rules/vale/no-simply/ <- the directory\n.taskless/rules/vale/no-simply/no-simply.yml <- the style file\nno-simply.no-simply = YES <- the assignment in .vale.ini\ntskl) rule = no-simply <- the breadcrumb in .vale.ini\n```\n\nThe doubled `no-simply.no-simply` is not a typo. Vale names a check\n`<style>.<rule>`, and here the rule\'s own directory is the style, so\nboth halves are the id. Anything else is a check that does not exist,\nwhich Vale accepts without complaint.\n\nYou will not find a project-wide `.vale.ini` to edit. The config Vale\nactually reads is assembled from every rule\'s own file at check time\nand is gitignored. Editing it is pointless, the next check regenerates\nit.\n\n## Steps\n\n1. **Say what the rule reads, then pick an extension point.** Write one\n sentence: "this fires when a document contains ___." Vale rules are\n built by extending one of its twelve checks, and the sentence tells\n you which. Twelve is measured, not counted off the docs: give Vale\n v%(VALE_VERSION)s an `extends` it does not know and it names the whole\n set back at you:\n\n ```\n \'extends\' key must be one of [capitalization conditional consistency\n existence occurrence repetition substitution readability spelling\n sequence metric script].\n ```\n\n The docs enumerate eleven, folding `readability` into `metric`. They\n are separate checks with separate fields.\n\n| If the rule is about… | extends |\n|-----------------------------------------------------------------------------------------|------------------|\n| words or phrases that should not appear | `existence` |\n| preferring one term over another. **including the correct spelling of a product name** | `substitution` |\n| the case of a whole heading or sentence | `capitalization` |\n| how many times something may appear | `occurrence` |\n| a word repeated back to back | `repetition` |\n| picking one of two acceptable spellings, consistently | `consistency` |\n| "if X appears, Y must also appear" | `conditional` |\n| a document-level length or ratio threshold | `metric` |\n| a readability grade, against a named formula | `readability` |\n| a misspelling, against a dictionary | `spelling` |\n| phrases that must appear in a fixed order | `sequence` |\n| anything the above cannot express (Tengo script) | `script` |\n\n **`capitalization` is about a whole scope, not a word.** It asks\n whether an entire heading or sentence matches a case pattern. It\n cannot express "the word GitHub, wherever it appears, is spelled\n thus". That is a `substitution`, because you are swapping a wrong\n spelling for a right one. Reaching for `capitalization` on a product\n name produces a rule that flags whole sentences: measured, a rule with\n `match: GitHub` reports `We host on Github and it is fine. should be\n GitHub`.\n\n For the five this recipe has no worked example of, `metric`,\n `readability`, `spelling`, `sequence`, `script`, read\n https://docs.vale.sh/styles before inventing something. Vale has no\n facility for a rule that does not extend one of these twelve, and an\n `extends` outside the set is not a rule that misbehaves: Vale exits 2\n and **every** Vale rule in the project goes unreported for that run.\n `verify` rejects it before Vale is invoked, and names the twelve.\n\n **The other seven have a worked rule at the end of this recipe**,\n nine rules between them, each with the near-miss that fails and why.\n Read the one closest to your intent before writing anything, the\n mistakes documented there are observed, and most of them fail\n silently.\n\n2. **Write the style file** to\n `.taskless/rules/vale/<id>/<id>.yml`, where `<id>` is kebab-case and\n names both the directory and the file.\n\n **One exception: a `consistency` rule\'s id must be word characters\n only** (`izeise`, not `ize-ise`). That check compiles the id into its\n pattern as a regex group name, and a hyphen there fails the entire\n Vale run. `verify` catches it. Kebab-case is correct for the other\n eleven.\n\n Every rule carries:\n\n| Field | Required | Notes |\n|-----------|----------|-----------------------------------------------|\n| `extends` | yes | one of the twelve above |\n| `message` | yes | shown to the user; see the `%%s` table below |\n| `level` | no | `suggestion` (default), `warning`, or `error` |\n| `scope` | no | narrow to part of a document; see below |\n| `link` | no | a URL the reader can follow for the reasoning |\n| `limit` | no | cap findings from this rule per scope |\n\n **`vocab` is not one of these.** It reads as though it were, but it is\n a per-check field: measured, only `existence`, `substitution`,\n `capitalization`, `conditional` and `repetition` accept it, and\n `occurrence`, `metric`, `readability`, `script` and `sequence` reject\n it. On one of those five it raises `E201` and takes every other Vale\n rule in the project down with it. It is listed with the per-check\n fields below.\n\n **The file extension must be `.yml`.** Measured: rename a working\n style file to `.yaml` and Vale loads nothing, no error, no warning,\n zero findings, and `<id>.<id> = YES` still parses. It is\n indistinguishable from a rule whose pattern never matched.\n\n **`scope` decides where the rule looks**, so getting it wrong is a\n silent under-fire rather than an error. An unrecognized scope is not\n rejected by Vale: `scope: fenced` loads, runs, and matches nothing.\n It is the worst of the three failures on this page, because unlike a\n bad `extends` or a foreign field it does not even take the run down to\n tell you. The rule is inert, forever. `verify` rejects a scope\n outside the table below, which is the only layer that ever will.\n\n Every value below was measured against Vale v%(VALE_VERSION)s by\n authoring a rule with that scope and a document the rule had to flag.\n "Fires" means the finding appeared; a scope that never fired is not on\n this list.\n\n| `scope` | reaches |\n|------------------------|-----------------------------------------------------------|\n| *(omitted)* | everything the format exposes as prose |\n| `text` | prose only, not inline code, not fenced blocks |\n| `code` | inline code spans only |\n| `raw` | the unparsed document: prose, inline code, fenced blocks |\n| `heading` | every heading |\n| `heading.h1`…`h6` | headings of that level |\n| `paragraph` | one paragraph at a time |\n| `sentence` | one sentence at a time |\n| `list` | list items |\n| `blockquote` | quoted blocks |\n| `link` | link text, not the URL |\n| `alt` | image alt text |\n| `summary` | `<summary>` of a disclosure |\n| `strong`, `emphasis` | bold and italic runs |\n| `table` | any part of a table |\n| `table.header` | header cells |\n| `table.cell` | body cells |\n| `table.caption` | a table\'s caption |\n| `figure.caption` | a figure\'s caption, but see below |\n| `frontmatter` | every YAML front-matter value |\n| `frontmatter.<key>` | one front-matter key\'s value |\n| `text.class.<name>` | HTML elements carrying that class |\n| `comment` | every comment, in a comment-tier format |\n| `comment.line` | `//`-style comments |\n| `comment.block` | `/* … */`-style comments |\n\n **`raw` subsumes `code` and `text`.** Measured on one document holding\n the token in prose, in an inline span, and in a fenced block: `text`\n found one, `code` found one, `[code, text]` found two, `raw` found all\n three. If you want prose and inline code but not fenced blocks, write\n the list, `raw` is not "a bit wider", it is everything.\n\n **Vale drops everything inside a `<figure>` element.** Measured: a\n `<figcaption>` nested in `<figure>` is invisible to *every* scope,\n `text` and `raw` included, so a `scope: figure.caption` rule over a\n normally-marked-up figure reports nothing and looks like a bad scope.\n A bare `<figcaption>` is linted, and `figure.caption` fires on it. If a\n fixture is not firing, check whether its subject is inside a `<figure>`\n before you touch the pattern.\n\n **`scope` also takes `~` and `&`.** `~code` is everything but inline\n code; `text & ~code` chains two operands; a list (`[code, text]`) is a\n union. All three parse and behave.\n\n **A negation over a scope Vale does not know is a silent no-op.**\n Measured: `~banana` and `text & ~banana` both fire on everything,\n because there is no such scope to subtract. A typo inside a `~` does\n not narrow the rule and does not widen it visibly. It removes the\n exclusion you wrote the rule for. `verify` checks the operands inside\n `~` and `&` as strictly as a bare one, for exactly this reason, the\n one place it is deliberately stricter than Vale itself.\n\n **`scope` is per-rule, and rules do not interact.** Taskless assembles\n every rule\'s matchers into one config for the run, which invites the\n assumption that one rule\'s `scope` narrows another\'s, or that two\n rules over the same file compete. They do not: each check carries its\n own scope and is evaluated independently. If a rule is over-firing,\n the cause is in that rule\'s own `scope` and glob, never in a\n neighbour\'s.\n\n **A directive turns any rule off, `raw` included, as of Vale 3.20.0.**\n `<!-- vale <id>.<id> = NO -->` opens a zone and `= YES` closes it.\n Vale records the region each directive covers and suppresses any\n alert located inside it, which reaches a `raw`-scoped rule too.\n Through 3.19.0 a directive was applied to the *parsed* document and\n `raw` reads the unparsed one, so a `raw` rule fired straight through\n every zone: a rule about a shell command, a flag, or a package name\n was exempt-or-remove with nothing in between. It is not any more.\n\n One thing to know before writing one over a `raw` rule: at `raw`\n scope the directive line is itself linted text. A rule whose token\n appears in its own id matches the marker that silences it, and\n reports a finding on the directive. Name the rule so its own id does\n not contain the word it looks for.\n\n **A zone is two lines, and no blank line may separate it from the\n prose it wraps.** Measured on Vale 3.20.0:\n\n - Inline at a list item\'s continuation indent works, and this is the\n form to reach for. The pair covers the lines between it and\n nothing else.\n - At column 0 it works too, and it ends any list it interrupts.\n Blocks that then reparse as indented code are already outside a\n prose rule\'s reach, so a zone placed there can move coverage\n rather than restore it.\n - A pair separated from its prose by blank lines, at an indent past\n the code-block threshold, suppresses nothing: what it wraps is not\n prose any more. Nothing reports that, either.\n\n Through 3.19.0 the first of those did nothing at all. An inline pair\n was read once per block, so the `NO` and the `YES` cancelled out\n before the paragraph was linted and the words stayed reported with\n no error and no warning. A zone had to wrap a whole step or a whole\n section at the margin, and exempting the two steps of this recipe\n that quote hedging words would have put 176 lines of prose out of\n reach to keep two words of an example. That price is gone.\n\n **A shown directive has to be inline or fenced, never a bare line.**\n A directive on a line of its own now takes effect on the recipe at\n any indent, and the CLI strips it before serving so a reader never\n sees it. Measured: a directive inside backticks and a directive\n inside a fenced block are both inert, which is why every directive\n quoted in this file is one or the other.\n\n **Then the fields the extension point adds**. This is where the rule\n actually lives, and each check reads only its own:\n\n| extends | its fields |\n|------------------|------------------------------------------------------------------------------|\n| `existence` | `tokens` (a list) or `raw`; `ignorecase`, `nonword`, `exceptions`, `append`, `vocab` |\n| `substitution` | `swap` (a map of observed → expected); `ignorecase`, `nonword`, `exceptions`, `capitalize`, `pos`, `vocab` |\n| `capitalization` | `match`; `style` (with `$title`), `exceptions`, `threshold`, `indicators`, `prefix`, `vocab` |\n| `occurrence` | `token`, `max` and/or `min`; `ignorecase` |\n| `repetition` | `tokens`; `alpha`, `ignorecase`, `exceptions`, `max`, `vocab` |\n| `consistency` | `either` (a map of the two acceptable forms); `ignorecase`, `nonword` |\n| `conditional` | `first`, `second`; `exceptions`, `ignorecase`, `vocab` |\n| `metric` | `formula`, `condition` |\n| `readability` | `metrics` (a list of formula names), `grade` |\n| `spelling` | `aff`, `dic`, `custom`, `filters`, `ignore`, `threshold` |\n| `sequence` | `tokens` (each a `pattern`/`tag` map); `ignorecase`, `exceptions` |\n| `script` | `script` (Tengo source) |\n\n The list above is measured, not transcribed: every entry was added to\n a minimal rule of that check and the run watched for `E201`. Three\n corrections fall out of it, all against the published docs:\n `capitalization` takes `prefix` (singular) and rejects both `prefixes`\n and `suffixes`, it rejects `ignorecase`, and `occurrence` rejects\n `exceptions` and `vocab`.\n\n **Field names are matched case-insensitively, but `extends`, `message`\n and `level` are not.** Measured: `Tokens:` and `ignoreCase:` are read\n exactly as their lowercase spellings, while `EXTENDS:` fails with\n "Missing the required \'extends\' key". Their *values* are case-sensitive\n too, `level: WARNING` and `extends: Existence` are both rejected.\n Write everything lowercase and none of this can bite you.\n\n **A field from the wrong check is the loudest failure Vale has.**\n `tokens` on an `occurrence` check gives\n `E201 … has invalid keys: \'tokens\'`, exit 2, and, because Vale reads\n one assembled config per run. **no** Vale rule in the project reports\n anything. `verify` rejects the rule before Vale is invoked, so this\n cannot reach `check`.\n\n **Two checks are exempt, and that is not a licence.** Measured,\n `consistency` and `spelling` accept any key at all: `bananafield:\n true` on either loads without complaint and is ignored. A misspelled\n field on those two is a silent no-op instead of a loud one, so the\n schema cannot catch a typo there and neither can Vale. Read the field\n list twice when writing those two.\n\n **What `%%s` fills with depends on the extension point.** Getting this\n wrong is the one mistake in this recipe that passes every check below\n. The rule fires, the fixtures are green, and only a human reading the\n message sees that it is nonsense.\n\n| extends | `%%s` count | fills with, left to right |\n|------------------|-------------|-------------------------------------------------------|\n| `existence` | one | the matched text |\n| `substitution` | **two** | the **replacement**, then the matched text |\n| `capitalization` | one | the scope that failed (the whole heading or sentence) |\n\n Measured: a `substitution` message with a single `%%s` interpolates the\n *replacement*, not the match, so `"Use GitHub not %%s"` against the text\n `Github` renders `Use GitHub not GitHub`.\n\n For the other nine, do not guess. Write the message, run step 6, and\n read it back off the finding, no test you can write catches a wrong\n `%%s`, so your own eyes on the rendered message are the check.\n\n ```yaml\n # existence, flag these tokens wherever they appear\n extends: existence\n message: "Avoid \'%%s\', it hides the work from the reader"\n level: warning\n ignorecase: true\n tokens:\n - simply\n - just\n ```\n\n ```yaml\n # substitution, first %%s is the replacement, second is what was found\n extends: substitution\n message: "Use \'%%s\' instead of \'%%s\'"\n level: warning\n ignorecase: true\n swap:\n utilize: use\n "in order to": to\n ```\n\n ```yaml\n # capitalization, a whole heading must be in sentence case\n extends: capitalization\n message: "\'%%s\' should be in sentence case"\n level: warning\n scope: heading\n match: $sentence\n exceptions:\n - Taskless\n - API\n ```\n\n `match` takes `$sentence`, `$title`, `$lower`, or `$upper`. A literal\n string is legal but means "this whole scope must read exactly that",\n which is almost never what anyone wants. See step 1.\n\n **`$sentence` means first word capitalized, everything else lowercase\n, proper nouns included.** It is not "sentence case allowing proper\n nouns". Measured with `exceptions: [Taskless, API]` on headings:\n\n| Heading | Result |\n|-----------------------------------|------------------------------------------------|\n| `Getting started with the API` | quiet |\n| `Getting started with APIs` | quiet, an exception covers its plural |\n| `Taskless and the API` | quiet, an exception may lead the scope |\n| `Getting started with Kubernetes` | **fires**: a proper noun you did not list |\n| `getting started lowercase` | **fires**: the first word must be capitalized |\n| `Getting Started With Title Case` | **fires** |\n\n So `exceptions` is not decoration: every proper noun, product name and\n acronym the docs use has to be listed, or the rule flags correct\n headings. Collect them from the docs before writing the rule, and\n expect to add to the list.\n\n3. **Know what you are writing: `tokens` and `swap` keys are patterns,\n not literals.** They compile as **Go RE2** regular expressions.\n\n *This step is about `tokens` and `swap` only. A `capitalization`,\n `occurrence` or `metric` rule has neither, skip to step 4.*\n\n - `(?:…)`, `[…]`, `|`, `+`, `?` all work.\n - **Lookahead and lookbehind do not exist in RE2.** A rule that needs\n "X but not when followed by Y" cannot be written as a single\n `substitution`; split it or narrow with `scope`.\n - **Word boundaries are applied for you, around the whole pattern.**\n Measured: `Github` does not fire inside `GithubToken`, and the\n multi-word `click here` does not fire inside `Clicking here`.\n - **A hyphen is a boundary, so a hyphenated compound is not\n protected.** `obviously` fires inside `obviously-named`, while\n `obviously_stale` is safe because `_` is a word character. Do not\n reach for a hyphenated compound as a `pass/` near-miss; it is the\n case most likely to fire.\n - **Regex metacharacters in a real phrase are live.** "maybe?" is a\n pattern meaning "mayb" followed by an optional "e". Escape it.\n - **Overlapping alternatives resolve first-wins**, one finding per\n match. If `can login` and `login with` both match a sentence, you\n get whichever is written first, once, not both.\n - `ignorecase: true` matches any casing **and still skips text that\n already equals the replacement.** Measured with `Github: GitHub`:\n `github` and `Github` are flagged, `GitHub` is not. You do not need\n `ignorecase: false` to protect the correct spelling.\n - `raw` takes a full regex when `tokens` is too restrictive;\n `nonword` removes the implicit boundaries.\n - **A token made only of punctuation can never match without\n `nonword: true`.** The boundaries above are `\\b`, which needs a word\n character on the inside. An em dash has none, on either side.\n Measured against `This is a sentence, with an em dash.`:\n\n ```yaml\n # fires on nothing, ever, and reports no error\n extends: existence\n message: "Use a comma, not an em dash"\n tokens:\n - \', \'\n ```\n\n ```yaml\n # fires\n extends: existence\n message: "Use a comma, not an em dash"\n nonword: true\n tokens:\n - \', \'\n ```\n\n The first rule verifies, tests green if its `fail/` fixture is\n missing the dash, and reports nothing forever. Any token whose\n pattern contains no `\\w` (punctuation, an emoji, a bare symbol)\n needs `nonword: true`.\n\n - **A bare word finds senses you did not mean.** `landed on` in a rule\n <!-- vale no-hedging.no-hedging = NO -->\n about jargon also matches "the plane landed on time"; `simply` in a\n rule about hedging also matches "simply connected" in a maths doc.\n <!-- vale no-hedging.no-hedging = YES -->\n Narrow the token to the **collocation** you actually object to\n (`landed on a decision`, not `landed on`), and check that you got it\n right by writing the `pass/` fixture from the literal sense *first*:\n put the innocent sentence in `pass/` before you write the guilty one\n in `fail/`. A rule whose `pass/` bucket was written afterwards tends\n to contain only sentences the author already knew were safe.\n\n Vale also understands the markup, which decides what counts as text\n before your pattern ever runs. Measured in markdown:\n\n - **URLs and code spans are not prose.** A `Github` key fires on\n `Plain Github here` and not on `https://Github.com/x` or\n `` `Github/docs` ``.\n - **Link text *is* prose.** In `[click here](https://example.com)`,\n `click here` is matched. The URL is not. With no `scope`, a rule\n fires on both link text and ordinary prose; `scope: link` narrows it\n to link text alone. Measured: without a scope the token hit both the\n link and the sentence; with `scope: link`, only the link.\n\n4. **Scope the rule** by writing `.taskless/rules/vale/<id>/.vale.ini`.\n This is the step that is easy to skip and impossible to notice\n skipping. A rule with no config is enabled nowhere: it parses, it\n runs, and it reports nothing.\n\n The whole file, for a rule that applies to markdown:\n\n ```ini\n # Which files this rule applies to.\n [*.md]\n tskl) rule = no-simply\n BasedOnStyles =\n no-simply.no-simply = YES\n ```\n\n Four lines, and each one earns its place:\n\n - `[*.md]` is a **matcher**: a glob over paths, deciding which files\n this rule sees. Match it to the files the rule is actually about,\n such as `[*.{md,markdown}]` or `[docs/**/*.md]`. A rule can declare\n several matchers if it needs to. Before you widen a glob, check the\n reach table below, what Vale does to a file it cannot parse is not\n "nothing".\n - `tskl) rule = <id>` is a breadcrumb Taskless reads to attribute the\n matcher back to this rule after assembly interleaves every rule\'s\n matchers into one file. Vale parses the key and ignores it. Write it\n in every matcher you add, or the tooling loses track of who owns\n what.\n - `BasedOnStyles =` with an empty value says explicitly that no\n bundled style loads. The default is already empty, so this changes\n nothing today. Write it anyway: it tells the next reader that no\n style is missing.\n - `no-simply.no-simply = YES` turns the rule on. The first half is\n the style, which is this rule\'s directory; the second is the check\n inside it, which is the file. Both are the id.\n\n **Scope a rule *out* with a second matcher, not a cleverer glob.** A\n glob says which files a rule sees; it has no way to say "these but not\n those". The exclusion is a second matcher that assigns `NO`, and\n because precedence here is positional (a later matcher wins), the\n exclusion goes **after** the inclusion:\n\n ```ini\n # Every markdown file…\n [*.md]\n tskl) rule = no-simply\n BasedOnStyles =\n no-simply.no-simply = YES\n\n # …except the changelog, which quotes release notes verbatim.\n [CHANGELOG.md]\n tskl) rule = no-simply\n no-simply.no-simply = NO\n ```\n\n Write the breadcrumb in the second matcher too, or assembly attributes\n it to nobody. Reversing the two blocks silently re-enables the rule on\n the file you meant to exempt, and nothing reports that.\n\n **Do NOT write `StylesPath` or `MinAlertLevel` here.** Those describe\n the run rather than a rule, the assembler supplies them, and a copy\n in a rule\'s config is dropped on the way in.\n\n Keep assignments underneath a matcher. An assignment above the first\n `[…]` line belongs to no matcher, and Vale ignores it after warning\n on stderr.\n\n **What a matcher\'s glob is allowed to catch.** Vale (v%(VALE_VERSION)s)\n treats a file one of four ways, decided by extension. The lists are\n rendered from the pinned Vale version, not written out here, so they\n track the shipped binary.\n\n - **markup**: the document is prose and the format\'s own non-prose\n constructs are skipped. This is the tier every `scope:` value\n assumes; `scope: heading` has nothing to find outside it:\n %(VALE_MARKUP_FORMATS)s\n - **comment text only**: the comments are linted and the code body\n <!-- vale no-hedging.no-hedging = NO -->\n is invisible, which is exactly right for "comments must not say\n \'obviously\'":\n <!-- vale no-hedging.no-hedging = YES -->\n %(VALE_COMMENT_FORMATS)s\n - **plaintext fallback**: everything else, `.yml` `.toml` `.sh`\n `.sql` and every extension not named above included. There is no\n parser, so the whole file is linted as prose: a rule matched to\n YAML flags key names and values, not just the comments. If that is\n not what the rule means, narrow the glob rather than accepting it.\n These land here despite reading like markup, so a `scope:` value\n has nothing to act on in them: %(VALE_PLAINTEXT_FORMATS)s\n - **not supported**: Vale parses these only by shelling out to an\n external program, and this build does not support any format that\n needs one:\n %(VALE_CONVERTER_FORMATS)s\n\n Do not tell the user to install the program. Taskless excludes these\n files from the run whatever is installed, so that a repository\n checks the same way on every machine; `.xml` could not work anyway,\n since an XSLT stylesheet is specific to the document.\n\n **A single unreadable file fails the whole Vale pass.** Vale exits 2\n with an `E100` runtime error and abandons the run, `--no-exit` does\n not suppress it, so every other Vale rule over every other file goes\n unreported. `[*.{md,typ}]` is not a slightly wider `[*.md]`; it is a\n matcher that takes `check` down the first time the repo grows a\n `.typ` file. Never put one of those extensions in a glob.\n\n That example changed with Vale v3.18.0, which is the point: the\n dangerous extension is whichever one the list above says needs a\n program, not the one you remember. `.mdx` was the example until that\n release parsed it natively, and `.typ` took its place.\n\n **`.mdx` is supported** as of Vale v3.18.0, which parses it natively\n rather than shelling out. `[*.{md,mdx}]` is a legitimate matcher\n again, the example this recipe used to warn about is no longer the\n broken one. Check the lists above rather than reaching for that\n memory: `.typ` moved the opposite way in the same release, so a\n matcher covering Typst is now the one that takes the run down.\n\n **In `.mdx`, a component\'s children are prose as of v3.19.0.** Vale\n reads a JSX element\'s children as the Markdown they are, so text\n inside a wrapping component (`<Steps>`, `<Tabs>`, `<Aside>`) is\n linted at its own source position. Only tags, attributes, `{...}`\n expressions, self-closing elements, and an element opened and closed\n on one standalone line are still treated as code.\n\n Two consequences for a rule you write against MDX. Coverage grew,\n so a rule can now fire in prose it never reached before, which is a\n finding count that moves without the rule changing. And the children\n carry the element\'s name as a class scope, exactly as MyST and\n Quarto directives do, so `scope: text.class.Aside` targets one\n component\'s content. That is an open family: `verify` accepts any\n `text.class.<name>` tail, because the set of component names is the\n author\'s, not Vale\'s.\n\n5. **Write the fixtures.** Two directories inside the rule, both flat.\n Vale lints the whole fixture tree, so a document nested a level\n deeper would be linted and never checked against either bucket, which\n `test` rejects by name rather than skipping. Keep both one level\n deep:\n\n ```\n .taskless/rules/vale/<id>/.tests/pass/ok.md # rule must stay quiet\n .taskless/rules/vale/<id>/.tests/fail/bad.md # rule must fire\n ```\n\n **The leading dot on `.tests/` is required.** ast-grep walks the\n rules tree and parses every `.yml` it reaches as a rule, and a plain\n `tests/` directory fails that scan for the whole project. A\n dot-directory is skipped. Do not rename it.\n\n Give the fixtures an extension your matcher\'s glob matches. A `.txt`\n fixture under a `[*.md]` matcher is never linted, so the `fail/`\n document silently passes.\n\n **The `pass/` bucket is not "correct prose".** Correct prose proves\n nothing. The rule was never going to fire on it. Fill it with the\n near-misses that would catch an over-broad pattern. What counts as a\n near-miss depends on the rule\'s shape:\n\n - **`tokens`/`swap` rules**: the noun form you are not flagging, the\n word inside a longer word, the term in a URL or a code span, the\n correct spelling itself.\n - **`scope`d rules**: the same phrase *outside* the scope. A rule\n with `scope: link` needs the phrase in ordinary prose; a rule with\n `scope: heading` needs it in body text. Without that, nothing proves\n the scope is doing anything.\n - **`capitalization` rules**: a scope that is entirely exceptions, a\n scope whose exception word comes first, and the plural of an\n exception.\n\n That is the half of the fixture set that has to work for you.\n\n **When the rule\'s subject normally appears in code, the `fail/`\n fixture must carry it three ways**, inline in a code span, inside a\n fenced block, and in ordinary prose, in that one document. A rule\n about a command, a flag, a package name or an env var has a subject\n that lives in fenced blocks in every real README, and the default\n scope cannot see fenced blocks at all. A `fail/` fixture written only\n in prose therefore fires, goes green, and the rule then catches none\n of the real violations. Measured on one document holding the token in\n all three places: the default scope found one of three, `raw` found\n three. If the fixture fires on the prose line and not on the other\n two, the answer is `scope: raw`. See step 2 for what that costs.\n\n **Fixtures run under a config that isolates this rule, so a green\n `test` is not evidence the rule reaches any real file.** `test`\n generates its own `.vale.ini` pointing at the fixture directory and\n enabling only `<id>.<id>`; your rule\'s own matcher globs are not\n consulted. So a glob of `packages/cli/src/**/*.ts` that matches\n nothing in the repository still produces a rule that verifies, tests\n green, and reports forever. The only check for that is a real\n `check` over a real file:\n\n ```\n %(TASKLESS_CLI)s check <a real path the rule should flag> --json\n ```\n\n Do that once, on a file you have deliberately made violate the rule,\n before you believe the rule works.\n\n6. **Verify, then test.** Two commands, both taking the rule\'s\n directory as their argument, both run from the project root:\n\n ```\n %(TASKLESS_CLI)s verify .taskless/rules/vale/<id> --json\n %(TASKLESS_CLI)s test .taskless/rules/vale/<id> --json\n ```\n\n `verify` asks whether the rule is well-formed: the style file parses,\n `extends` names one of the twelve checks, `message` is present, `level`\n is one Vale accepts, every `scope` operand is one Vale honors, every\n field belongs to the check the rule extends, and the config declares a\n matcher that enables `<id>.<id>`. It does **not** need fixtures, so run\n it as soon as the style file exists.\n\n Those checks are measured against Vale v%(VALE_VERSION)s rather than\n transcribed from its docs, and they run **before** Vale is invoked.\n That ordering matters for two of them: an unknown `extends` and a\n foreign field each fail the whole Vale run rather than just this rule,\n so letting either reach the binary would take every other Vale rule\'s\n findings down with it.\n\n `test` runs the rule against both buckets. It runs `verify` first and\n stops if that fails, so a malformed rule tells you what is malformed\n instead of complaining about fixtures.\n\n Both report the same shape:\n\n ```json\n {"ok":true,"rules":[{"engine":"vale","ruleId":"no-simply",\n "ok":true,"errors":[],"ran":true}]}\n ```\n\n `ok` is the answer. `errors` names what failed, one string per\n problem. Exit code is 0 when every rule passed and 1 otherwise, so\n both are safe to script.\n\n Pass a directory above a rule and every rule beneath it is checked,\n reported one entry per rule. `.taskless/rules/vale` covers every Vale\n rule; no argument at all covers the project.\n\n If you would rather see the raw findings, the message text and the\n line numbers, run `check` against a bucket instead:\n\n ```\n %(TASKLESS_CLI)s check .taskless/rules/vale/<id>/.tests/fail --json\n ```\n\n Read `results` there. Ignore `success` and the exit code: `success`\n says the run worked rather than that the fixture behaved, and the\n exit code follows severity, so a `level: error` rule exits 1 on\n `fail/` while a `warning` rule exits 0 and both are correct. `test`\n answers pass-or-fail; `check` shows you the finding.\n\n When a `fail/` document does not fire, work down this list before\n touching the pattern. The cause is usually further up:\n - Does the rule have a `.vale.ini` at all?\n - Is the assignment underneath a `[…]` matcher?\n - Is it spelled `<id>.<id>`, both halves the same?\n - Does the matcher\'s glob match the fixture\'s extension?\n - Only then: does the pattern actually match the text?\n\n A `pass/` document that fires means the pattern is too broad. Look\n for a missing word boundary, an unescaped metacharacter, or a swap\n key that also matches the form you meant to allow.\n\n7. **Report.** Show the rule directory you created and what is in it, a\n one-line summary of what the rule flags, and the glob it is scoped\n to. The scope is a decision the user should see rather than one\n buried in a config. Note that a whole-project `%(TASKLESS_CLI)s check` skips\n your fixtures: `.taskless/` is excluded from the project walk, by\n design. `test` is what exercises them.\n\n## Worked rules\n\nNine rules that work, each paired with the near-miss that fails. Every\none was run against the bundled Vale; the "what goes wrong" lines are\nobserved behavior, not warnings in principle. Find the entry closest to\nyour intent and start there.\n\n### 1. Ban a word or phrase, `existence`\n\n> "Our docs shouldn\'t hedge."\n\n```yaml\nextends: existence\nmessage: "Avoid hedging: \'%%s\'"\nlevel: warning\nignorecase: true\ntokens:\n - we think\n - it seems\n - sort of\n```\n\n**Goes wrong:** dropping `ignorecase: true` when you meant any casing.\n`We think` at the start of a sentence then sails through. And a phrase\nwith punctuation is a *pattern*: `maybe?` means "mayb" plus an optional\n"e", so it matches `mayb`. Escape it: `maybe\\?`.\n\n### 2. Prefer one term over another, `substitution`\n\n> "Say \'sign in\', not \'login\', when it\'s a verb."\n\n```yaml\nextends: substitution\nmessage: "Use \'%%s\' instead of \'%%s\'"\nlevel: warning\nignorecase: true\nswap:\n \'login (?:to|into)\': sign in to\n \'to login\': to sign in\n```\n\n**Goes wrong:** one `%%s` instead of two. Measured, `"Use sign in not\n%%s"` against `login to` renders **"Use sign in not sign in to"**, the\nreplacement, twice. The rule fires, both fixtures pass, and only a human\nreading the message sees it. Two `%%s`, always, in that order.\n\n### 3. Enforce a product\'s spelling, `substitution`, not `capitalization`\n\n> "It\'s \'GitHub\', never \'Github\' or \'github\'."\n\n```yaml\nextends: substitution\nmessage: "Use \'%%s\' instead of \'%%s\'"\nlevel: error\nignorecase: true\nswap:\n github: GitHub\n```\n\n**Goes wrong:** reaching for `capitalization` because the complaint is\nabout capitals. Measured, `match: GitHub` flags whole sentences:\n`\'We host on Github and it is fine. should be GitHub\'`, because that\ncheck tests a *scope*, not a word. Note also that `ignorecase: true` is\nsafe here: Vale skips text already equal to the replacement, so the\ncorrect `GitHub` is not flagged.\n\n### 4. Sentence-case headings, `capitalization`\n\n> "Headings are sentence case; our product names keep their capitals."\n\n```yaml\nextends: capitalization\nmessage: "\'%%s\' should be in sentence case"\nlevel: warning\nscope: heading\nmatch: $sentence\nexceptions:\n - Taskless\n - API\n - Kubernetes\n```\n\n**Goes wrong:** a short `exceptions` list. `$sentence` lowercases\neverything after the first word, proper nouns included, so every product\nname and acronym in the docs must be listed or correct headings get\nflagged. Collect them from the docs first; expect to add more.\n\n### 5. Restrict a rule to link text, any check, plus `scope`\n\n> "\'click here\' is useless link text."\n\n```yaml\nextends: existence\nmessage: "Link text \'%%s\' says nothing, name the destination"\nlevel: warning\nscope: link\nignorecase: true\ntokens:\n - click here\n - read more\n```\n\n**Goes wrong:** omitting `scope: link`. Measured, the token then fires\non `[click here](…)` **and** on "click here to focus the search box" in\nordinary prose, which is a false positive on a sentence that is fine.\nWhenever a rule is about a *place* in the document, the `pass/` fixture\nmust contain the same phrase outside that place, otherwise nothing\nproves the scope works.\n\n### 6. Cap how often something appears, `occurrence`\n\n> "At most one exclamation mark per paragraph."\n\n```yaml\nextends: occurrence\nmessage: "Too many exclamation marks"\nlevel: warning\nscope: paragraph\ntoken: "!"\nmax: 1\n```\n\n**Goes wrong:** forgetting `scope`. The count is per scope, so with no\nscope you are capping the whole document rather than the paragraph.\nNote `token` here is singular, this check takes one, not a `tokens` list.\n\n### 7. Catch a doubled word, `repetition`\n\n> "\'the the\' keeps slipping through review."\n\n```yaml\nextends: repetition\nmessage: "\'%%s\' is repeated"\nlevel: warning\nalpha: true\ntokens:\n - \'[^\\s]+\'\n```\n\n**Goes wrong:** leaving the pattern unquoted. Measured, an unquoted\n`[^\\s]+` in YAML silently matches nothing, zero findings, no error, no\ndiagnostic. Quote any pattern containing a backslash. This is the\nfailure mode this recipe warns about most, arriving through YAML rather\nthan through Vale.\n\n### 8. One spelling or the other, consistently, `consistency`\n\n> "Pick -ize or -ise and stick to it."\n\n```yaml\nextends: consistency\nmessage: "Use \'%%s\' consistently"\nlevel: warning\nnonword: true\neither:\n organize: organise\n```\n\n**The id must be word characters only.** `consistency` is the one\nextension point that compiles the rule\'s own name into the pattern, as\na `(?P<id>…)` capture group, and Go RE2 rejects a group name containing\na hyphen. Measured: an id of `ize-ise` fails with `E201 … invalid group\nname` and takes **every** Vale rule in the project down with it, because\nVale reads one config for the whole run. Name this one `izeise` or\n`spelling_variants`. Kebab-case is right everywhere else.\n\n**Goes wrong:** expecting it to pick a winner. It flags the *second*\nform once both appear in a document, it enforces internal consistency,\nnot house style. If you want one specific spelling, that is a\n`substitution`.\n\n### 9. Require a definition, `conditional`\n\n> "An acronym must be spelled out before it\'s used."\n\n```yaml\nextends: conditional\nmessage: "\'%%s\' has no definition"\nlevel: warning\nscope: text\nignorecase: false\nfirst: \'\\b([A-Z]{3,5})\\b\'\nsecond: \'(?:\\b[A-Z][a-z]+ )+\\(([A-Z]{3,5})\\)\'\n```\n\n`first` is what must be justified; `second` is what justifies it.\nMeasured: `Application Programming Interface (API)` licenses every later\n`API`, while an undefined `XYZ` is flagged.\n\n**Goes wrong:** swapping the two, which inverts the rule into "flag the\ndefinition when the acronym is missing".\n\n## Important Notes\n\n- Vale reads one document at a time and has no cross-document view. A\n rule about consistency *between* documents cannot be written here.\n- Prose inside code is still prose: comments and docstrings are Vale\'s\n subject, and a rule about them belongs under a matcher whose glob\n covers the source files.\n- Do NOT add a `[*]` matcher to widen a rule that isn\'t firing. Matchers\n from every rule are assembled into one config, so `[*]` applies this\n rule to every file the walk reaches and turns one rule\'s scoping bug\n into a flood of false positives.\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent route`: re-decide the destination\n- `%(TASKLESS_CLI)s agent check`: run every engine over the repo\n- `%(TASKLESS_CLI)s agent create-sg-rule`: author a rule over code structure\n', X = `# Topic: delete-rule (CLI v%(CLI_VERSION)s / topic v4)
885
+ `, Z = '# Topic: create-sg-rule (CLI v%(CLI_VERSION)s / topic v5)\n\n## You are here\nThis is `create-sg-rule`. It helps you write an ast-grep rule: a check\nover the structure of a single source file, authored on this machine.\nIf that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route`\nand follow its decision rather than adapting this recipe.\n\n## Goal\nProduce a verified ast-grep rule and its test file, locally, without\ncontacting the Taskless service. You derive the rule yourself, write it\nin the canonical on-disk shape, and validate it with `verify` and\n`test` in a feedback loop. The files you produce match exactly what the\nservice writes, so `check`, `improve-rule`, `verify`, and `test` treat\nthem identically.\n\n## Preconditions\n- `.taskless/` directory exists.\n- The agent can read/write files and run shell commands.\n- No auth required.\n\n## One rule, one directory\n\nEverything defining a rule lives in `.taskless/rules/sg/<id>/`. Writing\na rule means creating that directory; deleting a rule means deleting\nit. No file outside it is touched either way, so two agents authoring\ntwo rules never collide.\n\n```\n.taskless/rules/sg/no-eval/\n no-eval.yml the rule\n .tests/no-eval-YYYYMMDD-test.yml its test cases\n```\n\n**The leading dot on `.tests/` is required.** ast-grep walks the rules\ntree and parses every `.yml` it reaches as a rule, so a plain `tests/`\ndirectory makes it read your test file as a rule and fail the scan for\nthe whole project. A dot-directory is skipped by that walk, and the\ntest runner still finds it. Do not rename it.\n\nUnlike Vale, an `sg` rule needs no second config file. It carries its\nown `files` and `ignores` inside the rule, so the rule file is the\nwhole rule.\n\n## Steps\n\n1. **Learn the ast-grep rule format.** Consult the ast-grep rule\n reference at https://ast-grep.github.io/guide/rule-config.html for\n valid fields and operators (`pattern`, `kind`, `regex`,\n `any`/`all`/`has`/`inside`/`not`) and meta-variable syntax. This\n recipe does not embed the schema. Read it from upstream rather than\n writing a rule from memory.\n\n2. **Gather and confirm the pattern.** Make sure you have concrete\n success cases (code that should pass) and failure cases (code that\n should be flagged), the target language, and any exceptions. Search\n the codebase for real instances and confirm exclusions with the user.\n\n3. **Check for an existing rule that already covers this.** Scan\n `.taskless/rules/sg/` and read each rule\'s `message`, `note`, and\n `rule` fields. If one overlaps, show the user and ask whether they\n would rather improve it, `%(TASKLESS_CLI)s agent improve-rule --anonymous`\n iterates a rule locally.\n\n4. **Author the rule in the canonical shape.** Write the rule to\n `.taskless/rules/sg/<id>/<id>.yml`, where `<id>` is kebab-case and\n names both the directory and the file. At minimum:\n - `id`: kebab-case, matching the filename (e.g. `no-eval`)\n - `language`: the target language, in ast-grep\'s spelling (below)\n - `severity`: `error`, `warning`, `info`, or `hint`\n - `message`: a concise single-line explanation\n - `rule`: the ast-grep rule object\n\n Optional but useful: `note` (multi-line guidance, supports markdown),\n `fix` (auto-fix pattern), `ignores` (file patterns to skip).\n\n **`language` is ast-grep\'s vocabulary.** The vendored rule schema\n types the field as a bare string with no enum, so the schema layer\n accepts anything. `verify` checks it separately: an off-list spelling\n is an error naming the accepted list, and a spelling ast-grep resolves\n but does not itself use (`typescript` for `TypeScript`) is a notice.\n Get it right anyway rather than leaning on that check. An unrecognized\n name aborts config parsing, which takes every other sg rule in the\n project down with it and reports nothing.\n ast-grep (v%(AST_GREP_VERSION)s) parses:\n\n %(AST_GREP_LANGUAGES)s\n\n Copy a spelling from that list rather than typing one that looks\n right. Off-list spellings fail two different ways and neither is\n caught locally: one ast-grep does not recognize at all takes the\n whole scan down (`did not match any variant of untagged enum\n SgLang`, so every other rule goes unreported too), and one it\n recognizes but that names the wrong parser reports nothing and looks\n like a clean codebase.\n\n Two specific traps:\n - **Do not copy from `detect --json`.** It reports the\n *repository\'s* languages in a different vocabulary. It says\n `C++` where the list above says `Cpp`.\n - **`Tsx` and `TypeScript` are two parsers, not aliases.** A rule\n over `.tsx` files that declares `TypeScript` does not match JSX\n syntax; it does not read those files at all.\n - **`Markdown` sees blocks only.** tree-sitter-markdown splits\n block and inline grammars and ast-grep exposes only the block\n tree, so `atx_heading`, `setext_heading`, `fenced_code_block`,\n `list_item`, `paragraph`, `section` and `document` are real kinds\n and everything inside a line is one opaque `inline` node. There is\n no `link`, `emphasis` or `strong_emphasis` kind; naming one is a\n config error that exits 8 and takes the whole scan with it, and\n writing it as a pattern (`[$T]($U)`) matches nothing forever with\n no error at all. A rule about the words inside a line belongs to\n Vale, not here. ast-grep also has no count and no absence\n assertion, so "every doc has an h1" is not expressible as an `sg`\n rule at any severity.\n\n5. **Check any variadic pattern against the separator trap.** A `$$$`\n next to a comma does not mean "zero or more". The `,` in the pattern\n is itself an AST node, and under ast-grep\'s default `smart`\n strictness every node in the pattern must match, so a call with no\n comma cannot match a pattern that has one. Measured against the\n ast-grep this CLI ships (v%(AST_GREP_VERSION)s), given the four calls\n `foo()`, `foo(1)`, `foo(1,2)`, and `foo(1,2,3)`:\n\n| pattern | what it matches |\n|--------------------|----------------------------------------------|\n| `foo($$$)` | all four, `foo()` included |\n| `foo($A, $$$)` | `foo(1,2)` and `foo(1,2,3)`, never `foo(1)` |\n| `foo($$$, $A)` | `foo(1)` alone |\n| `foo($A, $$$, $B)` | `foo(1,2)` alone |\n\n A standalone `$$$` needs none of this. It is the comma beside it\n that narrows the pattern. The two remedies are not the same:\n\n - **Trailing `$$$`**: write the pattern as an object with\n `strictness: ast`, which compares named AST nodes and ignores the\n separator. An object pattern also requires `context` and\n `selector`:\n ```yaml\n rule:\n pattern:\n context: foo($A, $$$)\n selector: call_expression\n strictness: ast\n ```\n This moves the boundary from two arguments to one, **not to\n zero**, `$A` still has to bind something, so `foo()` is still\n unmatched. And `strictness` is valid only inside the pattern\n object: at rule level ast-grep rejects it as an unknown field and\n fails the whole scan.\n - **Leading `$$$`**: `strictness: ast` does not rescue it. Use\n `any` with one branch per arity you mean to cover.\n\n This is upstream\'s intended behaviour (ast-grep/ast-grep#1365,\n closed as working-as-intended), not a bug waiting on a release:\n 0.45.2 behaves identically, so there is no version to wait for.\n\n6. **Write the tests.** Write\n `.taskless/rules/sg/<id>/.tests/<id>-YYYYMMDD-test.yml` with the matching\n `id` field plus `valid` and `invalid` arrays, at least two of each,\n drawn from real code where you can. The `id` must match the rule\'s\n `id` so ast-grep test filtering pairs them. These paths and this\n shape are the same ones the service writes; do not invent a different\n layout.\n\n **Both arrays must be non-empty, and `test` fails the rule if either\n one is.** An empty `invalid:` is not a neutral starting point to fill\n in later: ast-grep reports `1 passed; 0 failed` and exits zero over\n no cases at all, so a rule that matches nothing anywhere looks\n exactly like a rule that works. The `invalid:` bucket is the only\n thing that demonstrates the rule can fire.\n\n Where the rule has an arity boundary (anything from step 5), put a\n case on each side of it. A pattern that starts at two arguments when\n it was meant to start at one passes a test suite whose fixtures all\n have two.\n\n7. **Run the verify feedback loop.** Both commands take the rule\'s\n directory as their argument:\n ```\n %(TASKLESS_CLI)s verify .taskless/rules/sg/<id> --json\n %(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json\n ```\n `verify` asks whether the rule is well-formed: the YAML matches the\n ast-grep schema and every Taskless-required field is present. It\n needs no test file, so run it the moment the rule exists. `test` runs\n the cases, after running `verify` and stopping if that fails, so a\n malformed rule reports the malformation rather than a test complaint.\n\n Both answer in the same shape:\n ```json\n {"ok":true,"rules":[{"engine":"sg","ruleId":"no-eval",\n "ok":true,"errors":[],"ran":true}]}\n ```\n - `ok: true` → go to step 8.\n - `ok: false` → read `errors` and fix. Repeat up to 3 times.\n\n| what `errors` says | fix |\n|--------------------------------------|-------------------------------------------------|\n| the YAML doesn\'t match the schema | check field types against the upstream schema |\n| a Taskless-required field is missing | add `id`/`language`/`severity`/`message`/`rule` |\n| a case didn\'t behave as expected | fix the rule pattern OR the test case |\n\n A `regex` without an accompanying `kind` fails verification, the two\n always travel together.\n\n Pass a directory above a rule and every rule beneath it is checked,\n one entry per rule in the report. `.taskless/rules/sg` covers every\n ast-grep rule; no argument at all covers the project.\n\n8. **On success, report.** Show the rule directory and what is in it,\n plus a one-line summary of what the rule detects. Suggest\n `%(TASKLESS_CLI)s agent check` to validate against the broader codebase.\n\n9. **On failure, escalate, with confirmation.** If after the feedback\n loop the rule still cannot capture the user\'s cases:\n - Delete the candidate `.taskless/rules/sg/<id>/` directory so the\n repo is not left with a broken rule. One `rm -rf` removes the rule\n and its tests together.\n - Tell the user the local rule could not capture the cases, and that\n generating via the Taskless service uses a generation and requires\n login.\n - Only after the user confirms, fetch `%(TASKLESS_CLI)s agent\n create-remote-rule` and follow it. Do not call the service silently.\n\n## Two ways a relational rule matches nothing\n\nBoth of these parse, verify, run, and exit 0. Neither reports anything\never, which reads exactly like a clean codebase.\n\n**`follows` and `precedes` need `stopBy: end` to cross punctuation.**\nA sibling relation walks the immediate siblings, and in most grammars\nthe separators are nodes too. Between two array elements sits a `,`, so\nthe previous element is not the previous SIBLING. Measured on\nTypeScript, `[10, 10, 30]` with\n\n```\nrule:\n kind: number\n follows: { kind: number }\n```\n\nmatches nothing, and the same rule with `stopBy: end` inside `follows`\nmatches. If a sibling relation reports zero on input you believe should\nmatch, add `stopBy: end` before doubting the rest of the rule.\n\n**A `not` containing a bare metavariable excludes everything.** Writing\n"the name is not used in the initializer" as\n\n```\nrule:\n kind: variable_declarator\n has: { field: name, pattern: $A }\n not:\n has: { pattern: $A, stopBy: end }\n```\n\nmatches nothing at all, because `$A` is bound to the name node and that\nnode is its own descendant: the `not` finds it inside every declarator\nand rejects all of them. Scope the negation to the part you meant, in\nthis case the value:\n\n```\n not:\n has:\n field: value\n has: { pattern: $A, stopBy: end }\n```\n\nThe general shape: when a `not` searches the same subtree that bound the\nmetavariable, it always finds it.\n\n## Important Notes\n\n- Do NOT make any HTTP requests to taskless.io on this path.\n- Do NOT write to `.taskless/rule-metadata/`: no rule has a metadata\n sidecar, service-generated ones included, because the CLI never\n writes that directory. Locally authored rules iterate via file edits.\n- The verify loop is the quality gate. A clean failure is a legitimate\n reason to escalate, but only with the user\'s confirmation (step 9).\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent route`: re-decide the destination\n- `%(TASKLESS_CLI)s agent verify-rule`: the `verify` and `test` commands step 7 calls\n- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists\n- `%(TASKLESS_CLI)s agent create-remote-rule`: generate via the service (login)\n- `%(TASKLESS_CLI)s agent check`: validate the new rule against the codebase\n', ee = '# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v8)\n\n## You are here\nThis is `create-vale-rule`. It helps you write a Vale rule: a check over\nthe words of a document, prose, markup, and the prose parts of code.\nIf that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route`\nand follow its decision rather than adapting this recipe.\n\n## Goal\nProduce a Vale rule that fires on the prose it should flag and stays\nquiet on the prose it should not, scoped to the files it is about.\n\n## Preconditions\n- `.taskless/` directory exists.\n- The agent can read/write files and run shell commands.\n- No auth required.\n\n## One rule, one directory\n\nEverything that defines a Vale rule lives in\n`.taskless/rules/vale/<id>/`. Writing a rule means creating that\ndirectory. Deleting a rule means deleting it. No file outside it is\ntouched either way, so two agents authoring two rules never collide.\n\n```\n.taskless/rules/vale/no-simply/\n no-simply.yml the style: what the rule looks for\n .vale.ini the scope: which files it applies to\n .tests/fail/bad.md prose it must flag\n .tests/pass/ok.md prose it must leave alone\n```\n\nThe rule is **incomplete until the style and the config both exist**,\nand skipping the config fails silently rather than loudly: the rule\nparses, the check passes, and nothing is ever reported. `verify` exists\nto catch exactly that, so run it (step 6) before you believe a rule\nworks.\n\n**The id appears four times and all four must agree:**\n\n```\n.taskless/rules/vale/no-simply/ <- the directory\n.taskless/rules/vale/no-simply/no-simply.yml <- the style file\nno-simply.no-simply = YES <- the assignment in .vale.ini\ntskl) rule = no-simply <- the breadcrumb in .vale.ini\n```\n\nThe doubled `no-simply.no-simply` is not a typo. Vale names a check\n`<style>.<rule>`, and here the rule\'s own directory is the style, so\nboth halves are the id. Anything else is a check that does not exist,\nwhich Vale accepts without complaint.\n\nYou will not find a project-wide `.vale.ini` to edit. The config Vale\nactually reads is assembled from every rule\'s own file at check time\nand is gitignored. Editing it is pointless, the next check regenerates\nit.\n\n## Steps\n\n1. **Say what the rule reads, then pick an extension point.** Write one\n sentence: "this fires when a document contains ___." Vale rules are\n built by extending one of its twelve checks, and the sentence tells\n you which. Twelve is measured, not counted off the docs: give Vale\n v%(VALE_VERSION)s an `extends` it does not know and it names the whole\n set back at you:\n\n ```\n \'extends\' key must be one of [capitalization conditional consistency\n existence occurrence repetition substitution readability spelling\n sequence metric script].\n ```\n\n The docs enumerate eleven, folding `readability` into `metric`. They\n are separate checks with separate fields.\n\n| If the rule is about… | extends |\n|-----------------------------------------------------------------------------------------|------------------|\n| words or phrases that should not appear | `existence` |\n| preferring one term over another. **including the correct spelling of a product name** | `substitution` |\n| the case of a whole heading or sentence | `capitalization` |\n| how many times something may appear | `occurrence` |\n| a word repeated back to back | `repetition` |\n| picking one of two acceptable spellings, consistently | `consistency` |\n| "if X appears, Y must also appear" | `conditional` |\n| a length or ratio threshold, over the document or one `scope` of it | `metric` |\n| a readability grade, against a named formula | `readability` |\n| a misspelling, against a dictionary | `spelling` |\n| phrases that must appear in a fixed order | `sequence` |\n| anything the above cannot express (Tengo script) | `script` |\n\n **`capitalization` is about a whole scope, not a word.** It asks\n whether an entire heading or sentence matches a case pattern. It\n cannot express "the word GitHub, wherever it appears, is spelled\n thus". That is a `substitution`, because you are swapping a wrong\n spelling for a right one. Reaching for `capitalization` on a product\n name produces a rule that flags whole sentences: measured, a rule with\n `match: GitHub` reports `We host on Github and it is fine. should be\n GitHub`.\n\n For the five this recipe has no worked example of, `metric`,\n `readability`, `spelling`, `sequence`, `script`, read\n https://docs.vale.sh/styles before inventing something. Vale has no\n facility for a rule that does not extend one of these twelve, and an\n `extends` outside the set is not a rule that misbehaves: Vale exits 2\n and **every** Vale rule in the project goes unreported for that run.\n `verify` rejects it before Vale is invoked, and names the twelve.\n\n **The other seven have a worked rule at the end of this recipe**,\n nine rules between them, each with the near-miss that fails and why.\n Read the one closest to your intent before writing anything, the\n mistakes documented there are observed, and most of them fail\n silently.\n\n2. **Write the style file** to\n `.taskless/rules/vale/<id>/<id>.yml`, where `<id>` is kebab-case and\n names both the directory and the file.\n\n **One exception: a `consistency` rule\'s id must be word characters\n only** (`izeise`, not `ize-ise`). That check compiles the id into its\n pattern as a regex group name, and a hyphen there fails the entire\n Vale run. `verify` catches it. Kebab-case is correct for the other\n eleven.\n\n Every rule carries:\n\n| Field | Required | Notes |\n|-----------|----------|-----------------------------------------------|\n| `extends` | yes | one of the twelve above |\n| `message` | yes | shown to the user; see the `%%s` table below |\n| `level` | no | `suggestion` (default), `warning`, or `error` |\n| `scope` | no | narrow to part of a document; see below |\n| `link` | no | a URL the reader can follow for the reasoning |\n| `limit` | no | cap findings from this rule per scope |\n\n **`vocab` is not one of these.** It reads as though it were, but it is\n a per-check field: measured, only `existence`, `substitution`,\n `capitalization`, `conditional` and `repetition` accept it, and\n `occurrence`, `metric`, `readability`, `script` and `sequence` reject\n it. On one of those five it raises `E201` and takes every other Vale\n rule in the project down with it. It is listed with the per-check\n fields below.\n\n **The file extension must be `.yml`.** Measured: rename a working\n style file to `.yaml` and Vale loads nothing, no error, no warning,\n zero findings, and `<id>.<id> = YES` still parses. It is\n indistinguishable from a rule whose pattern never matched.\n\n **`scope` decides where the rule looks**, so getting it wrong is a\n silent under-fire rather than an error. An unrecognized scope is not\n rejected by Vale: `scope: fenced` loads, runs, and matches nothing.\n It is the worst of the three failures on this page, because unlike a\n bad `extends` or a foreign field it does not even take the run down to\n tell you. The rule is inert, forever. `verify` rejects a scope\n outside the table below, which is the only layer that ever will.\n\n Every value below was measured against Vale v%(VALE_VERSION)s by\n authoring a rule with that scope and a document the rule had to flag.\n "Fires" means the finding appeared; a scope that never fired is not on\n this list.\n\n| `scope` | reaches |\n|------------------------|-----------------------------------------------------------|\n| *(omitted)* | everything the format exposes as prose |\n| `text` | prose only, not inline code, not fenced blocks |\n| `code` | inline code spans only |\n| `raw` | the unparsed document: prose, inline code, fenced blocks |\n| `heading` | every heading |\n| `heading.h1`…`h6` | headings of that level |\n| `paragraph` | one paragraph at a time |\n| `sentence` | one sentence at a time |\n| `list` | list items |\n| `blockquote` | quoted blocks |\n| `link` | link text, not the URL |\n| `alt` | image alt text |\n| `summary` | `<summary>` of a disclosure |\n| `strong`, `emphasis` | bold and italic runs |\n| `table` | any part of a table |\n| `table.header` | header cells |\n| `table.cell` | body cells |\n| `table.caption` | a table\'s caption |\n| `figure.caption` | a figure\'s caption, but see below |\n| `frontmatter` | every YAML front-matter value |\n| `frontmatter.<key>` | one front-matter key\'s value |\n| `text.class.<name>` | HTML elements carrying that class |\n| `comment` | every comment, in a comment-tier format |\n| `comment.line` | `//`-style comments |\n| `comment.block` | `/* … */`-style comments |\n| `doc(<selector>)` | elements matched by a CSS selector; see below |\n\n **`doc(<selector>)` picks part of a document by CSS selector**, the\n same way in every markup format, and a heading with everything under\n it is a `section`, so one section of a document is\n `doc(section:has(> h2:contains("Decision")))`. Chain it to narrow an\n ordinary scope to that element: `text & doc(...)` is prose inside it,\n `sentence & doc(...)` one sentence at a time inside it, `~doc(...)` is\n everything outside it. On its own, `doc(...)` lints what is INSIDE the\n element as one block, which is what `occurrence` (a section must say\n "we will") and `metric` (a section runs over budget) want. Measured: a\n `metric` with `scope: doc(section:has(> h2:contains("Consequences")))`\n and `formula: words` counts that section\'s words, not the document\'s.\n\n **A leaf element on its own is inert.** `doc(h2)` alone selects a\n heading, and a heading has nothing inside it to lint as a block, so the\n rule matches nothing, with no error anywhere. Write `text & doc(h2)`\n for the heading\'s own text. The same holds for `doc(p)` and `doc(li)`.\n `verify` accepts both spellings, because telling a leaf from a container\n needs the document; `test` shows which one fires.\n\n **The selector is Vale\'s to check, not `verify`\'s.** A selector Vale\n cannot compile (`doc(h2[)`) fails the whole run at load with\n `E201 invalid selector in \'doc(...)\'`, which `test` reports. A selector\n that compiles and matches nothing is silent, like any scope with no\n construct to find. `verify` checks that the term is `doc(` … `)` with\n something between, and no more.\n\n **`raw` subsumes `code` and `text`.** Measured on one document holding\n the token in prose, in an inline span, and in a fenced block: `text`\n found one, `code` found one, `[code, text]` found two, `raw` found all\n three. If you want prose and inline code but not fenced blocks, write\n the list, `raw` is not "a bit wider", it is everything.\n\n **Vale drops everything inside a `<figure>` element.** Measured: a\n `<figcaption>` nested in `<figure>` is invisible to *every* scope,\n `text` and `raw` included, so a `scope: figure.caption` rule over a\n normally-marked-up figure reports nothing and looks like a bad scope.\n A bare `<figcaption>` is linted, and `figure.caption` fires on it. If a\n fixture is not firing, check whether its subject is inside a `<figure>`\n before you touch the pattern.\n\n **`scope` also takes `~` and `&`.** `~code` is everything but inline\n code; `text & ~code` chains two operands; a list (`[code, text]`) is a\n union. All three parse and behave.\n\n **A negation over a scope Vale does not know is a silent no-op.**\n Measured: `~banana` and `text & ~banana` both fire on everything,\n because there is no such scope to subtract. A typo inside a `~` does\n not narrow the rule and does not widen it visibly. It removes the\n exclusion you wrote the rule for. `verify` checks the operands inside\n `~` and `&` as strictly as a bare one, for exactly this reason, the\n one place it is deliberately stricter than Vale itself.\n\n **`scope` is per-rule, and rules do not interact.** Taskless assembles\n every rule\'s matchers into one config for the run, which invites the\n assumption that one rule\'s `scope` narrows another\'s, or that two\n rules over the same file compete. They do not: each check carries its\n own scope and is evaluated independently. If a rule is over-firing,\n the cause is in that rule\'s own `scope` and glob, never in a\n neighbour\'s.\n\n **A directive turns any rule off, `raw` included, as of Vale 3.20.0.**\n `<!-- vale <id>.<id> = NO -->` opens a zone and `= YES` closes it.\n Vale records the region each directive covers and suppresses any\n alert located inside it, which reaches a `raw`-scoped rule too.\n Through 3.19.0 a directive was applied to the *parsed* document and\n `raw` reads the unparsed one, so a `raw` rule fired straight through\n every zone: a rule about a shell command, a flag, or a package name\n was exempt-or-remove with nothing in between. It is not any more.\n\n One thing to know before writing one over a `raw` rule: at `raw`\n scope the directive line is itself linted text. A rule whose token\n appears in its own id matches the marker that silences it, and\n reports a finding on the directive. Name the rule so its own id does\n not contain the word it looks for.\n\n **A zone is two lines, and no blank line may separate it from the\n prose it wraps.** Measured on Vale 3.20.0:\n\n - Inline at a list item\'s continuation indent works, and this is the\n form to reach for. The pair covers the lines between it and\n nothing else.\n - At column 0 it works too, and it ends any list it interrupts.\n Blocks that then reparse as indented code are already outside a\n prose rule\'s reach, so a zone placed there can move coverage\n rather than restore it.\n - A pair separated from its prose by blank lines, at an indent past\n the code-block threshold, suppresses nothing: what it wraps is not\n prose any more. Nothing reports that, either.\n\n Through 3.19.0 the first of those did nothing at all. An inline pair\n was read once per block, so the `NO` and the `YES` cancelled out\n before the paragraph was linted and the words stayed reported with\n no error and no warning. A zone had to wrap a whole step or a whole\n section at the margin, and exempting the two steps of this recipe\n that quote hedging words would have put 176 lines of prose out of\n reach to keep two words of an example. That price is gone.\n\n **A shown directive has to be inline or fenced, never a bare line.**\n A directive on a line of its own now takes effect on the recipe at\n any indent, and the CLI strips it before serving so a reader never\n sees it. Measured: a directive inside backticks and a directive\n inside a fenced block are both inert, which is why every directive\n quoted in this file is one or the other.\n\n **Then the fields the extension point adds**. This is where the rule\n actually lives, and each check reads only its own:\n\n| extends | its fields |\n|------------------|------------------------------------------------------------------------------|\n| `existence` | `tokens` (a list) or `raw`; `ignorecase`, `nonword`, `exceptions`, `append`, `vocab` |\n| `substitution` | `swap` (a map of observed → expected); `ignorecase`, `nonword`, `exceptions`, `capitalize`, `pos`, `vocab` |\n| `capitalization` | `match`; `style` (with `$title`), `exceptions`, `threshold`, `indicators`, `prefix`, `vocab` |\n| `occurrence` | `token`, `max` and/or `min`; `ignorecase` |\n| `repetition` | `tokens`; `alpha`, `ignorecase`, `exceptions`, `max`, `vocab` |\n| `consistency` | `either` (a map of the two acceptable forms); `ignorecase`, `nonword` |\n| `conditional` | `first`, `second`; `exceptions`, `ignorecase`, `vocab` |\n| `metric` | `formula`, `condition` |\n| `readability` | `metrics` (a list of formula names), `grade` |\n| `spelling` | `aff`, `dic`, `custom`, `filters`, `ignore`, `threshold` |\n| `sequence` | `tokens` (each a `pattern`/`tag` map); `ignorecase`, `exceptions` |\n| `script` | `script` (Tengo source) |\n\n The list above is measured, not transcribed: every entry was added to\n a minimal rule of that check and the run watched for `E201`. Three\n corrections fall out of it, all against the published docs:\n `capitalization` takes `prefix` (singular) and rejects both `prefixes`\n and `suffixes`, it rejects `ignorecase`, and `occurrence` rejects\n `exceptions` and `vocab`.\n\n **Field names are matched case-insensitively, but `extends`, `message`\n and `level` are not.** Measured: `Tokens:` and `ignoreCase:` are read\n exactly as their lowercase spellings, while `EXTENDS:` fails with\n "Missing the required \'extends\' key". Their *values* are case-sensitive\n too, `level: WARNING` and `extends: Existence` are both rejected.\n Write everything lowercase and none of this can bite you.\n\n **A field from the wrong check is the loudest failure Vale has.**\n `tokens` on an `occurrence` check gives\n `E201 … has invalid keys: \'tokens\'`, exit 2, and, because Vale reads\n one assembled config per run. **no** Vale rule in the project reports\n anything. `verify` rejects the rule before Vale is invoked, so this\n cannot reach `check`.\n\n **Two checks are exempt, and that is not a licence.** Measured,\n `consistency` and `spelling` accept any key at all: `bananafield:\n true` on either loads without complaint and is ignored. A misspelled\n field on those two is a silent no-op instead of a loud one, so the\n schema cannot catch a typo there and neither can Vale. Read the field\n list twice when writing those two.\n\n **What `%%s` fills with depends on the extension point.** Getting this\n wrong is the one mistake in this recipe that passes every check below\n. The rule fires, the fixtures are green, and only a human reading the\n message sees that it is nonsense.\n\n| extends | `%%s` count | fills with, left to right |\n|------------------|-------------|-------------------------------------------------------|\n| `existence` | one | the matched text |\n| `substitution` | **two** | the **replacement**, then the matched text |\n| `capitalization` | one | the scope that failed (the whole heading or sentence) |\n\n Measured: a `substitution` message with a single `%%s` interpolates the\n *replacement*, not the match, so `"Use GitHub not %%s"` against the text\n `Github` renders `Use GitHub not GitHub`.\n\n For the other nine, do not guess. Write the message, run step 6, and\n read it back off the finding, no test you can write catches a wrong\n `%%s`, so your own eyes on the rendered message are the check.\n\n ```yaml\n # existence, flag these tokens wherever they appear\n extends: existence\n message: "Avoid \'%%s\', it hides the work from the reader"\n level: warning\n ignorecase: true\n tokens:\n - simply\n - just\n ```\n\n ```yaml\n # substitution, first %%s is the replacement, second is what was found\n extends: substitution\n message: "Use \'%%s\' instead of \'%%s\'"\n level: warning\n ignorecase: true\n swap:\n utilize: use\n "in order to": to\n ```\n\n ```yaml\n # capitalization, a whole heading must be in sentence case\n extends: capitalization\n message: "\'%%s\' should be in sentence case"\n level: warning\n scope: heading\n match: $sentence\n exceptions:\n - Taskless\n - API\n ```\n\n `match` takes `$sentence`, `$title`, `$lower`, or `$upper`. A literal\n string is legal but means "this whole scope must read exactly that",\n which is almost never what anyone wants. See step 1.\n\n **`$sentence` means first word capitalized, everything else lowercase\n, proper nouns included.** It is not "sentence case allowing proper\n nouns". Measured with `exceptions: [Taskless, API]` on headings:\n\n| Heading | Result |\n|-----------------------------------|------------------------------------------------|\n| `Getting started with the API` | quiet |\n| `Getting started with APIs` | quiet, an exception covers its plural |\n| `Taskless and the API` | quiet, an exception may lead the scope |\n| `Getting started with Kubernetes` | **fires**: a proper noun you did not list |\n| `getting started lowercase` | **fires**: the first word must be capitalized |\n| `Getting Started With Title Case` | **fires** |\n\n So `exceptions` is not decoration: every proper noun, product name and\n acronym the docs use has to be listed, or the rule flags correct\n headings. Collect them from the docs before writing the rule, and\n expect to add to the list.\n\n3. **Know what you are writing: `tokens` and `swap` keys are patterns,\n not literals.** They compile as **Go RE2** regular expressions.\n\n *This step is about `tokens` and `swap` only. A `capitalization`,\n `occurrence` or `metric` rule has neither, skip to step 4.*\n\n - `(?:…)`, `[…]`, `|`, `+`, `?` all work.\n - **Lookahead and lookbehind do not exist in RE2.** A rule that needs\n "X but not when followed by Y" cannot be written as a single\n `substitution`; split it or narrow with `scope`.\n - **Word boundaries are applied for you, around the whole pattern.**\n Measured: `Github` does not fire inside `GithubToken`, and the\n multi-word `click here` does not fire inside `Clicking here`.\n - **A hyphen is a boundary, so a hyphenated compound is not\n protected.** `obviously` fires inside `obviously-named`, while\n `obviously_stale` is safe because `_` is a word character. Do not\n reach for a hyphenated compound as a `pass/` near-miss; it is the\n case most likely to fire.\n - **Regex metacharacters in a real phrase are live.** "maybe?" is a\n pattern meaning "mayb" followed by an optional "e". Escape it.\n - **Overlapping alternatives resolve first-wins**, one finding per\n match. If `can login` and `login with` both match a sentence, you\n get whichever is written first, once, not both.\n - `ignorecase: true` matches any casing **and still skips text that\n already equals the replacement.** Measured with `Github: GitHub`:\n `github` and `Github` are flagged, `GitHub` is not. You do not need\n `ignorecase: false` to protect the correct spelling.\n - `raw` takes a full regex when `tokens` is too restrictive;\n `nonword` removes the implicit boundaries.\n - **A token made only of punctuation can never match without\n `nonword: true`.** The boundaries above are `\\b`, which needs a word\n character on the inside. An em dash has none, on either side.\n Measured against `This is a sentence, with an em dash.`:\n\n ```yaml\n # fires on nothing, ever, and reports no error\n extends: existence\n message: "Use a comma, not an em dash"\n tokens:\n - \', \'\n ```\n\n ```yaml\n # fires\n extends: existence\n message: "Use a comma, not an em dash"\n nonword: true\n tokens:\n - \', \'\n ```\n\n The first rule verifies, tests green if its `fail/` fixture is\n missing the dash, and reports nothing forever. Any token whose\n pattern contains no `\\w` (punctuation, an emoji, a bare symbol)\n needs `nonword: true`.\n\n - **A bare word finds senses you did not mean.** `landed on` in a rule\n <!-- vale no-hedging.no-hedging = NO -->\n about jargon also matches "the plane landed on time"; `simply` in a\n rule about hedging also matches "simply connected" in a maths doc.\n <!-- vale no-hedging.no-hedging = YES -->\n Narrow the token to the **collocation** you actually object to\n (`landed on a decision`, not `landed on`), and check that you got it\n right by writing the `pass/` fixture from the literal sense *first*:\n put the innocent sentence in `pass/` before you write the guilty one\n in `fail/`. A rule whose `pass/` bucket was written afterwards tends\n to contain only sentences the author already knew were safe.\n\n Vale also understands the markup, which decides what counts as text\n before your pattern ever runs. Measured in markdown:\n\n - **URLs and code spans are not prose.** A `Github` key fires on\n `Plain Github here` and not on `https://Github.com/x` or\n `` `Github/docs` ``.\n - **Link text *is* prose.** In `[click here](https://example.com)`,\n `click here` is matched. The URL is not. With no `scope`, a rule\n fires on both link text and ordinary prose; `scope: link` narrows it\n to link text alone. Measured: without a scope the token hit both the\n link and the sentence; with `scope: link`, only the link.\n\n4. **Scope the rule** by writing `.taskless/rules/vale/<id>/.vale.ini`.\n This is the step that is easy to skip and impossible to notice\n skipping. A rule with no config is enabled nowhere: it parses, it\n runs, and it reports nothing.\n\n The whole file, for a rule that applies to markdown:\n\n ```ini\n # Which files this rule applies to.\n [*.md]\n tskl) rule = no-simply\n BasedOnStyles =\n no-simply.no-simply = YES\n ```\n\n Four lines, and each one earns its place:\n\n - `[*.md]` is a **matcher**: a glob over paths, deciding which files\n this rule sees. Match it to the files the rule is actually about,\n such as `[*.{md,markdown}]` or `[docs/**/*.md]`. A rule can declare\n several matchers if it needs to. Before you widen a glob, check the\n reach table below, what Vale does to a file it cannot parse is not\n "nothing".\n - `tskl) rule = <id>` is a breadcrumb Taskless reads to attribute the\n matcher back to this rule after assembly interleaves every rule\'s\n matchers into one file. Vale parses the key and ignores it. Write it\n in every matcher you add, or the tooling loses track of who owns\n what.\n - `BasedOnStyles =` with an empty value says explicitly that no\n bundled style loads. The default is already empty, so this changes\n nothing today. Write it anyway: it tells the next reader that no\n style is missing.\n - `no-simply.no-simply = YES` turns the rule on. The first half is\n the style, which is this rule\'s directory; the second is the check\n inside it, which is the file. Both are the id.\n\n **Scope a rule *out* with a second matcher, not a cleverer glob.** A\n glob says which files a rule sees; it has no way to say "these but not\n those". The exclusion is a second matcher that assigns `NO`, and\n because precedence here is positional (a later matcher wins), the\n exclusion goes **after** the inclusion:\n\n ```ini\n # Every markdown file…\n [*.md]\n tskl) rule = no-simply\n BasedOnStyles =\n no-simply.no-simply = YES\n\n # …except the changelog, which quotes release notes verbatim.\n [CHANGELOG.md]\n tskl) rule = no-simply\n no-simply.no-simply = NO\n ```\n\n Write the breadcrumb in the second matcher too, or assembly attributes\n it to nobody. Reversing the two blocks silently re-enables the rule on\n the file you meant to exempt, and nothing reports that.\n\n **Do NOT write `StylesPath` or `MinAlertLevel` here.** Those describe\n the run rather than a rule, the assembler supplies them, and a copy\n in a rule\'s config is dropped on the way in.\n\n Keep assignments underneath a matcher. An assignment above the first\n `[…]` line belongs to no matcher, and Vale ignores it after warning\n on stderr.\n\n **What a matcher\'s glob is allowed to catch.** Vale (v%(VALE_VERSION)s)\n treats a file one of four ways, decided by extension. The lists are\n rendered from the pinned Vale version, not written out here, so they\n track the shipped binary.\n\n - **markup**: the document is prose and the format\'s own non-prose\n constructs are skipped. This is the tier every `scope:` value\n assumes; `scope: heading` has nothing to find outside it:\n %(VALE_MARKUP_FORMATS)s\n - **comment text only**: the comments are linted and the code body\n <!-- vale no-hedging.no-hedging = NO -->\n is invisible, which is exactly right for "comments must not say\n \'obviously\'":\n <!-- vale no-hedging.no-hedging = YES -->\n %(VALE_COMMENT_FORMATS)s\n - **plaintext fallback**: everything else, `.yml` `.toml` `.sh`\n `.sql` and every extension not named above included. There is no\n parser, so the whole file is linted as prose: a rule matched to\n YAML flags key names and values, not just the comments. If that is\n not what the rule means, narrow the glob rather than accepting it.\n These land here despite reading like markup, so a `scope:` value\n has nothing to act on in them: %(VALE_PLAINTEXT_FORMATS)s\n - **not supported**: Vale parses these only by shelling out to an\n external program, and this build does not support any format that\n needs one:\n %(VALE_CONVERTER_FORMATS)s\n\n Do not tell the user to install the program. Taskless excludes these\n files from the run whatever is installed, so that a repository\n checks the same way on every machine; `.xml` could not work anyway,\n since an XSLT stylesheet is specific to the document.\n\n **A single unreadable file fails the whole Vale pass.** Vale exits 2\n with an `E100` runtime error and abandons the run, `--no-exit` does\n not suppress it, so every other Vale rule over every other file goes\n unreported. `[*.{md,typ}]` is not a slightly wider `[*.md]`; it is a\n matcher that takes `check` down the first time the repo grows a\n `.typ` file. Never put one of those extensions in a glob.\n\n **A single oversized file is excluded before Vale ever opens it, not\n linted slowly.** Vale\'s cost is quadratic in one file\'s size, so a\n large enough document can consume the whole run\'s time budget on its\n own and cost every other file its findings: the same failure mode as\n the unreadable-file case above, from a different cause. `check`\n preempts it: a target file over 128KB is skipped **only if some\n matcher\'s own section would actually reach it**. The scan asks the\n assembled config\'s own section patterns, the same ones you write in\n this file\'s `.vale.ini`, rather than walking every file in the\n project. A large lockfile or a generated file no rule\'s glob names is\n left alone entirely, not merely reported softly: naming a file no\n matcher was ever going to check would be a false positive, not a\n caught coverage hole. A file that IS excluded is named in a `notices`\n entry rather than a finding: unlike the unreadable-file case above,\n where Vale\'s own error proves the file was a real target, this is a\n preemptive guess from a filesystem walk, and a soft advisory fits an\n unconfirmed guess better than a hard error does. A rule\'s own\n fixtures are never this large in practice, so this should not surface\n while authoring one. It matters when a matcher\'s glob is broad, such as\n `[*.md]` or `[**/README.md]` at the project root, where a generated\n changelog or an exported note can cross it.\n\n That example changed with Vale v3.18.0, which is the point: the\n dangerous extension is whichever one the list above says needs a\n program, not the one you remember. `.mdx` was the example until that\n release parsed it natively, and `.typ` took its place.\n\n **`.mdx` is supported** as of Vale v3.18.0, which parses it natively\n rather than shelling out. `[*.{md,mdx}]` is a legitimate matcher\n again, the example this recipe used to warn about is no longer the\n broken one. Check the lists above rather than reaching for that\n memory: `.typ` moved the opposite way in the same release, so a\n matcher covering Typst is now the one that takes the run down.\n\n **In `.mdx`, a component\'s children are prose as of v3.19.0.** Vale\n reads a JSX element\'s children as the Markdown they are, so text\n inside a wrapping component (`<Steps>`, `<Tabs>`, `<Aside>`) is\n linted at its own source position. Only tags, attributes, `{...}`\n expressions, self-closing elements, and an element opened and closed\n on one standalone line are still treated as code.\n\n Two consequences for a rule you write against MDX. Coverage grew,\n so a rule can now fire in prose it never reached before, which is a\n finding count that moves without the rule changing. And the children\n carry the element\'s name as a class scope, exactly as MyST and\n Quarto directives do, so `scope: text.class.Aside` targets one\n component\'s content. That is an open family: `verify` accepts any\n `text.class.<name>` tail, because the set of component names is the\n author\'s, not Vale\'s.\n\n5. **Write the fixtures.** Two directories inside the rule, both flat.\n Vale lints the whole fixture tree, so a document nested a level\n deeper would be linted and never checked against either bucket, which\n `test` rejects by name rather than skipping. Keep both one level\n deep:\n\n ```\n .taskless/rules/vale/<id>/.tests/pass/ok.md # rule must stay quiet\n .taskless/rules/vale/<id>/.tests/fail/bad.md # rule must fire\n ```\n\n **The leading dot on `.tests/` is required.** ast-grep walks the\n rules tree and parses every `.yml` it reaches as a rule, and a plain\n `tests/` directory fails that scan for the whole project. A\n dot-directory is skipped. Do not rename it.\n\n Give the fixtures an extension your matcher\'s glob matches. A `.txt`\n fixture under a `[*.md]` matcher is never linted, so the `fail/`\n document silently passes.\n\n **The `pass/` bucket is not "correct prose".** Correct prose proves\n nothing. The rule was never going to fire on it. Fill it with the\n near-misses that would catch an over-broad pattern. What counts as a\n near-miss depends on the rule\'s shape:\n\n - **`tokens`/`swap` rules**: the noun form you are not flagging, the\n word inside a longer word, the term in a URL or a code span, the\n correct spelling itself.\n - **`scope`d rules**: the same phrase *outside* the scope. A rule\n with `scope: link` needs the phrase in ordinary prose; a rule with\n `scope: heading` needs it in body text. Without that, nothing proves\n the scope is doing anything.\n - **`capitalization` rules**: a scope that is entirely exceptions, a\n scope whose exception word comes first, and the plural of an\n exception.\n\n That is the half of the fixture set that has to work for you.\n\n **When the rule\'s subject normally appears in code, the `fail/`\n fixture must carry it three ways**, inline in a code span, inside a\n fenced block, and in ordinary prose, in that one document. A rule\n about a command, a flag, a package name or an env var has a subject\n that lives in fenced blocks in every real README, and the default\n scope cannot see fenced blocks at all. A `fail/` fixture written only\n in prose therefore fires, goes green, and the rule then catches none\n of the real violations. Measured on one document holding the token in\n all three places: the default scope found one of three, `raw` found\n three. If the fixture fires on the prose line and not on the other\n two, the answer is `scope: raw`. See step 2 for what that costs.\n\n **Fixtures run under a config that isolates this rule, so a green\n `test` is not evidence the rule reaches any real file.** `test`\n generates its own `.vale.ini` pointing at the fixture directory and\n enabling only `<id>.<id>`; your rule\'s own matcher globs are not\n consulted. So a glob of `packages/cli/src/**/*.ts` that matches\n nothing in the repository still produces a rule that verifies, tests\n green, and reports forever. The only check for that is a real\n `check` over a real file:\n\n ```\n %(TASKLESS_CLI)s check <a real path the rule should flag> --json\n ```\n\n Do that once, on a file you have deliberately made violate the rule,\n before you believe the rule works.\n\n6. **Verify, then test.** Two commands, both taking the rule\'s\n directory as their argument, both run from the project root:\n\n ```\n %(TASKLESS_CLI)s verify .taskless/rules/vale/<id> --json\n %(TASKLESS_CLI)s test .taskless/rules/vale/<id> --json\n ```\n\n `verify` asks whether the rule is well-formed: the style file parses,\n `extends` names one of the twelve checks, `message` is present, `level`\n is one Vale accepts, every `scope` operand is one Vale honors, every\n field belongs to the check the rule extends, and the config declares a\n matcher that enables `<id>.<id>`. It does **not** need fixtures, so run\n it as soon as the style file exists.\n\n Those checks are measured against Vale v%(VALE_VERSION)s rather than\n transcribed from its docs, and they run **before** Vale is invoked.\n That ordering matters for two of them: an unknown `extends` and a\n foreign field each fail the whole Vale run rather than just this rule,\n so letting either reach the binary would take every other Vale rule\'s\n findings down with it.\n\n `test` runs the rule against both buckets. It runs `verify` first and\n stops if that fails, so a malformed rule tells you what is malformed\n instead of complaining about fixtures.\n\n Both report the same shape:\n\n ```json\n {"ok":true,"rules":[{"engine":"vale","ruleId":"no-simply",\n "ok":true,"errors":[],"ran":true}]}\n ```\n\n `ok` is the answer. `errors` names what failed, one string per\n problem. Exit code is 0 when every rule passed and 1 otherwise, so\n both are safe to script.\n\n Pass a directory above a rule and every rule beneath it is checked,\n reported one entry per rule. `.taskless/rules/vale` covers every Vale\n rule; no argument at all covers the project.\n\n If you would rather see the raw findings, the message text and the\n line numbers, run `check` against a bucket instead:\n\n ```\n %(TASKLESS_CLI)s check .taskless/rules/vale/<id>/.tests/fail --json\n ```\n\n Read `results` there. Ignore `success` and the exit code: `success`\n says the run worked rather than that the fixture behaved, and the\n exit code follows severity, so a `level: error` rule exits 1 on\n `fail/` while a `warning` rule exits 0 and both are correct. `test`\n answers pass-or-fail; `check` shows you the finding.\n\n When a `fail/` document does not fire, work down this list before\n touching the pattern. The cause is usually further up:\n - Does the rule have a `.vale.ini` at all?\n - Is the assignment underneath a `[…]` matcher?\n - Is it spelled `<id>.<id>`, both halves the same?\n - Does the matcher\'s glob match the fixture\'s extension?\n - Only then: does the pattern actually match the text?\n\n A `pass/` document that fires means the pattern is too broad. Look\n for a missing word boundary, an unescaped metacharacter, or a swap\n key that also matches the form you meant to allow.\n\n7. **Report.** Show the rule directory you created and what is in it, a\n one-line summary of what the rule flags, and the glob it is scoped\n to. The scope is a decision the user should see rather than one\n buried in a config. Note that a whole-project `%(TASKLESS_CLI)s check` skips\n your fixtures: `.taskless/` is excluded from the project walk, by\n design. `test` is what exercises them.\n\n## Worked rules\n\nNine rules that work, each paired with the near-miss that fails. Every\none was run against the bundled Vale; the "what goes wrong" lines are\nobserved behavior, not warnings in principle. Find the entry closest to\nyour intent and start there.\n\n### 1. Ban a word or phrase, `existence`\n\n> "Our docs shouldn\'t hedge."\n\n```yaml\nextends: existence\nmessage: "Avoid hedging: \'%%s\'"\nlevel: warning\nignorecase: true\ntokens:\n - we think\n - it seems\n - sort of\n```\n\n**Goes wrong:** dropping `ignorecase: true` when you meant any casing.\n`We think` at the start of a sentence then sails through. And a phrase\nwith punctuation is a *pattern*: `maybe?` means "mayb" plus an optional\n"e", so it matches `mayb`. Escape it: `maybe\\?`.\n\n### 2. Prefer one term over another, `substitution`\n\n> "Say \'sign in\', not \'login\', when it\'s a verb."\n\n```yaml\nextends: substitution\nmessage: "Use \'%%s\' instead of \'%%s\'"\nlevel: warning\nignorecase: true\nswap:\n \'login (?:to|into)\': sign in to\n \'to login\': to sign in\n```\n\n**Goes wrong:** one `%%s` instead of two. Measured, `"Use sign in not\n%%s"` against `login to` renders **"Use sign in not sign in to"**, the\nreplacement, twice. The rule fires, both fixtures pass, and only a human\nreading the message sees it. Two `%%s`, always, in that order.\n\n### 3. Enforce a product\'s spelling, `substitution`, not `capitalization`\n\n> "It\'s \'GitHub\', never \'Github\' or \'github\'."\n\n```yaml\nextends: substitution\nmessage: "Use \'%%s\' instead of \'%%s\'"\nlevel: error\nignorecase: true\nswap:\n github: GitHub\n```\n\n**Goes wrong:** reaching for `capitalization` because the complaint is\nabout capitals. Measured, `match: GitHub` flags whole sentences:\n`\'We host on Github and it is fine. should be GitHub\'`, because that\ncheck tests a *scope*, not a word. Note also that `ignorecase: true` is\nsafe here: Vale skips text already equal to the replacement, so the\ncorrect `GitHub` is not flagged.\n\n### 4. Sentence-case headings, `capitalization`\n\n> "Headings are sentence case; our product names keep their capitals."\n\n```yaml\nextends: capitalization\nmessage: "\'%%s\' should be in sentence case"\nlevel: warning\nscope: heading\nmatch: $sentence\nexceptions:\n - Taskless\n - API\n - Kubernetes\n```\n\n**Goes wrong:** a short `exceptions` list. `$sentence` lowercases\neverything after the first word, proper nouns included, so every product\nname and acronym in the docs must be listed or correct headings get\nflagged. Collect them from the docs first; expect to add more.\n\n### 5. Restrict a rule to link text, any check, plus `scope`\n\n> "\'click here\' is useless link text."\n\n```yaml\nextends: existence\nmessage: "Link text \'%%s\' says nothing, name the destination"\nlevel: warning\nscope: link\nignorecase: true\ntokens:\n - click here\n - read more\n```\n\n**Goes wrong:** omitting `scope: link`. Measured, the token then fires\non `[click here](…)` **and** on "click here to focus the search box" in\nordinary prose, which is a false positive on a sentence that is fine.\nWhenever a rule is about a *place* in the document, the `pass/` fixture\nmust contain the same phrase outside that place, otherwise nothing\nproves the scope works.\n\n### 6. Cap how often something appears, `occurrence`\n\n> "At most one exclamation mark per paragraph."\n\n```yaml\nextends: occurrence\nmessage: "Too many exclamation marks"\nlevel: warning\nscope: paragraph\ntoken: "!"\nmax: 1\n```\n\n**Goes wrong:** forgetting `scope`. The count is per scope, so with no\nscope you are capping the whole document rather than the paragraph.\nNote `token` here is singular, this check takes one, not a `tokens` list.\n\n### 7. Catch a doubled word, `repetition`\n\n> "\'the the\' keeps slipping through review."\n\n```yaml\nextends: repetition\nmessage: "\'%%s\' is repeated"\nlevel: warning\nalpha: true\ntokens:\n - \'[^\\s]+\'\n```\n\n**Goes wrong:** leaving the pattern unquoted. Measured, an unquoted\n`[^\\s]+` in YAML silently matches nothing, zero findings, no error, no\ndiagnostic. Quote any pattern containing a backslash. This is the\nfailure mode this recipe warns about most, arriving through YAML rather\nthan through Vale.\n\n### 8. One spelling or the other, consistently, `consistency`\n\n> "Pick -ize or -ise and stick to it."\n\n```yaml\nextends: consistency\nmessage: "Use \'%%s\' consistently"\nlevel: warning\nnonword: true\neither:\n organize: organise\n```\n\n**The id must be word characters only.** `consistency` is the one\nextension point that compiles the rule\'s own name into the pattern, as\na `(?P<id>…)` capture group, and Go RE2 rejects a group name containing\na hyphen. Measured: an id of `ize-ise` fails with `E201 … invalid group\nname` and takes **every** Vale rule in the project down with it, because\nVale reads one config for the whole run. Name this one `izeise` or\n`spelling_variants`. Kebab-case is right everywhere else.\n\n**Goes wrong:** expecting it to pick a winner. It flags the *second*\nform once both appear in a document, it enforces internal consistency,\nnot house style. If you want one specific spelling, that is a\n`substitution`.\n\n### 9. Require a definition, `conditional`\n\n> "An acronym must be spelled out before it\'s used."\n\n```yaml\nextends: conditional\nmessage: "\'%%s\' has no definition"\nlevel: warning\nscope: text\nignorecase: false\nfirst: \'\\b([A-Z]{3,5})\\b\'\nsecond: \'(?:\\b[A-Z][a-z]+ )+\\(([A-Z]{3,5})\\)\'\n```\n\n`first` is what must be justified; `second` is what justifies it.\nMeasured: `Application Programming Interface (API)` licenses every later\n`API`, while an undefined `XYZ` is flagged.\n\n**Goes wrong:** swapping the two, which inverts the rule into "flag the\ndefinition when the acronym is missing".\n\n## Important Notes\n\n- Vale reads one document at a time and has no cross-document view. A\n rule about consistency *between* documents cannot be written here.\n- Prose inside code is still prose: comments and docstrings are Vale\'s\n subject, and a rule about them belongs under a matcher whose glob\n covers the source files.\n- Do NOT add a `[*]` matcher to widen a rule that isn\'t firing. Matchers\n from every rule are assembled into one config, so `[*]` applies this\n rule to every file the walk reaches and turns one rule\'s scoping bug\n into a flood of false positives.\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent route`: re-decide the destination\n- `%(TASKLESS_CLI)s agent check`: run every engine over the repo\n- `%(TASKLESS_CLI)s agent create-sg-rule`: author a rule over code structure\n', ne = `# Topic: delete-rule (CLI v%(CLI_VERSION)s / topic v4)
882
886
 
883
887
  ## Goal
884
888
  Remove a rule and its associated test files from \`.taskless/\`. Does not
@@ -943,7 +947,7 @@ emitted from this command.
943
947
 
944
948
  - \`%(TASKLESS_CLI)s agent route\`: make a new rule
945
949
  - \`%(TASKLESS_CLI)s agent check\`: run remaining rules to confirm nothing broke
946
- `, J = `# Topic: detect (CLI v%(CLI_VERSION)s / topic v1)
950
+ `, te = `# Topic: detect (CLI v%(CLI_VERSION)s / topic v1)
947
951
 
948
952
  ## Goal
949
953
  Scan the working directory for the linters it configures, the
@@ -1006,7 +1010,149 @@ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
1006
1010
 
1007
1011
  - \`%(TASKLESS_CLI)s agent route\`: decide where to author a rule from these signals
1008
1012
  - \`%(TASKLESS_CLI)s agent check\`: run rules against the codebase
1009
- `, Q = `# Topic: improve-rule (anonymous) (CLI v%(CLI_VERSION)s / topic v2)
1013
+ `, se = `# Topic: feedback-invite (CLI v%(CLI_VERSION)s / topic v1)
1014
+
1015
+ ## Before you finish: one question for the user
1016
+
1017
+ Taskless would like to hear how this went. When the task above is done,
1018
+ and only then, put this to the user exactly once, in these words:
1019
+
1020
+ > Taskless would like to know how the CLI is doing. Would you be okay
1021
+ > sharing a few sentences about your experience? Or just skip it with
1022
+ > \`skip\`.
1023
+
1024
+ Then act on the reply:
1025
+
1026
+ - **They gave feedback.** Fetch \`%(TASKLESS_CLI)s agent feedback\` and follow it.
1027
+ It takes their words as they are and asks you, not them, for the rest.
1028
+ - **They said \`skip\`, said nothing, or replied about something else.**
1029
+ That is a decline. Run:
1030
+ \`\`\`
1031
+ %(TASKLESS_CLI)s feedback dismiss
1032
+ \`\`\`
1033
+ and carry on with whatever they asked for. An unrelated reply is not
1034
+ feedback, and it is not a reason to ask again.
1035
+
1036
+ Ask once. Do not repeat the question later in this session, do not ask
1037
+ before the task is finished, and do not run \`feedback send\` without
1038
+ having asked.
1039
+ `, ae = `# Topic: feedback (CLI v%(CLI_VERSION)s / topic v1)
1040
+
1041
+ ## You are here
1042
+ This is \`feedback\`. It helps you turn what a user just said about
1043
+ Taskless into a survey response the CLI can send, and send it.
1044
+ You reach it from the invite at the end of an authoring or onboarding
1045
+ recipe, after the user replied with something other than \`skip\`.
1046
+ If that is not why you are reading this, re-run \`%(TASKLESS_CLI)s agent\` and
1047
+ find the topic you meant.
1048
+
1049
+ ## Goal
1050
+ Produce one JSON payload that answers the survey, write it to
1051
+ \`.taskless/.tmp-feedback.json\`, send it with \`feedback send\`, and delete
1052
+ the file. The whole thing is one short exchange with the user and a few
1053
+ sentences from you; it is not an interview.
1054
+
1055
+ ## Preconditions
1056
+ - The user replied to the invite with feedback rather than \`skip\`,
1057
+ silence, or something unrelated. If they did any of those, this is the
1058
+ wrong recipe: run \`%(TASKLESS_CLI)s feedback dismiss\` and continue with what
1059
+ they asked for.
1060
+ - The agent can write a file and run a shell command.
1061
+ - No auth required.
1062
+
1063
+ ## You are the respondent
1064
+
1065
+ The survey is addressed to you, the agent, not to the user. One answer is
1066
+ the user's words and you record them verbatim. The other four are your
1067
+ own account of the session you just ran: what they were trying to do,
1068
+ whether they got it, what went well, and what did not. You already know
1069
+ all of that. Do not put the survey's questions to the user one by one.
1070
+
1071
+ ## Steps
1072
+
1073
+ 1. **Take the user's reply as it is.** Whatever they wrote after the
1074
+ invite is \`verbatim\`. Do not paraphrase, shorten, or tidy it. If they
1075
+ wrote several messages, join them in order with a blank line between.
1076
+
1077
+ 2. **Ask one follow-up at most, and only if needed.** If the reply is
1078
+ feedback but leaves you unable to fill \`completed\`, ask whether they
1079
+ got what they came for. Otherwise ask nothing further; the invite
1080
+ already asked for their time once.
1081
+
1082
+ 3. **Fill the rest from the session.**
1083
+ - \`goal\`: what the user was trying to accomplish, in one or two of
1084
+ your own sentences. Name the recipe you were following and the
1085
+ rule or task it was for.
1086
+ - \`completed\`: \`Yes\`, \`No\`, or \`Unknown\`. Success is binary here. A
1087
+ rule that verifies and the user accepted is \`Yes\`; a rule the user
1088
+ abandoned or that never verified is \`No\`; if the session ended
1089
+ before you could tell, \`Unknown\`. There is no partial.
1090
+ - \`workedWell\`: the steps of the interaction with Taskless that went
1091
+ smoothly. Omit the key if nothing stands out.
1092
+ - \`needsImprovement\`: the steps that cost time, needed a retry, or
1093
+ that you had to work around. Be specific: name the command, the
1094
+ field, or the message. Omit the key if nothing stands out.
1095
+
1096
+ Keep your own answers to a few sentences each. The people reading
1097
+ them want the shape of the friction, not a transcript.
1098
+
1099
+ 4. **Write the payload** to \`.taskless/.tmp-feedback.json\`, matching the
1100
+ input schema below. Use the human keys exactly as given; the CLI maps
1101
+ them to the survey's own question identifiers, and a payload carrying
1102
+ a \`$survey_\` key is not what it expects.
1103
+
1104
+ 5. **Send.** Run:
1105
+ \`\`\`
1106
+ %(TASKLESS_CLI)s feedback send --from .taskless/.tmp-feedback.json --json
1107
+ \`\`\`
1108
+ Under \`--json\`, a failure is \`{ ok: false, code, message }\`; see the
1109
+ table below. On success the command prints a thank-you.
1110
+
1111
+ 6. **Clean up.** Delete \`.taskless/.tmp-feedback.json\` whether the call
1112
+ succeeded or failed. \`.taskless/.gitignore\` already ignores it, so a
1113
+ forgotten file is a stray rather than a commit, but leave nothing
1114
+ behind.
1115
+
1116
+ 7. **Return to the user's task.** Thank them in one line and carry on.
1117
+ Do not ask for more, and do not run this recipe a second time in the
1118
+ same session.
1119
+
1120
+ ## Input schema
1121
+
1122
+ The \`--from\` JSON file conforms to:
1123
+
1124
+ \`\`\`json
1125
+ %(INPUT_SCHEMA)s
1126
+ \`\`\`
1127
+
1128
+ \`verbatim\`, \`goal\`, and \`completed\` are required. \`workedWell\` and
1129
+ \`needsImprovement\` are optional, and an optional answer you have nothing
1130
+ for is an omitted key rather than an empty string.
1131
+
1132
+ ## Important Notes
1133
+
1134
+ - Do NOT edit the user's words. \`verbatim\` is the one answer that is
1135
+ theirs, and its value to the people reading it is that it is theirs.
1136
+ - Do NOT invent a follow-up interview. The invite asked once; step 2 is
1137
+ the only question this recipe allows, and only when \`completed\` would
1138
+ otherwise be a guess.
1139
+ - If telemetry is disabled in this environment the command says so and
1140
+ exits 0 with nothing sent. That is the expected outcome there, not an
1141
+ error to retry.
1142
+
1143
+ ## Errors
1144
+
1145
+ With \`--json\`, failures emit \`{ ok: false, code, message }\`:
1146
+
1147
+ | code | meaning | fix |
1148
+ |-----------------|---------------------------------------------|-------------------------------------------------------|
1149
+ | \`INVALID_INPUT\` | \`--from\` missing, unreadable, or failed validation | the message names the field; fix the payload and retry |
1150
+
1151
+ ## See Also
1152
+
1153
+ - \`%(TASKLESS_CLI)s feedback dismiss\`: what to run when the user declined
1154
+ - \`%(TASKLESS_CLI)s agent\`: the topic index, if you arrived here by mistake
1155
+ `, oe = `# Topic: improve-rule (anonymous) (CLI v%(CLI_VERSION)s / topic v2)
1010
1156
 
1011
1157
  ## Goal
1012
1158
  Iterate on an existing ast-grep rule **locally** without contacting
@@ -1095,7 +1241,7 @@ The verify primitive returns structured errors per layer:
1095
1241
  - \`%(TASKLESS_CLI)s agent improve-rule\`: API-backed flow (auth required)
1096
1242
  - \`%(TASKLESS_CLI)s agent create-sg-rule\`: make a new rule locally
1097
1243
  - \`%(TASKLESS_CLI)s agent check\`: validate the updated rule
1098
- `, Z = `# Topic: improve-rule (CLI v%(CLI_VERSION)s / topic v5)
1244
+ `, ie = `# Topic: improve-rule (CLI v%(CLI_VERSION)s / topic v5)
1099
1245
 
1100
1246
  ## Goal
1101
1247
  Iterate on an existing Taskless rule. The CLI submits the user's
@@ -1239,7 +1385,7 @@ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
1239
1385
  - \`%(TASKLESS_CLI)s agent improve-rule --anonymous\`: local-only flow
1240
1386
  - \`%(TASKLESS_CLI)s agent route\`: make a new rule from scratch
1241
1387
  - \`%(TASKLESS_CLI)s agent check\`: validate the updated rule
1242
- `, ee = `# Topic: info (CLI v%(CLI_VERSION)s / topic v1)
1388
+ `, re = `# Topic: info (CLI v%(CLI_VERSION)s / topic v1)
1243
1389
 
1244
1390
  ## Goal
1245
1391
  Report local Taskless state: CLI version, installed skill versions
@@ -1306,61 +1452,119 @@ failing.)
1306
1452
 
1307
1453
  - \`%(TASKLESS_CLI)s agent auth\`: log in / log out / status detail
1308
1454
  - \`%(TASKLESS_CLI)s agent check\`: run rules against the codebase
1309
- `, ne = `# Topic: init (CLI v%(CLI_VERSION)s / topic v1)
1455
+ `, le = `# Topic: init (CLI v%(CLI_VERSION)s / topic v2)
1310
1456
 
1311
1457
  ## Goal
1312
- Install or update the Taskless skill into the user's coding-agent
1313
- tools (Claude Code, OpenCode, Cursor, etc.). The user runs this
1314
- themselves, the agent's role is mostly to point the user at the
1315
- right command when they need to install or upgrade.
1458
+ Install or update the Taskless skill in this project, and migrate the
1459
+ \`.taskless/\` layout when the project is behind the CLI. You most often
1460
+ arrive here because \`check\`, \`verify\`, or \`test\` refused with
1461
+ \`SCAFFOLD_MIGRATION_REQUIRED\`: those commands only read, so the rewrite
1462
+ is left to \`init\`, which is the one command that migrates.
1463
+
1464
+ An install rewrites files under version control and can change what an
1465
+ upgrade means for the rules already in the project. Running the command
1466
+ is the first of three steps, not the whole job.
1316
1467
 
1317
1468
  ## Preconditions
1318
- - None at the user level. The command works in any directory and
1469
+ - None at the project level. The command works in any directory and
1319
1470
  bootstraps \`.taskless/\` on first run.
1320
- - For interactive mode: a TTY (running from a terminal).
1471
+ - No auth. \`init\` never calls the Taskless API.
1472
+ - No TTY needed. \`init\` is the batch path in every context; the wizard
1473
+ is only reached by running the CLI with no subcommand in a terminal.
1321
1474
 
1322
1475
  ## Steps
1323
1476
 
1324
- The user should run:
1325
- \`\`\`
1326
- %(TASKLESS_CLI)s
1327
- \`\`\`
1328
- (no subcommand). In a TTY this launches the interactive wizard. In
1329
- non-TTY contexts it prints the topic index instead.
1330
-
1331
- For scripted installs (CI, Dockerfiles):
1332
- \`\`\`
1333
- %(TASKLESS_CLI)s init --no-interactive
1334
- \`\`\`
1335
-
1336
- The wizard will:
1337
- 1. Detect installed tools (Claude Code, OpenCode, Cursor) and ask
1338
- which tools to enable Taskless for.
1339
- 2. Show the auth tradeoff and offer to log in (skippable).
1340
- 3. Show a diff against the previous install state before writing.
1341
- 4. Write the canonical \`taskless\` skill (and \`tskl\` command) once to
1342
- \`.taskless/\`, then a thin reference stub into each selected tool
1343
- directory (\`.claude/\`, \`.cursor/\`, \`.opencode/\`, \`.agents/\`).
1344
- 5. Update \`.taskless/taskless.json\` with the install manifest.
1345
-
1346
- The skill content lives in exactly one place, \`.taskless/skills/\`,
1347
- and each tool directory holds only a short stub that points at it.
1348
- Stale layouts from older installs (full per-tool copies, symlinks)
1349
- are converged into stubs automatically. If the user is on v0.6 or
1350
- earlier, the obsolete per-task skills (taskless-check, etc.) and old
1351
- slash commands are removed as part of the install; the summary shows
1352
- what was removed.
1477
+ 1. **Run the install.**
1478
+ \`\`\`
1479
+ %(TASKLESS_CLI)s init --json
1480
+ \`\`\`
1481
+ The envelope:
1482
+ \`\`\`json
1483
+ {
1484
+ "success": true,
1485
+ "commandsInstalled": true,
1486
+ "cliVersion": { "previous": "0.10.2", "installed": "0.11.1" },
1487
+ "targets": [
1488
+ { "dir": ".taskless", "mode": "canonical",
1489
+ "writtenSkills": ["taskless"], "writtenCommands": [],
1490
+ "removedSkills": [], "removedCommands": [] },
1491
+ { "dir": ".claude", "mode": "reference",
1492
+ "writtenSkills": ["taskless"], "writtenCommands": ["tskl.md"],
1493
+ "removedSkills": [], "removedCommands": [] }
1494
+ ],
1495
+ "changed": true,
1496
+ "migrated": { "from": 3, "to": 4, "applied": [4],
1497
+ "files": { "added": [], "modified": [], "removed": [] } }
1498
+ }
1499
+ \`\`\`
1500
+ - \`cliVersion.previous\` is \`null\` on a project with no recorded
1501
+ install. When it differs from \`installed\`, the CLI was upgraded.
1502
+ - \`targets\` lists every install location and what this run wrote or
1503
+ removed there, by name. \`mode: "canonical"\` is the \`.taskless/\`
1504
+ store; \`reference\` is a tool directory holding stubs.
1505
+ - \`changed\` is \`true\` when a migration ran, any target list is
1506
+ non-empty, or \`cliVersion\` moved (that rewrites
1507
+ \`.taskless/taskless.json\`). When it is \`false\`, stop here: nothing
1508
+ to commit, nothing to reconcile.
1509
+ - \`migrated\` is present only when a migration ran, with the paths it
1510
+ added, rewrote, or deleted.
1511
+
1512
+ Without \`--json\`, the same facts print as prose: a per-target summary,
1513
+ then a trailer naming the directories that changed and, after a
1514
+ version move, pointing at \`update\`.
1515
+
1516
+ 2. **Tell the user what needs committing.** Every \`targets[].dir\` with
1517
+ a non-empty list, plus \`.taskless/\` and any \`migrated.files\` entries,
1518
+ now holds changes that belong in version control. Name those paths and
1519
+ say what Taskless rewrote and why (a CLI upgrade, a layout migration),
1520
+ so the user can include them in the commit they choose. Do not stage
1521
+ or commit on your own; the git operations are theirs.
1522
+
1523
+ 3. **After a version move, reconcile the rules.** When
1524
+ \`cliVersion.previous\` is non-null and differs from \`installed\`, run
1525
+ \`\`\`
1526
+ %(TASKLESS_CLI)s update
1527
+ \`\`\`
1528
+ and follow it. The migration moved the DIRECTORY; the rules in it may
1529
+ still need work an upgrade cannot do for them (a rewriter that now
1530
+ needs a \`fix\`, a rule whose matching semantics shifted under a new
1531
+ engine). \`update\` is how to find out, and the only way to record that
1532
+ the walk was done.
1533
+
1534
+ 4. **Treat your own session as stale.** A tool loads its skill list once,
1535
+ at startup. If Taskless was installed or upgraded during this session,
1536
+ the skill text in your context is the previous version. Tell the user
1537
+ the skills changed and that a new session, or a skill reload, picks
1538
+ them up. Recipes are unaffected: every \`agent <topic>\` fetch reads the
1539
+ installed CLI.
1540
+
1541
+ 5. **Return to what sent you here.** Re-run the command that refused.
1542
+
1543
+ ## For a person at a terminal
1544
+
1545
+ \`%(TASKLESS_CLI)s\` with no subcommand launches an interactive wizard. It
1546
+ detects the installed tools (Claude Code, OpenCode, Cursor, Codex), asks
1547
+ which to enable, offers a login (skippable), shows a diff against the
1548
+ previous install, then writes the canonical \`taskless\` skill (and \`tskl\`
1549
+ command) once to \`.taskless/\` and a thin stub into each selected tool
1550
+ directory. Stale layouts from older installs converge to stubs
1551
+ automatically; v0.6-era per-task skills and commands are removed and the
1552
+ summary shows what went.
1353
1553
 
1354
1554
  ## Errors
1355
1555
 
1556
+ - \`SCAFFOLD_VERSION_MISMATCH\`: the project's \`.taskless/\` is NEWER than
1557
+ this CLI. Upgrade the CLI; do not migrate downward.
1558
+ - No tools detected → the skill is written to \`.agents/skills/\` and no
1559
+ slash command is installed. Not an error.
1356
1560
  - Wizard cancelled (Ctrl-C) → no filesystem writes. Re-run when ready.
1357
- - No tools detected → falls back to \`.agents/skills/\`.
1358
1561
 
1359
1562
  ## See Also
1360
1563
 
1564
+ - \`%(TASKLESS_CLI)s update\`: what an upgrade changed for existing rules
1361
1565
  - \`%(TASKLESS_CLI)s agent info\`: verify what's installed and check staleness
1362
1566
  - \`%(TASKLESS_CLI)s agent auth\`: authenticate after installing
1363
- `, te = `# Topic: onboard (CLI v%(CLI_VERSION)s / topic v3)
1567
+ `, he = `# Topic: onboard (CLI v%(CLI_VERSION)s / topic v3)
1364
1568
 
1365
1569
  ## Goal
1366
1570
  Help a user who has just installed Taskless go from zero rules to a
@@ -1519,7 +1723,7 @@ rules as a bullet list the user can choose to materialize via the
1519
1723
  and authors, which bounds what a candidate can be
1520
1724
  - \`%(TASKLESS_CLI)s agent check\`: validate newly created rules against the codebase
1521
1725
  - \`%(TASKLESS_CLI)s agent info\`: inspect the current \`.taskless/taskless.json\` state
1522
- `, se = `# Topic: route (CLI v%(CLI_VERSION)s / topic v5)
1726
+ `, ce = `# Topic: route (CLI v%(CLI_VERSION)s / topic v5)
1523
1727
 
1524
1728
  ## Goal
1525
1729
  Turn "write me a rule that…" into one command to run. This is the front
@@ -1831,7 +2035,7 @@ will work.
1831
2035
  - \`%(TASKLESS_CLI)s agent create-runtime-rule\`: the runtime tier, logged out
1832
2036
  - \`%(TASKLESS_CLI)s agent create-remote-rule\`: generate via the service (login)
1833
2037
  - \`%(TASKLESS_CLI)s agent check\`: run every engine over the repo
1834
- `, ae = `# Topic: rule-meta (CLI v%(CLI_VERSION)s / topic v3)
2038
+ `, de = `# Topic: rule-meta (CLI v%(CLI_VERSION)s / topic v3)
1835
2039
 
1836
2040
  ## Goal
1837
2041
  Report what \`%(TASKLESS_CLI)s rule meta\` does today, so no recipe and no
@@ -1870,7 +2074,7 @@ that created the rule, not from disk:
1870
2074
  ## See Also
1871
2075
 
1872
2076
  - \`%(TASKLESS_CLI)s agent improve-rule\`: how the ticket id is actually sourced
1873
- `, re = `# Topic: rule (CLI v%(CLI_VERSION)s / topic v2)
2077
+ `, ue = `# Topic: rule (CLI v%(CLI_VERSION)s / topic v2)
1874
2078
 
1875
2079
  ## Goal
1876
2080
  Umbrella for rule operations. Fetch the topic for the action you want.
@@ -1898,7 +2102,7 @@ For the local-only flow on improve, append \`--anonymous\`.
1898
2102
  ## See Also
1899
2103
 
1900
2104
  - \`%(TASKLESS_CLI)s agent check\`: run all configured rules
1901
- `, ie = `# Topic: update (CLI v%(CLI_VERSION)s / topic v5)
2105
+ `, pe = `# Topic: update (CLI v%(CLI_VERSION)s / topic v6)
1902
2106
 
1903
2107
  ## You are here
1904
2108
  This is \`update\`. It tells you what an upgrade changed for the rules
@@ -2127,6 +2331,72 @@ leaked text into people's files.
2127
2331
  (ast-grep/ast-grep#2868), but no shape we tried reproduced a difference,
2128
2332
  including the TSX case that PR names. Nothing to do unless you see one.
2129
2333
 
2334
+ ### Migrating to 0.11.2
2335
+
2336
+ Vale moves from 3.20.0 to 3.21.0, and ast-grep from 0.45.2 to 0.45.3.
2337
+ Nothing installed migrates for ast-grep: its one user-visible change is
2338
+ that an inline \`ast-grep-ignore\` comment takes effect only as the
2339
+ comment's first alphabetic text, which the 0.45.3 changeset describes
2340
+ and no rule file carries. Six things follow for existing Vale rules,
2341
+ every one measured against both binaries; the last is behaviour a rule
2342
+ can now use, not a change to one it has.
2343
+
2344
+ **A duplicated matcher now keeps its LAST assignment.** Where a rule's
2345
+ \`.vale.ini\` repeats a \`[glob]\` section, or repeats a key inside one,
2346
+ 3.20.0 kept the first value and 3.21.0 keeps the last (upstream 1e4f6ed,
2347
+ "let the project's rule settings win"). Precedence across different
2348
+ matchers was already last-wins, so the two directions now agree, and the
2349
+ guidance to put a disable AFTER the enable it narrows is right for both
2350
+ shapes. A rule that relied on the old order is one whose second
2351
+ assignment was being ignored; it now takes effect, and the rule turns on
2352
+ or off where it did not before. \`git grep -c '^\\[' .taskless/rules/vale\`
2353
+ finds a config with more than one section per rule to read.
2354
+
2355
+ **A \`metric\` rule's \`scope\` is honored.** Through 3.20.0 a \`metric\`
2356
+ measured the whole document whatever its \`scope\` said; 3.21.0 measures
2357
+ the blocks the scope names, and only an absent scope, or \`scope: text\`,
2358
+ still means the document. A \`metric\` with \`scope: sentence\` or
2359
+ \`scope: paragraph\` now reports per block, so its findings move, appear,
2360
+ or disappear. If the document-wide reading was what you wanted, delete
2361
+ the \`scope\`.
2362
+
2363
+ **Notebooks are read cell by cell.** \`.ipynb\` had no parser, so a rule
2364
+ matching \`[*.ipynb]\` was linting the notebook's JSON: cell source,
2365
+ outputs, and metadata alike. 3.21.0 reads a Markdown cell as Markdown
2366
+ and a code cell as its kernel's comments, and reads raw cells and
2367
+ outputs not at all. Findings drop, and \`Line\` now points into the
2368
+ notebook file. Nothing warns; a suspiciously clean run over notebooks is
2369
+ this.
2370
+
2371
+ **An unknown \`action\` name fails the run at load.** Measured on
2372
+ 3.20.0, a rule carrying \`action: {name: bogus}\` loaded, and the run
2373
+ died only when that rule fired, as an \`E100\` with no file and no line.
2374
+ 3.21.0 refuses it when the rule loads, as an \`E201\` naming the file, so
2375
+ a rule that has been carrying a bad action without ever matching now
2376
+ fails every check, not just the ones where it matched. \`verify\` rejects
2377
+ a name outside \`replace\`, \`remove\`, \`suggest\`, \`convert\`, \`edit\`, so run
2378
+ it: the rule is named directly.
2379
+
2380
+ **\`BlockIgnores\` and \`TokenIgnores\` apply to HTML.** A rule's
2381
+ \`.vale.ini\` carrying either key under an \`[*.html]\` matcher was ignored
2382
+ through 3.20.0 and takes effect on 3.21.0. Findings inside the ignored
2383
+ region disappear.
2384
+
2385
+ Upstream also says a \`sequence\` rule with a negated scope (\`~list\`,
2386
+ \`~code\`) reported every match twice (vale-cli/vale#1169). No shape we
2387
+ tried reproduced a difference between the two binaries, including the
2388
+ one that PR's own test uses, so it is recorded the way the ast-grep
2389
+ root-metavariable case is: nothing to do unless you see one.
2390
+
2391
+ **\`scope: doc(<selector>)\` selects part of a document by CSS selector**,
2392
+ which a rule could not do before: \`text & doc(section:has(> h2:contains("Decision")))\`
2393
+ is the prose of one section, \`~doc(...)\` is everything outside it, and
2394
+ a \`metric\` scoped to \`doc(...)\` puts a word budget on that section
2395
+ alone. \`%(TASKLESS_CLI)s agent create-vale-rule\` has the shapes and the
2396
+ one trap (a leaf element on its own, \`doc(h2)\`, is inert; chain it).
2397
+ No existing rule changes; this is a reason to revisit one that was
2398
+ narrowed by hand.
2399
+
2130
2400
  ## Errors
2131
2401
 
2132
2402
  With \`--json\`, \`--rules\` failures emit \`{ ok: false, code, message }\`:
@@ -2140,12 +2410,12 @@ With \`--json\`, \`--rules\` failures emit \`{ ok: false, code, message }\`:
2140
2410
  - \`%(TASKLESS_CLI)s agent check\`: run every engine over the repo
2141
2411
  - \`%(TASKLESS_CLI)s agent improve-rule\`: rewrite a rule the walk flagged
2142
2412
  - \`%(TASKLESS_CLI)s agent info\`: what is installed, and staleness
2143
- `, oe = '# Topic: verify-rule (CLI v%(CLI_VERSION)s / topic v3)\n\n## Goal\nCheck that a rule is well-formed (`verify`), then run its tests\n(`test`). Both address rules **by path**, so one command works the same\nfor every engine. Primary consumer is the create/improve feedback loop,\nbut agents can call either directly.\n\n## Preconditions\n- The rule exists at `.taskless/rules/<engine>/<id>/`.\n- Tests are optional for `verify` and are what `test` runs.\n- No auth required.\n\n## The two commands\n\n```\n%(TASKLESS_CLI)s verify <path> --json\n%(TASKLESS_CLI)s test <path> --json\n```\n\n`verify` asks whether the rule has the components its engine requires.\nIt never runs the rule against anything, so it works on a rule whose\ntests do not exist yet. That is the point of splitting the two: an\nagent part-way through authoring needs to know the rule itself is valid\nbefore it can write a meaningful test for it.\n\n`test` runs the rule\'s tests. **It runs `verify` first and stops on\nfailure**, so a rule that is both malformed and under-fixtured reports\nthe malformation. The fixture complaint is the less useful of the two\nand would otherwise bury the reason the rule could never have run.\n\nWhat each engine is checked for:\n\n| engine | `verify` checks | `test` runs |\n|-----------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|\n| `sg` | ast-grep schema, plus `id`/`language`/`severity`/`message`/`rule`, and `regex` accompanied by `kind` | the `valid`/`invalid` cases in `.tests/` |\n| `vale` | style parses, `extends` and `message` present, `level` in vocabulary, and the rule\'s `.vale.ini` enables `<id>.<id>` under a matcher | the `.tests/pass/` and `.tests/fail/` buckets |\n| `runtime` | `check.ts` present, at least one capture rule in `captures/` | each directory under `.tests/pass/` and `.tests/fail/`, as the check\'s `root` |\n\nA runtime rule\'s fixtures execute its `check.ts`, so they run only under\nthe policy `check` already applies: an authenticated reconcile that\nreturns the rule\'s signature, or `--dangerously-run-scripts`. When\nneither holds, that rule prints `○` rather than a tick, names the reason\nand the flag, and is reported as `"ran": false` with a `refused` string\nsaying why. It is counted under "did not run" rather than among the\nrules tested, and on its own it does not fail the command.\n\nRead `ran` before `ok`. A runtime rule that did not run is not a rule\nthat passed, and treating the two alike is the defect this reporting\nexists to prevent.\n\n## What a path means\n\nThe path is a location under `.taskless/rules/`, and the `<engine>`\nsegment is what decides the engine. Nothing parses a rule file to work\nout who owns it, so the same id under two engines is never ambiguous.\n\n| Path | Scope |\n|----------------------------------|-----------------------------------------------------|\n| `.taskless/rules/vale/no-simply` | that one rule |\n| `.taskless/rules/vale` | every Vale rule |\n| `.taskless/rules` | every rule (also the default when you pass no path) |\n\nA path outside `.taskless/rules/` is an error naming the path. So is a\npath inside it that holds no rule.\n\n## Output\n\nBoth commands answer in the same shape, one entry per rule:\n\n```json\n{\n "ok": false,\n "rules": [\n {"engine":"sg","ruleId":"no-eval","ok":true,"errors":[]},\n {"engine":"vale","ruleId":"no-simply","ok":false,\n "errors":["no-simply/.vale.ini never enables no-simply.no-simply, so the rule is present but off."]}\n ]\n}\n```\n\n- `ok` at the top is true only when every rule passed.\n- `errors` is one string per problem, written to be read by a person.\n- `test` adds `ran`, which is false when `verify` failed and the tests\n never executed, and false for a runtime rule the execution policy\n refused. The second case also carries `refused` with the reason, and\n is the one where `ok` alone would mislead you.\n\nWithout `--json` the same information prints as a `✓`/`✗`/`○` line per\nrule with its errors indented beneath. `○` is a rule that did not run.\n\n## Exit codes\n\n- `0`: every rule in scope passed. An empty rules tree also exits 0: a\n project that has not written a rule yet is an ordinary state, and\n failing there would make `verify` unusable in CI on a fresh install.\n- `1`: any rule failed, or the path could not be resolved.\n\n## In a feedback loop\n\nRun `verify` as soon as the rule file exists, fix what it names, then\nwrite tests and run `test`. Cap at 3 attempts and report to the user if\nit still fails.\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent create-sg-rule`: author an ast-grep rule\n- `%(TASKLESS_CLI)s agent create-vale-rule`: author a Vale rule\n- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists\n';
2144
- var I = {}, _;
2145
- function le() {
2146
- return _ || (_ = 1, (function(t) {
2413
+ `, me = '# Topic: verify-rule (CLI v%(CLI_VERSION)s / topic v3)\n\n## Goal\nCheck that a rule is well-formed (`verify`), then run its tests\n(`test`). Both address rules **by path**, so one command works the same\nfor every engine. Primary consumer is the create/improve feedback loop,\nbut agents can call either directly.\n\n## Preconditions\n- The rule exists at `.taskless/rules/<engine>/<id>/`.\n- Tests are optional for `verify` and are what `test` runs.\n- No auth required.\n\n## The two commands\n\n```\n%(TASKLESS_CLI)s verify <path> --json\n%(TASKLESS_CLI)s test <path> --json\n```\n\n`verify` asks whether the rule has the components its engine requires.\nIt never runs the rule against anything, so it works on a rule whose\ntests do not exist yet. That is the point of splitting the two: an\nagent part-way through authoring needs to know the rule itself is valid\nbefore it can write a meaningful test for it.\n\n`test` runs the rule\'s tests. **It runs `verify` first and stops on\nfailure**, so a rule that is both malformed and under-fixtured reports\nthe malformation. The fixture complaint is the less useful of the two\nand would otherwise bury the reason the rule could never have run.\n\nWhat each engine is checked for:\n\n| engine | `verify` checks | `test` runs |\n|-----------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|\n| `sg` | ast-grep schema, plus `id`/`language`/`severity`/`message`/`rule`, and `regex` accompanied by `kind` | the `valid`/`invalid` cases in `.tests/` |\n| `vale` | style parses, `extends` and `message` present, `level` in vocabulary, and the rule\'s `.vale.ini` enables `<id>.<id>` under a matcher | the `.tests/pass/` and `.tests/fail/` buckets |\n| `runtime` | `check.ts` present, at least one capture rule in `captures/` | each directory under `.tests/pass/` and `.tests/fail/`, as the check\'s `root` |\n\nA runtime rule\'s fixtures execute its `check.ts`, so they run only under\nthe policy `check` already applies: an authenticated reconcile that\nreturns the rule\'s signature, or `--dangerously-run-scripts`. When\nneither holds, that rule prints `○` rather than a tick, names the reason\nand the flag, and is reported as `"ran": false` with a `refused` string\nsaying why. It is counted under "did not run" rather than among the\nrules tested, and on its own it does not fail the command.\n\nRead `ran` before `ok`. A runtime rule that did not run is not a rule\nthat passed, and treating the two alike is the defect this reporting\nexists to prevent.\n\n## What a path means\n\nThe path is a location under `.taskless/rules/`, and the `<engine>`\nsegment is what decides the engine. Nothing parses a rule file to work\nout who owns it, so the same id under two engines is never ambiguous.\n\n| Path | Scope |\n|----------------------------------|-----------------------------------------------------|\n| `.taskless/rules/vale/no-simply` | that one rule |\n| `.taskless/rules/vale` | every Vale rule |\n| `.taskless/rules` | every rule (also the default when you pass no path) |\n\nA path outside `.taskless/rules/` is an error naming the path. So is a\npath inside it that holds no rule.\n\n## Output\n\nBoth commands answer in the same shape, one entry per rule:\n\n```json\n{\n "ok": false,\n "rules": [\n {"engine":"sg","ruleId":"no-eval","ok":true,"errors":[]},\n {"engine":"vale","ruleId":"no-simply","ok":false,\n "errors":["no-simply/.vale.ini never enables no-simply.no-simply, so the rule is present but off."]}\n ]\n}\n```\n\n- `ok` at the top is true only when every rule passed.\n- `errors` is one string per problem, written to be read by a person.\n- `test` adds `ran`, which is false when `verify` failed and the tests\n never executed, and false for a runtime rule the execution policy\n refused. The second case also carries `refused` with the reason, and\n is the one where `ok` alone would mislead you.\n\nWithout `--json` the same information prints as a `✓`/`✗`/`○` line per\nrule with its errors indented beneath. `○` is a rule that did not run.\n\n## Exit codes\n\n- `0`: every rule in scope passed. An empty rules tree also exits 0: a\n project that has not written a rule yet is an ordinary state, and\n failing there would make `verify` unusable in CI on a fresh install.\n- `1`: any rule failed, or the path could not be resolved.\n\n## In a feedback loop\n\nRun `verify` as soon as the rule file exists, fix what it names, then\nwrite tests and run `test`. Cap at 3 attempts and report to the user if\nit still fails.\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent create-sg-rule`: author an ast-grep rule\n- `%(TASKLESS_CLI)s agent create-vale-rule`: author a Vale rule\n- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists\n';
2414
+ var A = {}, _;
2415
+ function ge() {
2416
+ return _ || (_ = 1, (function(e) {
2147
2417
  (function() {
2148
- var n = {
2418
+ var t = {
2149
2419
  not_type: /[^T]/,
2150
2420
  not_primitive: /[^v]/,
2151
2421
  number: /[diefg]/,
@@ -2160,168 +2430,211 @@ function le() {
2160
2430
  sign: /^[+-]/
2161
2431
  };
2162
2432
  function s(h) {
2163
- return l(b(h), arguments);
2433
+ return l(k(h), arguments);
2164
2434
  }
2165
- function r(h, u) {
2435
+ function o(h, u) {
2166
2436
  return s.apply(null, [h].concat(u || []));
2167
2437
  }
2168
2438
  function l(h, u) {
2169
- var i = 1, y = h.length, e, p = "", d, c, a, v, k, S, T, w;
2170
- for (d = 0; d < y; d++)
2439
+ var r = 1, w = h.length, n, p = "", d, c, a, b, S, T, I, y;
2440
+ for (d = 0; d < w; d++)
2171
2441
  if (typeof h[d] == "string")
2172
2442
  p += h[d];
2173
2443
  else if (typeof h[d] == "object") {
2174
2444
  if (a = h[d], a.keys)
2175
- for (e = u[i], c = 0; c < a.keys.length; c++) {
2176
- if (e == null)
2445
+ for (n = u[r], c = 0; c < a.keys.length; c++) {
2446
+ if (n == null)
2177
2447
  throw new Error(s('[sprintf] Cannot access property "%s" of undefined value "%s"', a.keys[c], a.keys[c - 1]));
2178
- e = e[a.keys[c]];
2448
+ n = n[a.keys[c]];
2179
2449
  }
2180
- else a.param_no ? e = u[a.param_no] : e = u[i++];
2181
- if (n.not_type.test(a.type) && n.not_primitive.test(a.type) && e instanceof Function && (e = e()), n.numeric_arg.test(a.type) && typeof e != "number" && isNaN(e))
2182
- throw new TypeError(s("[sprintf] expecting number but found %T", e));
2183
- switch (n.number.test(a.type) && (T = e >= 0), a.type) {
2450
+ else a.param_no ? n = u[a.param_no] : n = u[r++];
2451
+ if (t.not_type.test(a.type) && t.not_primitive.test(a.type) && n instanceof Function && (n = n()), t.numeric_arg.test(a.type) && typeof n != "number" && isNaN(n))
2452
+ throw new TypeError(s("[sprintf] expecting number but found %T", n));
2453
+ switch (t.number.test(a.type) && (I = n >= 0), a.type) {
2184
2454
  case "b":
2185
- e = parseInt(e, 10).toString(2);
2455
+ n = parseInt(n, 10).toString(2);
2186
2456
  break;
2187
2457
  case "c":
2188
- e = String.fromCharCode(parseInt(e, 10));
2458
+ n = String.fromCharCode(parseInt(n, 10));
2189
2459
  break;
2190
2460
  case "d":
2191
2461
  case "i":
2192
- e = parseInt(e, 10);
2462
+ n = parseInt(n, 10);
2193
2463
  break;
2194
2464
  case "j":
2195
- e = JSON.stringify(e, null, a.width ? parseInt(a.width) : 0);
2465
+ n = JSON.stringify(n, null, a.width ? parseInt(a.width) : 0);
2196
2466
  break;
2197
2467
  case "e":
2198
- e = a.precision ? parseFloat(e).toExponential(a.precision) : parseFloat(e).toExponential();
2468
+ n = a.precision ? parseFloat(n).toExponential(a.precision) : parseFloat(n).toExponential();
2199
2469
  break;
2200
2470
  case "f":
2201
- e = a.precision ? parseFloat(e).toFixed(a.precision) : parseFloat(e);
2471
+ n = a.precision ? parseFloat(n).toFixed(a.precision) : parseFloat(n);
2202
2472
  break;
2203
2473
  case "g":
2204
- e = a.precision ? String(Number(e.toPrecision(a.precision))) : parseFloat(e);
2474
+ n = a.precision ? String(Number(n.toPrecision(a.precision))) : parseFloat(n);
2205
2475
  break;
2206
2476
  case "o":
2207
- e = (parseInt(e, 10) >>> 0).toString(8);
2477
+ n = (parseInt(n, 10) >>> 0).toString(8);
2208
2478
  break;
2209
2479
  case "s":
2210
- e = String(e), e = a.precision ? e.substring(0, a.precision) : e;
2480
+ n = String(n), n = a.precision ? n.substring(0, a.precision) : n;
2211
2481
  break;
2212
2482
  case "t":
2213
- e = String(!!e), e = a.precision ? e.substring(0, a.precision) : e;
2483
+ n = String(!!n), n = a.precision ? n.substring(0, a.precision) : n;
2214
2484
  break;
2215
2485
  case "T":
2216
- e = Object.prototype.toString.call(e).slice(8, -1).toLowerCase(), e = a.precision ? e.substring(0, a.precision) : e;
2486
+ n = Object.prototype.toString.call(n).slice(8, -1).toLowerCase(), n = a.precision ? n.substring(0, a.precision) : n;
2217
2487
  break;
2218
2488
  case "u":
2219
- e = parseInt(e, 10) >>> 0;
2489
+ n = parseInt(n, 10) >>> 0;
2220
2490
  break;
2221
2491
  case "v":
2222
- e = e.valueOf(), e = a.precision ? e.substring(0, a.precision) : e;
2492
+ n = n.valueOf(), n = a.precision ? n.substring(0, a.precision) : n;
2223
2493
  break;
2224
2494
  case "x":
2225
- e = (parseInt(e, 10) >>> 0).toString(16);
2495
+ n = (parseInt(n, 10) >>> 0).toString(16);
2226
2496
  break;
2227
2497
  case "X":
2228
- e = (parseInt(e, 10) >>> 0).toString(16).toUpperCase();
2498
+ n = (parseInt(n, 10) >>> 0).toString(16).toUpperCase();
2229
2499
  break;
2230
2500
  }
2231
- n.json.test(a.type) ? p += e : (n.number.test(a.type) && (!T || a.sign) ? (w = T ? "+" : "-", e = e.toString().replace(n.sign, "")) : w = "", k = a.pad_char ? a.pad_char === "0" ? "0" : a.pad_char.charAt(1) : " ", S = a.width - (w + e).length, v = a.width && S > 0 ? k.repeat(S) : "", p += a.align ? w + e + v : k === "0" ? w + v + e : v + w + e);
2501
+ t.json.test(a.type) ? p += n : (t.number.test(a.type) && (!I || a.sign) ? (y = I ? "+" : "-", n = n.toString().replace(t.sign, "")) : y = "", S = a.pad_char ? a.pad_char === "0" ? "0" : a.pad_char.charAt(1) : " ", T = a.width - (y + n).length, b = a.width && T > 0 ? S.repeat(T) : "", p += a.align ? y + n + b : S === "0" ? y + b + n : b + y + n);
2232
2502
  }
2233
2503
  return p;
2234
2504
  }
2235
- var m = /* @__PURE__ */ Object.create(null);
2236
- function b(h) {
2237
- if (m[h])
2238
- return m[h];
2239
- for (var u = h, i, y = [], e = 0; u; ) {
2240
- if ((i = n.text.exec(u)) !== null)
2241
- y.push(i[0]);
2242
- else if ((i = n.modulo.exec(u)) !== null)
2243
- y.push("%");
2244
- else if ((i = n.placeholder.exec(u)) !== null) {
2245
- if (i[2]) {
2246
- e |= 1;
2247
- var p = [], d = i[2], c = [];
2248
- if ((c = n.key.exec(d)) !== null)
2505
+ var g = /* @__PURE__ */ Object.create(null);
2506
+ function k(h) {
2507
+ if (g[h])
2508
+ return g[h];
2509
+ for (var u = h, r, w = [], n = 0; u; ) {
2510
+ if ((r = t.text.exec(u)) !== null)
2511
+ w.push(r[0]);
2512
+ else if ((r = t.modulo.exec(u)) !== null)
2513
+ w.push("%");
2514
+ else if ((r = t.placeholder.exec(u)) !== null) {
2515
+ if (r[2]) {
2516
+ n |= 1;
2517
+ var p = [], d = r[2], c = [];
2518
+ if ((c = t.key.exec(d)) !== null)
2249
2519
  for (p.push(c[1]); (d = d.substring(c[0].length)) !== ""; )
2250
- if ((c = n.key_access.exec(d)) !== null)
2520
+ if ((c = t.key_access.exec(d)) !== null)
2251
2521
  p.push(c[1]);
2252
- else if ((c = n.index_access.exec(d)) !== null)
2522
+ else if ((c = t.index_access.exec(d)) !== null)
2253
2523
  p.push(c[1]);
2254
2524
  else
2255
2525
  throw new SyntaxError("[sprintf] failed to parse named argument key");
2256
2526
  else
2257
2527
  throw new SyntaxError("[sprintf] failed to parse named argument key");
2258
- i[2] = p;
2528
+ r[2] = p;
2259
2529
  } else
2260
- e |= 2;
2261
- if (e === 3)
2530
+ n |= 2;
2531
+ if (n === 3)
2262
2532
  throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");
2263
- y.push(
2533
+ w.push(
2264
2534
  {
2265
- placeholder: i[0],
2266
- param_no: i[1],
2267
- keys: i[2],
2268
- sign: i[3],
2269
- pad_char: i[4],
2270
- align: i[5],
2271
- width: i[6],
2272
- precision: i[7],
2273
- type: i[8]
2535
+ placeholder: r[0],
2536
+ param_no: r[1],
2537
+ keys: r[2],
2538
+ sign: r[3],
2539
+ pad_char: r[4],
2540
+ align: r[5],
2541
+ width: r[6],
2542
+ precision: r[7],
2543
+ type: r[8]
2274
2544
  }
2275
2545
  );
2276
2546
  } else
2277
2547
  throw new SyntaxError("[sprintf] unexpected placeholder");
2278
- u = u.substring(i[0].length);
2548
+ u = u.substring(r[0].length);
2279
2549
  }
2280
- return m[h] = y;
2550
+ return g[h] = w;
2281
2551
  }
2282
- t.sprintf = s, t.vsprintf = r, typeof window < "u" && (window.sprintf = s, window.vsprintf = r);
2552
+ e.sprintf = s, e.vsprintf = o, typeof window < "u" && (window.sprintf = s, window.vsprintf = o);
2283
2553
  })();
2284
- })(I)), I;
2554
+ })(A)), A;
2285
2555
  }
2286
- var j = le();
2287
- const he = f({
2288
- prompt: o().trim().min(1, "prompt must be a non-empty string").describe("Description of the rule to generate"),
2289
- successCases: g(o()).optional().describe("Examples of correct code that should pass the rule"),
2290
- failureCases: g(o()).optional().describe("Examples of incorrect code that should fail the rule")
2291
- }), qe = f({
2292
- success: K(!0),
2293
- ruleId: o().describe("UUID of the generated rule job"),
2294
- rules: g(o()).describe("Rule IDs that were generated"),
2295
- files: g(o()).describe("File paths that were written"),
2296
- notices: g(o()).optional().describe(
2556
+ var V = ge();
2557
+ const fe = f({
2558
+ prompt: i().trim().min(1, "prompt must be a non-empty string").describe("Description of the rule to generate"),
2559
+ successCases: m(i()).optional().describe("Examples of correct code that should pass the rule"),
2560
+ failureCases: m(i()).optional().describe("Examples of incorrect code that should fail the rule")
2561
+ }), Je = f({
2562
+ success: j(!0),
2563
+ ruleId: i().describe("UUID of the generated rule job"),
2564
+ rules: m(i()).describe("Rule IDs that were generated"),
2565
+ files: m(i()).describe("File paths that were written"),
2566
+ notices: m(i()).optional().describe(
2297
2567
  "Advisory messages about a delivery that was still written — a rule delivered with no `.tests/` fixtures above all. Present only when there is something to say. A machine consumer cannot read stderr prose, so these are carried here rather than only printed"
2298
2568
  )
2299
2569
  });
2300
2570
  f({
2301
- error: o().describe("Error message")
2571
+ error: i().describe("Error message")
2302
2572
  });
2303
- const ce = f({
2304
- ruleId: o().trim().min(1, "ruleId must be a non-empty string").describe("ID of the rule to improve"),
2305
- guidance: o().trim().min(1, "guidance must be a non-empty string").describe("Feedback for iterating on the existing rule"),
2306
- references: g(
2573
+ const we = f({
2574
+ ruleId: i().trim().min(1, "ruleId must be a non-empty string").describe("ID of the rule to improve"),
2575
+ guidance: i().trim().min(1, "guidance must be a non-empty string").describe("Feedback for iterating on the existing rule"),
2576
+ references: m(
2307
2577
  f({
2308
- filename: o().describe("File path relative to .taskless/"),
2309
- content: o().describe("File content")
2578
+ filename: i().describe("File path relative to .taskless/"),
2579
+ content: i().describe("File content")
2310
2580
  })
2311
2581
  ).optional().describe("Reference files to include as context")
2312
- }), Ue = f({
2313
- success: K(!0),
2314
- requestId: o().describe("The request ID for polling status"),
2315
- rules: g(o()).describe("Rule IDs that were updated"),
2316
- files: g(o()).describe("File paths that were written"),
2317
- notices: g(o()).optional().describe(
2582
+ }), Qe = f({
2583
+ success: j(!0),
2584
+ requestId: i().describe("The request ID for polling status"),
2585
+ rules: m(i()).describe("Rule IDs that were updated"),
2586
+ files: m(i()).describe("File paths that were written"),
2587
+ notices: m(i()).optional().describe(
2318
2588
  "Advisory messages about a delivery that was still written — a rule delivered with no `.tests/` fixtures above all. Present only when there is something to say. A machine consumer cannot read stderr prose, so these are carried here rather than only printed"
2319
2589
  )
2320
2590
  });
2321
2591
  f({
2322
- error: o().describe("Error message")
2592
+ error: i().describe("Error message")
2323
2593
  });
2324
- const de = "0.45.2", G = [
2594
+ const Ze = "01a0b1a0-80fb-0000-5dc1-baa4ec44e619", en = [
2595
+ {
2596
+ key: "verbatim",
2597
+ id: "5feff6a3-6768-4817-92d7-5ae3975c6baa",
2598
+ question: "What was the user's comments verbatim?"
2599
+ },
2600
+ {
2601
+ key: "goal",
2602
+ id: "561e87f4-a1b7-4855-b728-29d19421f7e7",
2603
+ question: "What was the user trying to accomplish?"
2604
+ },
2605
+ {
2606
+ key: "completed",
2607
+ id: "6ebdfabb-3575-49aa-857c-47b6bbfdebc8",
2608
+ question: "Did the user successfully complete the task in your opinion?"
2609
+ },
2610
+ {
2611
+ key: "workedWell",
2612
+ id: "2316428e-dc3e-4c96-ae67-a6e8c66d7db5",
2613
+ question: "What steps of the interaction with Taskless worked well?"
2614
+ },
2615
+ {
2616
+ key: "needsImprovement",
2617
+ id: "67bedbd9-ca70-4c1c-b1a6-6df830a453dd",
2618
+ question: "What steps of the interaction with Taskless could use improvement?"
2619
+ }
2620
+ ], C = ["Yes", "No", "Unknown"], nn = /* @__PURE__ */ new Set([
2621
+ "onboard",
2622
+ "create-sg-rule",
2623
+ "create-vale-rule",
2624
+ "create-remote-rule"
2625
+ ]), D = 1440 * 60 * 1e3, tn = 10 * D, sn = 20 * D, ye = f({
2626
+ verbatim: i().trim().min(1, "verbatim must be the user's own words, non-empty").describe("The user's reply, in their own words, unedited"),
2627
+ goal: i().trim().min(1, "goal must be a non-empty string").describe("What the user was trying to accomplish, in your words"),
2628
+ completed: F(C, {
2629
+ error: `completed must be one of ${C.map((e) => `"${e}"`).join(", ")}`
2630
+ }).describe(
2631
+ "Whether the user completed the task, in your opinion. Success is binary; use Unknown when you cannot tell"
2632
+ ),
2633
+ workedWell: i().trim().min(1, "workedWell, when present, must be non-empty").optional().describe("Steps of the interaction with Taskless that worked well"),
2634
+ needsImprovement: i().trim().min(1, "needsImprovement, when present, must be non-empty").optional().describe(
2635
+ "Steps of the interaction with Taskless that could use improvement"
2636
+ )
2637
+ }), be = "0.45.3", P = [
2325
2638
  "Bash",
2326
2639
  "C",
2327
2640
  "Cpp",
@@ -2350,7 +2663,7 @@ const de = "0.45.2", G = [
2350
2663
  "Tsx",
2351
2664
  "TypeScript",
2352
2665
  "Yaml"
2353
- ], ue = {
2666
+ ], ve = {
2354
2667
  "c++": "Cpp",
2355
2668
  cc: "Cpp",
2356
2669
  cs: "CSharp",
@@ -2367,20 +2680,21 @@ const de = "0.45.2", G = [
2367
2680
  sol: "Solidity",
2368
2681
  ts: "TypeScript",
2369
2682
  yml: "Yaml"
2370
- }, pe = new Map(
2371
- G.map((t) => [t.toLowerCase(), t])
2683
+ }, ke = new Map(
2684
+ P.map((e) => [e.toLowerCase(), e])
2372
2685
  );
2373
- function Fe(t) {
2374
- const n = t.toLowerCase();
2375
- return pe.get(n) ?? ue[n];
2686
+ function an(e) {
2687
+ const t = e.toLowerCase();
2688
+ return ke.get(t) ?? ve[t];
2376
2689
  }
2377
- const We = {
2690
+ const on = {
2378
2691
  TypeScript: "ts",
2379
2692
  Tsx: "tsx"
2380
- }, ge = "3.20.0", C = "converter:", P = {
2693
+ }, Se = "3.21.0", R = "converter:", G = {
2381
2694
  // markup — parsed, the format's own constructs skipped
2382
2695
  ".htm": "markup",
2383
2696
  ".html": "markup",
2697
+ ".ipynb": "markup",
2384
2698
  ".markdown": "markup",
2385
2699
  ".md": "markup",
2386
2700
  ".mdx": "markup",
@@ -2441,7 +2755,7 @@ const We = {
2441
2755
  ".mkdn": "plaintext",
2442
2756
  ".tex": "plaintext",
2443
2757
  // plaintext HERE, though Vale's own docs list it as comment-tier. Measured on
2444
- // the pinned 3.20.0 binary a bare non-comment line lints, which is the
2758
+ // the pinned 3.21.0 binary a bare non-comment line lints, which is the
2445
2759
  // plaintext signature. `.qml` and `.scss` sat here for the same reason until
2446
2760
  // 3.18.0 made the docs true for them; `.pyi` is the row where transcribing
2447
2761
  // the docs would still ship the wrong tier — the case for probing rather than
@@ -2457,206 +2771,224 @@ const We = {
2457
2771
  ".rst": "converter:rst2html",
2458
2772
  ".xml": "converter:xsltproc and an XSLT stylesheet"
2459
2773
  };
2460
- function A(t) {
2461
- return Object.entries(P).filter(([, n]) => n === t).map(([n]) => n);
2774
+ function x(e) {
2775
+ return Object.entries(G).filter(([, t]) => t === e).map(([t]) => t);
2462
2776
  }
2463
- const me = A("markup"), fe = A("comment"), ye = A("plaintext"), V = Object.fromEntries(
2464
- Object.entries(P).flatMap(
2465
- ([t, n]) => n.startsWith(C) ? [[t, n.slice(C.length)]] : []
2777
+ const Te = x("markup"), Ie = x("comment"), Ae = x("plaintext"), M = Object.fromEntries(
2778
+ Object.entries(G).flatMap(
2779
+ ([e, t]) => t.startsWith(R) ? [[e, t.slice(R.length)]] : []
2466
2780
  )
2467
2781
  );
2468
- function we() {
2469
- const t = /* @__PURE__ */ new Map();
2470
- for (const [n, s] of Object.entries(
2471
- V
2782
+ function xe() {
2783
+ const e = /* @__PURE__ */ new Map();
2784
+ for (const [t, s] of Object.entries(
2785
+ M
2472
2786
  )) {
2473
- const r = t.get(s);
2474
- r === void 0 ? t.set(s, [n]) : r.push(n);
2787
+ const o = e.get(s);
2788
+ o === void 0 ? e.set(s, [t]) : o.push(t);
2475
2789
  }
2476
- return [...t].map(([n, s]) => ({
2477
- converter: n,
2790
+ return [...e].map(([t, s]) => ({
2791
+ converter: t,
2478
2792
  extensions: s
2479
2793
  }));
2480
2794
  }
2481
- const ve = we(), $e = Object.keys(V);
2482
- function be() {
2483
- return G.join(", ");
2795
+ const Le = xe(), rn = Object.keys(M);
2796
+ function Ee() {
2797
+ return P.join(", ");
2484
2798
  }
2485
- function ke() {
2486
- return me.join(", ");
2799
+ function _e() {
2800
+ return Te.join(", ");
2487
2801
  }
2488
- function Se() {
2489
- return fe.join(", ");
2802
+ function Ce() {
2803
+ return Ie.join(", ");
2490
2804
  }
2491
- function Te() {
2492
- return ye.join(", ");
2805
+ function Re() {
2806
+ return Ae.join(", ");
2493
2807
  }
2494
- function Ie() {
2495
- return ve.map(
2496
- ({ extensions: t, converter: n }) => `${t.join("/")} (needs ${n})`
2808
+ function Ne() {
2809
+ return Le.map(
2810
+ ({ extensions: e, converter: t }) => `${e.join("/")} (needs ${t})`
2497
2811
  ).join(", ");
2498
2812
  }
2499
- const xe = /* @__PURE__ */ Object.assign({
2500
- "../agent/auth.md": U,
2501
- "../agent/check.md": F,
2502
- "../agent/ci.md": W,
2503
- "../agent/create-legacy-rule.md": $,
2504
- "../agent/create-remote-rule.md": z,
2505
- "../agent/create-runtime-rule.md": H,
2506
- "../agent/create-sg-rule.md": Y,
2507
- "../agent/create-vale-rule.md": B,
2508
- "../agent/delete-rule.md": X,
2509
- "../agent/detect.md": J,
2510
- "../agent/improve-rule.anonymous.md": Q,
2511
- "../agent/improve-rule.md": Z,
2512
- "../agent/info.md": ee,
2513
- "../agent/init.md": ne,
2514
- "../agent/onboard.md": te,
2515
- "../agent/route.md": se,
2516
- "../agent/rule-meta.md": ae,
2517
- "../agent/rule.md": re,
2518
- "../agent/update.md": ie,
2519
- "../agent/verify-rule.md": oe
2813
+ const Oe = /* @__PURE__ */ Object.assign({
2814
+ "../agent/auth.md": Y,
2815
+ "../agent/check.md": H,
2816
+ "../agent/ci.md": B,
2817
+ "../agent/create-legacy-rule.md": X,
2818
+ "../agent/create-remote-rule.md": J,
2819
+ "../agent/create-runtime-rule.md": Q,
2820
+ "../agent/create-sg-rule.md": Z,
2821
+ "../agent/create-vale-rule.md": ee,
2822
+ "../agent/delete-rule.md": ne,
2823
+ "../agent/detect.md": te,
2824
+ "../agent/feedback-invite.md": se,
2825
+ "../agent/feedback.md": ae,
2826
+ "../agent/improve-rule.anonymous.md": oe,
2827
+ "../agent/improve-rule.md": ie,
2828
+ "../agent/info.md": re,
2829
+ "../agent/init.md": le,
2830
+ "../agent/onboard.md": he,
2831
+ "../agent/route.md": ce,
2832
+ "../agent/rule-meta.md": de,
2833
+ "../agent/rule.md": ue,
2834
+ "../agent/update.md": pe,
2835
+ "../agent/verify-rule.md": me
2520
2836
  });
2521
- function Ae() {
2522
- const t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
2523
- for (const [s, r] of Object.entries(xe)) {
2524
- const l = je(r), m = s.split("/").pop()?.replace(/\.md$/, "");
2525
- if (m)
2526
- if (m.endsWith(".anonymous")) {
2527
- const b = m.slice(0, -10);
2528
- n.set(b, l);
2837
+ function Ke() {
2838
+ const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map();
2839
+ for (const [s, o] of Object.entries(Oe)) {
2840
+ const l = Fe(o), g = s.split("/").pop()?.replace(/\.md$/, "");
2841
+ if (g)
2842
+ if (g.endsWith(".anonymous")) {
2843
+ const k = g.slice(0, -10);
2844
+ t.set(k, l);
2529
2845
  } else
2530
- t.set(m, l);
2846
+ e.set(g, l);
2531
2847
  }
2532
- return { recipeMap: t, anonymousMap: n };
2848
+ return { recipeMap: e, anonymousMap: t };
2533
2849
  }
2534
- const { recipeMap: R, anonymousMap: Le } = Ae(), Ee = {
2535
- "create-remote-rule": he,
2536
- "improve-rule": ce
2537
- }, _e = "<package-manager-dlx>", Ce = "<taskless-cli>", Re = "The caller supplies this evidence, which names", Ne = "The caller supplies";
2538
- function N(t, n, s) {
2850
+ const { recipeMap: N, anonymousMap: je } = Ke(), Ve = {
2851
+ "create-remote-rule": fe,
2852
+ "improve-rule": we,
2853
+ feedback: ye
2854
+ }, De = "<package-manager-dlx>", Pe = "<taskless-cli>", Ge = "The caller supplies this evidence, which names", Me = "The caller supplies";
2855
+ function O(e, t, s) {
2539
2856
  return `Run:
2540
2857
  \`\`\`
2541
- ${t} ${n}
2858
+ ${e} ${t}
2542
2859
  \`\`\`
2543
2860
  ${s}`;
2544
2861
  }
2545
- function x(t) {
2546
- return t.invocation ?? Ce;
2862
+ function v(e) {
2863
+ return e.invocation ?? Pe;
2547
2864
  }
2548
- function Oe(t, n, s = {}) {
2549
- const r = {
2550
- CLI_VERSION: "0.11.1",
2865
+ function qe(e, t, s = {}) {
2866
+ const o = {
2867
+ CLI_VERSION: "0.11.2",
2551
2868
  // Engine reach, from the pinned engine versions rather than transcribed
2552
2869
  // into a recipe. A recipe carrying these lists by hand would go stale on
2553
2870
  // the next binary bump with nothing to catch it, and stale prose about
2554
2871
  // what an engine can read is worse than the silence it replaced — an agent
2555
2872
  // acts on it. `src/rules/capabilities.ts` is the single place a bump edits,
2556
2873
  // and the two vendor-contract tests fail until it agrees with the binary.
2557
- AST_GREP_VERSION: de,
2558
- AST_GREP_LANGUAGES: be(),
2559
- VALE_VERSION: ge,
2560
- VALE_MARKUP_FORMATS: ke(),
2561
- VALE_COMMENT_FORMATS: Se(),
2562
- VALE_PLAINTEXT_FORMATS: Te(),
2563
- VALE_CONVERTER_FORMATS: Ie(),
2564
- PACKAGE_MANAGER_DLX: s.packageManagerDlx ?? _e,
2874
+ AST_GREP_VERSION: be,
2875
+ AST_GREP_LANGUAGES: Ee(),
2876
+ VALE_VERSION: Se,
2877
+ VALE_MARKUP_FORMATS: _e(),
2878
+ VALE_COMMENT_FORMATS: Ce(),
2879
+ VALE_PLAINTEXT_FORMATS: Re(),
2880
+ VALE_CONVERTER_FORMATS: Ne(),
2881
+ PACKAGE_MANAGER_DLX: s.packageManagerDlx ?? De,
2565
2882
  // The two steps that gather evidence by running this CLI. Rendered as
2566
2883
  // whole blocks rather than stripped afterwards, because the default must
2567
2884
  // stay byte-identical to what `taskless agent route` has always printed
2568
2885
  // and a post-strip cannot promise that.
2569
- DETECT_EVIDENCE: s.mechanics === !1 ? Re : N(x(s), "detect --json", "This returns"),
2570
- LOGIN_EVIDENCE: s.mechanics === !1 ? Ne : N(x(s), "info --json", "and note"),
2886
+ DETECT_EVIDENCE: s.mechanics === !1 ? Ge : O(v(s), "detect --json", "This returns"),
2887
+ LOGIN_EVIDENCE: s.mechanics === !1 ? Me : O(v(s), "info --json", "and note"),
2571
2888
  // Three steps, in descending order of how much the resolver actually
2572
2889
  // knows: the caller was told how the CLI was launched; the build is a
2573
2890
  // nightly/dev/self that knows what it is; nobody knows, so ask the agent.
2574
- TASKLESS_CLI: x(s)
2891
+ TASKLESS_CLI: v(s)
2575
2892
  };
2576
- if (t.includes("%(INPUT_SCHEMA)s")) {
2577
- const l = Ee[n];
2578
- r.INPUT_SCHEMA = l ? JSON.stringify(q(l), null, 2) : "(no input schema for this topic)";
2893
+ if (e.includes("%(INPUT_SCHEMA)s")) {
2894
+ const l = Ve[t];
2895
+ o.INPUT_SCHEMA = l ? JSON.stringify($(l), null, 2) : "(no input schema for this topic)";
2579
2896
  }
2580
- return r;
2897
+ return o;
2581
2898
  }
2582
- function D(t) {
2583
- const n = /* @__PURE__ */ new Set(), s = new Proxy(
2899
+ function q(e) {
2900
+ const t = /* @__PURE__ */ new Set(), s = new Proxy(
2584
2901
  {},
2585
2902
  {
2586
- get(r, l) {
2587
- return typeof l == "string" && n.add(l), "";
2903
+ get(o, l) {
2904
+ return typeof l == "string" && t.add(l), "";
2588
2905
  },
2589
2906
  has() {
2590
2907
  return !0;
2591
2908
  }
2592
2909
  }
2593
2910
  );
2594
- return j.sprintf(t, s), [...n];
2911
+ return V.sprintf(e, s), [...t];
2595
2912
  }
2596
- function Ke(t, n, s = {}) {
2597
- return M(t, n, s);
2913
+ function Ue(e, t, s = {}) {
2914
+ return U(e, t, s);
2598
2915
  }
2599
- function M(t, n, s = {}) {
2600
- const r = j.sprintf(t, Oe(t, n, s));
2601
- return s.header === !1 ? L(r) : r;
2916
+ function U(e, t, s = {}) {
2917
+ const o = V.sprintf(e, qe(e, t, s));
2918
+ return s.header === !1 ? L(o) : s.directive === !0 ? We(o, v(s)) : o;
2602
2919
  }
2603
- const O = "# Topic:";
2604
- function L(t) {
2605
- const n = t.indexOf(`
2920
+ const W = "# Topic:";
2921
+ function K(e) {
2922
+ return `Resolved by the CLI when you fetched it. Your next Taskless task, in this session or another, fetches it again with \`${e} agent <topic>\`; do not reuse this copy. If Taskless was installed or upgraded during this session, the skill in your context is stale until it is reloaded.`;
2923
+ }
2924
+ function We(e, t) {
2925
+ if (!e.startsWith(W)) return e;
2926
+ const s = e.indexOf(`
2927
+ `);
2928
+ return s === -1 ? `${e}
2929
+ ${K(t)}` : `${e.slice(0, s)}
2930
+ ${K(t)}${e.slice(s)}`;
2931
+ }
2932
+ function L(e) {
2933
+ if (!e.startsWith(W)) return e;
2934
+ const t = e.indexOf(`
2935
+
2606
2936
  `);
2607
- if (n === -1)
2608
- return t.startsWith(O) ? "" : t;
2609
- if (!t.startsWith(O)) return t;
2610
- const s = t.slice(n + 1);
2611
- return s.startsWith(`
2612
- `) ? s.slice(1) : s;
2937
+ return t === -1 ? "" : e.slice(t + 2);
2613
2938
  }
2614
- function ze(t, n = {}) {
2615
- const s = E(t, n);
2939
+ function ln(e, t = {}) {
2940
+ const s = E(e, t);
2616
2941
  if (s !== void 0)
2617
- return Ke(s, t, n);
2942
+ return Ue(s, e, t);
2618
2943
  }
2619
- function E(t, n) {
2620
- return n.anonymous ? Le.get(t) ?? R.get(t) : R.get(t);
2944
+ function E(e, t) {
2945
+ return t.anonymous ? je.get(e) ?? N.get(e) : N.get(e);
2621
2946
  }
2622
- function He(t, n = {}) {
2623
- const s = E(t, n);
2947
+ function hn(e, t = {}) {
2948
+ const s = E(e, t);
2624
2949
  if (s === void 0) return;
2625
- const r = s, l = n.header === !1 ? L(r) : r;
2950
+ const o = s, l = t.header === !1 ? L(o) : o;
2626
2951
  return {
2627
2952
  text: l,
2628
- variables: D(l)
2953
+ variables: q(l)
2629
2954
  };
2630
2955
  }
2631
- function Ye(t, n = {}) {
2632
- const s = E(t, n);
2956
+ function cn(e, t = {}) {
2957
+ const s = E(e, t);
2633
2958
  if (s === void 0) return;
2634
- const r = s, l = n.header === !1 ? L(r) : r;
2959
+ const o = s, l = t.header === !1 ? L(o) : o;
2635
2960
  return {
2636
- text: M(r, t, n),
2637
- variables: D(l)
2961
+ text: U(o, e, t),
2962
+ variables: q(l)
2638
2963
  };
2639
2964
  }
2640
- function je(t) {
2641
- return t.replaceAll(/^[ \t]*<!--\s*vale\b.*?-->[ \t]*\r?\n?/gm, "");
2965
+ function Fe(e) {
2966
+ return e.replaceAll(/^[ \t]*<!--\s*vale\b.*?-->[ \t]*\r?\n?/gm, "");
2642
2967
  }
2643
2968
  export {
2644
- de as A,
2645
- Pe as P,
2646
- $e as V,
2647
- Ye as a,
2648
- He as b,
2649
- Ve as c,
2650
- De as d,
2651
- V as e,
2652
- ge as f,
2653
- ze as g,
2654
- be as h,
2655
- We as i,
2656
- ce as j,
2657
- he as k,
2658
- qe as l,
2659
- Ue as o,
2660
- Fe as r,
2661
- Me as w
2969
+ be as A,
2970
+ ze as P,
2971
+ Ze as S,
2972
+ rn as V,
2973
+ cn as a,
2974
+ hn as b,
2975
+ Ye as c,
2976
+ nn as d,
2977
+ tn as e,
2978
+ Be as f,
2979
+ ln as g,
2980
+ M as h,
2981
+ Se as i,
2982
+ ye as j,
2983
+ sn as k,
2984
+ en as l,
2985
+ an as m,
2986
+ Ee as n,
2987
+ on as o,
2988
+ we as p,
2989
+ Qe as q,
2990
+ He as r,
2991
+ fe as s,
2992
+ Je as t,
2993
+ Xe as w
2662
2994
  };