@taskless/cli 0.11.0 → 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.
@@ -0,0 +1,2994 @@
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
+ return "npx @taskless/cli";
5
+ }
6
+ const z = "%(TASKLESS_CLI)s";
7
+ function He(e) {
8
+ return e.replaceAll(z, "npx @taskless/cli");
9
+ }
10
+ function Be(e) {
11
+ return e;
12
+ }
13
+ function Xe(e) {
14
+ return e;
15
+ }
16
+ const Y = `# Topic: auth (CLI v%(CLI_VERSION)s / topic v1)
17
+
18
+ ## Goal
19
+ Manage Taskless authentication. Three branches:
20
+ - **Login**: start the device-code flow and wait for the user to
21
+ approve in their browser.
22
+ - **Logout**: remove the saved token.
23
+ - **Status**: check whether a token is present and whose identity
24
+ it represents.
25
+
26
+ ## Preconditions
27
+ - \`.taskless/\` directory exists.
28
+ - For login: the user has a browser to approve the device code.
29
+
30
+ ## Steps
31
+
32
+ Pick the branch matching the user's intent.
33
+
34
+ ### Login
35
+
36
+ 1. Run:
37
+ \`\`\`
38
+ %(TASKLESS_CLI)s auth login
39
+ \`\`\`
40
+ 2. The CLI prints a URL and a device code. Tell the user to open the
41
+ URL and enter the code.
42
+ 3. The CLI polls until the token is approved. On success, the token
43
+ is written to \`.taskless/.env.local.json\` and a confirmation is
44
+ printed.
45
+ 4. Report success. Suggest \`%(TASKLESS_CLI)s info\` to verify identity.
46
+
47
+ The \`--anonymous\` flag is rejected on \`auth login\`, it errors with
48
+ "auth commands cannot be anonymous". Don't pass it.
49
+
50
+ ### Logout
51
+
52
+ 1. Run:
53
+ \`\`\`
54
+ %(TASKLESS_CLI)s auth logout
55
+ \`\`\`
56
+ 2. The CLI removes the saved token (or reports "Not logged in" if
57
+ none was present).
58
+ 3. Report the outcome.
59
+
60
+ ### Status (no subcommand)
61
+
62
+ 1. Run:
63
+ \`\`\`
64
+ %(TASKLESS_CLI)s auth
65
+ \`\`\`
66
+ 2. Output is one of:
67
+ - "Not logged in." (with hint to run \`auth login\`)
68
+ - "Logged in as <user> (<orgs>)."
69
+ - "Logged in, but unable to verify identity." (token expired or
70
+ revoked, suggest re-login)
71
+ 3. Report to the user.
72
+
73
+ ## Errors
74
+
75
+ \`auth login\` and \`auth logout\` accept \`--json\`. On error, the
76
+ standardized \`{ ok: false, code, message }\` envelope is written to
77
+ stdout (and human text on stderr is suppressed). On success in
78
+ \`--json\` mode, the commands exit 0 silently, no success envelope is
79
+ emitted. The status path (\`%(TASKLESS_CLI)s auth\` with no subcommand) accepts
80
+ \`--json\` for forward-compat but currently has no error paths to
81
+ report.
82
+
83
+ | code | meaning | fix |
84
+ |-----------------|--------------------------------------------------------------------------------------|--------------------------------------|
85
+ | \`INVALID_INPUT\` | \`--anonymous\` passed to \`auth login\` (rejected: auth commands cannot be anonymous) | Don't pass \`--anonymous\` |
86
+ | \`NETWORK_ERROR\` | Device flow / token endpoint unreachable, or the device code expired before approval | Check connectivity; retry |
87
+ | \`AUTH_REQUIRED\` | The user denied the authorization request in their browser | Re-run \`%(TASKLESS_CLI)s auth login\` |
88
+
89
+ ## See Also
90
+
91
+ - \`%(TASKLESS_CLI)s agent info\`: see auth state and skill versions
92
+ - \`%(TASKLESS_CLI)s agent route\`: first action that requires auth
93
+ `, H = `# Topic: check (CLI v%(CLI_VERSION)s / topic v2)
94
+
95
+ ## Goal
96
+ Run the applicable rules against the codebase and report matches. Two
97
+ rule kinds run: **static** ast-grep rules in \`.taskless/rules/sg/\`, and
98
+ **runtime** rules in \`.taskless/rules/runtime/\` (a directory of ast-grep
99
+ capture rules plus a \`check.ts\`). Used standalone (full project scan),
100
+ in CI (diff-only scan), or after rule create/improve to validate.
101
+
102
+ ## Preconditions
103
+ - \`.taskless/\` directory exists.
104
+ - At least one rule exists in \`.taskless/rules/sg/\` or
105
+ \`.taskless/rules/runtime/\`. (If none exist, the CLI exits 0 with a
106
+ friendly message suggesting \`%(TASKLESS_CLI)s rule create\`.)
107
+ - No auth required. Static rules always run; whether runtime rules run
108
+ depends on auth state. See "What runs".
109
+
110
+ ## What runs
111
+
112
+ \`check\` never requires auth. The two rule kinds run differently:
113
+
114
+ - **Static rules** (\`.taskless/rules/sg/<id>/<id>.yml\`) are inert ast-grep patterns
115
+ and **always run**, in every mode, with no network call. The offline
116
+ linter posture.
117
+ - **Runtime rules** (\`.taskless/rules/runtime/<name>/\`) execute a
118
+ \`check.ts\` (arbitrary code), so they run ONLY when that code is
119
+ verified:
120
+ - **Logged in** (token or API key): each rule's \`check.ts\` is
121
+ reconciled against the Taskless service; rules the server blessed
122
+ (\`run\`) execute, and the rest are withheld and reported (advisory).
123
+ - **Logged out, \`--anonymous\`, no GitHub remote, or service
124
+ unavailable**, runtime rules are **skipped** (reported, never run).
125
+ Static rules still run.
126
+ - **\`--dangerously-run-scripts\`**: runs every runtime rule trusting
127
+ local signatures, with no network call, behind a prominent warning.
128
+ This is the only way to run runtime rules unverified.
129
+
130
+ Notices about skipped/withheld runtime rules are human-readable stderr
131
+ only: they never change the exit code. Under \`--json\` they do NOT appear
132
+ as warnings; instead an additive optional \`skipped: [{ rule, reason }]\`
133
+ array is included alongside the unchanged \`{ success, results }\`. The
134
+ authoritative allow-list is the server's; the CI backstop
135
+ (\`%(TASKLESS_CLI)s agent ci\`) is the enforcement point for runtime rules.
136
+
137
+ ## Flags
138
+ - \`--json\`: machine output (\`{ success, results, skipped? }\`).
139
+ - \`--anonymous\`: run only static rules; skip runtime rules.
140
+ - \`--dangerously-run-scripts\`: run runtime \`check.ts\` unverified.
141
+ - \`--timeout <seconds>\`: per-runtime-check wall-clock bound (default 10).
142
+
143
+ ## Steps
144
+
145
+ 1. **Decide scope.** Default is a full project scan. If the user
146
+ specified files (or you're in a CI context with a known diff),
147
+ pass them as positional arguments.
148
+
149
+ 2. **Invoke the CLI.** Either:
150
+ \`\`\`
151
+ %(TASKLESS_CLI)s check --json
152
+ \`\`\`
153
+ or, scoped to specific paths:
154
+ \`\`\`
155
+ %(TASKLESS_CLI)s check --json src/foo.ts src/bar.ts
156
+ \`\`\`
157
+ or, against a git diff:
158
+ \`\`\`
159
+ %(TASKLESS_CLI)s check --json $(git diff --name-only main...HEAD)
160
+ \`\`\`
161
+ Paths that don't exist on disk are silently filtered, so you can
162
+ pipe raw \`git diff\` output directly without pre-filtering.
163
+
164
+ When runtime rules were present but not run (e.g. logged out), the
165
+ JSON also carries \`"skipped": [{ "rule": "<name>", "reason": "…" }]\`
166
+ alongside \`success\`/\`results\`. Surface it so CI can tell that runtime
167
+ rules did not execute; it never affects the exit code.
168
+
169
+ 3. **Parse the JSON output.** Shape:
170
+ \`\`\`json
171
+ {
172
+ "success": false,
173
+ "results": [
174
+ {
175
+ "source": "ast-grep",
176
+ "ruleId": "no-eval",
177
+ "severity": "error",
178
+ "message": "Avoid eval()",
179
+ "note": null,
180
+ "file": "src/foo.ts",
181
+ "range": {
182
+ "start": { "line": 12, "column": 4 },
183
+ "end": { "line": 12, "column": 14 }
184
+ },
185
+ "matchedText": "eval(input)",
186
+ "fix": null
187
+ }
188
+ ]
189
+ }
190
+ \`\`\`
191
+
192
+ 4. **Report findings to the user.** Group by \`file\`. Show \`severity\`,
193
+ \`message\`, and \`ruleId\` for each finding; the \`range.start\` is the
194
+ useful line/column to surface. The \`success\` field reflects only
195
+ error-severity findings: \`success: false\` means at least one
196
+ \`severity: "error"\` finding exists (exit code 1); \`success: true\`
197
+ with a non-empty \`results\` array means there are only
198
+ warning/info/hint findings (exit code 0); \`success: true\` with an
199
+ empty \`results\` array means the codebase is clean. Findings are
200
+ never reported via the \`{ ok: false, code, message }\` envelope:
201
+ the envelope only appears when the scan itself fails (e.g.
202
+ \`SCAN_FAILED\`) and the normal results payload is absent.
203
+
204
+ ## Exit codes
205
+
206
+ - \`0\`: All checks passed, no rules configured, or all supplied
207
+ paths missing
208
+ - \`1\`: Errors detected or scan failed
209
+
210
+ ## Errors
211
+
212
+ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
213
+
214
+ | code | meaning | fix |
215
+ |---------------|-----------------------|----------------------------------|
216
+ | \`SCAN_FAILED\` | ast-grep scan errored | Report; check rule YAML validity |
217
+
218
+ ## See Also
219
+
220
+ - \`%(TASKLESS_CLI)s agent route\`: add a rule if none exist
221
+ - \`%(TASKLESS_CLI)s agent ci\`: wire \`check\` into a CI pipeline
222
+ `, B = `# Topic: ci (CLI v%(CLI_VERSION)s / topic v1)
223
+
224
+ ## Goal
225
+ Wire \`%(TASKLESS_CLI)s check\` into the user's existing CI so rules run
226
+ automatically on pushes and pull requests. Integrate with what they
227
+ already have, never replace or edit their main pipeline.
228
+
229
+ This recipe teaches two patterns (full scan and diff scan) that
230
+ translate to any CI system. Common systems are listed as hints; if
231
+ you recognize one not on the list, apply the same patterns.
232
+
233
+ ## Preconditions
234
+ - \`.taskless/\` directory exists and contains at least one rule. (If
235
+ no rules exist, instruct the user to fetch
236
+ \`%(TASKLESS_CLI)s agent route\` first, wiring CI with zero rules
237
+ produces an always-green check that gives false confidence.)
238
+ - A local \`%(TASKLESS_CLI)s check\` succeeds (or fails with real findings the
239
+ user is OK with seeing in CI's first run).
240
+ - No auth required for CI: \`check\` is unauthenticated by default.
241
+ (Optionally tokenized as a server-enforced backstop; see step 7.)
242
+
243
+ ## Steps
244
+
245
+ ### 1. Discover the user's CI system
246
+
247
+ Scan the repo root for CI config files. Hints (not exhaustive):
248
+
249
+ | File / directory | CI system |
250
+ |---------------------------|---------------------|
251
+ | \`.github/workflows/*.yml\` | GitHub Actions |
252
+ | \`.gitlab-ci.yml\` | GitLab CI |
253
+ | \`.circleci/config.yml\` | CircleCI |
254
+ | \`Jenkinsfile\` | Jenkins |
255
+ | \`azure-pipelines.yml\` | Azure Pipelines |
256
+ | \`bitbucket-pipelines.yml\` | Bitbucket Pipelines |
257
+ | \`.buildkite/\` | Buildkite |
258
+ | \`.drone.yml\` | Drone |
259
+ | \`.travis.yml\` | Travis CI |
260
+
261
+ Sum up what you found and confirm with the user. If zero match, ask
262
+ which CI they use. If multiple match, ask which should run Taskless.
263
+
264
+ ### 2. Agree on the scan pattern
265
+
266
+ - **Full scan**: \`%(TASKLESS_CLI)s check\`. Scans everything. Best for runs
267
+ on the main/default branch.
268
+ - **Diff scan**: \`%(TASKLESS_CLI)s check $(git diff --name-only ...)\`.
269
+ Faster for PR builds. Per-CI diff target var:
270
+
271
+ | CI | Target branch variable |
272
+ |---------------------|-----------------------------------------------|
273
+ | GitHub Actions | \`github.base_ref\` |
274
+ | GitLab CI | \`CI_MERGE_REQUEST_TARGET_BRANCH_NAME\` |
275
+ | CircleCI | \`CIRCLE_BRANCH\` (fetch main and diff against) |
276
+ | Jenkins | \`env.CHANGE_TARGET\` |
277
+ | Azure Pipelines | \`System.PullRequest.TargetBranch\` |
278
+ | Bitbucket Pipelines | \`BITBUCKET_PR_DESTINATION_BRANCH\` |
279
+
280
+ \`%(TASKLESS_CLI)s check\` silently filters paths that don't exist, so raw
281
+ \`git diff --name-only\` output can pipe in directly.
282
+
283
+ **Recommended default:** diff scan on PRs, full scan on pushes to
284
+ main.
285
+
286
+ ### 3. Verify locally first
287
+
288
+ Run \`%(TASKLESS_CLI)s check\`:
289
+ - Clean pass → proceed.
290
+ - "No rules configured" → stop. Fetch \`%(TASKLESS_CLI)s agent route\`.
291
+ - Findings → tell the user CI will fail; ask whether to fix,
292
+ suppress, or proceed knowing the first CI run will be red.
293
+
294
+ ### 4. Generate the config
295
+
296
+ Rules:
297
+ 1. Write a NEW standalone file. Never modify the user's existing
298
+ CI config.
299
+ 2. Prefer the CI's native include mechanism. Tell the user the one
300
+ line they need to add to their main config.
301
+ 3. If the file you'd write already exists, ask before overwriting.
302
+
303
+ Canonical paths:
304
+
305
+ | CI | File path |
306
+ |---------------------|---------------------------------------------------------------------|
307
+ | GitHub Actions | \`.github/workflows/taskless.yml\` (standalone, no include needed) |
308
+ | GitLab CI | \`.taskless/ci/gitlab.yml\` (user adds \`include\`) |
309
+ | CircleCI | \`.taskless/ci/circleci-job.yml\` (no include, user copies job) |
310
+ | Jenkins | \`.taskless/ci/taskless.Jenkinsfile\` (user \`load()\`s) |
311
+ | Azure Pipelines | \`.taskless/ci/azure-taskless.yml\` (user references via \`template:\`) |
312
+ | Bitbucket Pipelines | \`.taskless/ci/bitbucket-pipelines.yml\` (user merges manually) |
313
+ | Other | \`.taskless/ci/<system>.<ext>\` + clear wiring instructions |
314
+
315
+ ### 5. GitHub Actions reference template
316
+
317
+ The reference template, translate the same shape (checkout with
318
+ full history, set up Node, conditional check) for other CIs.
319
+
320
+ Substitute \`%(PACKAGE_MANAGER_DLX)s\` with the CI runner's own complete
321
+ invocation, \`npx @taskless/cli\`, \`pnpm dlx @taskless/cli\`,
322
+ \`yarn dlx @taskless/cli\`, or \`bunx @taskless/cli\`, picking the launcher
323
+ from \`pnpm-lock.yaml\`/\`yarn.lock\`/\`bun.lockb\`. It is a whole command on
324
+ its own, exactly as the template uses it; never combine it with
325
+ \`%(TASKLESS_CLI)s\`. Which launcher the repository you are wiring up should
326
+ run is a different question from how this recipe was fetched, which is why
327
+ it stays a separate placeholder.
328
+
329
+ \`\`\`yaml
330
+ name: Taskless
331
+
332
+ on:
333
+ push:
334
+ branches: [main]
335
+ pull_request:
336
+
337
+ permissions:
338
+ contents: read
339
+
340
+ jobs:
341
+ check:
342
+ runs-on: ubuntu-latest
343
+ steps:
344
+ - uses: actions/checkout@v4
345
+ with:
346
+ fetch-depth: 0
347
+
348
+ - uses: actions/setup-node@v4
349
+ with:
350
+ node-version: 20
351
+
352
+ - name: Taskless check
353
+ run: |
354
+ if [ "\${{ github.event_name }}" = "pull_request" ]; then
355
+ git fetch origin "\${{ github.base_ref }}" --depth=1
356
+ FILES=$(git diff --name-only "origin/\${{ github.base_ref }}...HEAD")
357
+ if [ -z "$FILES" ]; then
358
+ echo "No changed files."
359
+ exit 0
360
+ fi
361
+ %(PACKAGE_MANAGER_DLX)s check $FILES
362
+ else
363
+ %(PACKAGE_MANAGER_DLX)s check
364
+ fi
365
+ \`\`\`
366
+
367
+ Simplifications:
368
+ - Full-scan only → drop the \`if\`, just run \`%(PACKAGE_MANAGER_DLX)s check\`.
369
+ - Diff-scan only → remove the \`push:\` trigger.
370
+
371
+ ### 6. Translate to other CIs
372
+
373
+ The six universal steps:
374
+ 1. Set up Node (v20 default; match the user's existing CI version).
375
+ 2. Fetch with full depth (or enough to reach the target branch).
376
+ 3. Fetch the target branch.
377
+ 4. Compute changed files with \`git diff --name-only "origin/<target>...HEAD"\`.
378
+ 5. Exit early if the diff is empty.
379
+ 6. Call \`%(PACKAGE_MANAGER_DLX)s check $FILES\` for PR builds, or
380
+ \`%(PACKAGE_MANAGER_DLX)s check\` for main-branch builds.
381
+
382
+ YAML for GitHub/GitLab/Azure/Bitbucket; Groovy for Jenkins;
383
+ different structure for CircleCI. The six steps stay the same.
384
+
385
+ ### 7. Authentication in CI (optional backstop)
386
+
387
+ \`%(TASKLESS_CLI)s check\` does NOT require authentication. The generated CI
388
+ config works out of the box with no secrets and scans all local rules.
389
+
390
+ Static ast-grep rules always run in CI with no secrets. **Runtime
391
+ rules** (\`.taskless/rules/runtime/\`, which execute a \`check.ts\`) only
392
+ run when their code is server-verified, so an unauthenticated CI job
393
+ runs the static rules and skips the runtime ones.
394
+
395
+ Exposing a \`TASKLESS_TOKEN\` secret turns CI into the **backstop** for
396
+ runtime rules: an authenticated \`check\` reconciles each runtime rule's
397
+ \`check.ts\` against the Taskless service and runs exactly the
398
+ server-blessed set, withholding any that drift or were never issued.
399
+ This is the enforcement point for runtime rules, local developer runs
400
+ skip them unless \`--dangerously-run-scripts\` is passed. To wire it, set
401
+ the token as an env var on the check step (GitHub Actions):
402
+
403
+ \`\`\`yaml
404
+ - name: Taskless check
405
+ env:
406
+ TASKLESS_TOKEN: \${{ secrets.TASKLESS_TOKEN }}
407
+ run: |
408
+ ...
409
+ \`\`\`
410
+
411
+ Add this only when the user wants server-enforced rules in CI; the
412
+ unauthenticated default remains fully supported. Also mention auth if
413
+ the user explicitly asks to run authenticated commands (e.g.
414
+ \`rule create\`/\`rule improve\`) in CI, uncommon.
415
+
416
+ ### 8. Package manager caveats
417
+
418
+ - **pnpm**: \`pnpm dlx\` works but has slow cold starts. If the user's
419
+ pipeline already sets up pnpm, suggest adding \`@taskless/cli\` as a
420
+ dev dep and calling it via \`pnpm taskless check\`. That \`taskless\` is the
421
+ binary pnpm resolves from the wired repo's own \`node_modules/.bin\`, not
422
+ this CLI's invocation, so it is not \`%(TASKLESS_CLI)s\`.
423
+ - **Yarn v1 (classic)**: doesn't support \`yarn dlx\`. Use \`npx\`.
424
+ - **Bun**: \`bunx\` works.
425
+
426
+ ### 9. Report back
427
+
428
+ Show:
429
+ 1. The path written and a 10-15 line excerpt.
430
+ 2. For CIs needing manual wiring, the exact \`include:\` / reference
431
+ line for their main config.
432
+ 3. \`git status\` so they can review before committing.
433
+ 4. A note that the first CI run exercises rules, if existing
434
+ matches exist, CI will fail until fixed or suppressed.
435
+
436
+ ## Errors
437
+
438
+ - **No rules** → fetch \`%(TASKLESS_CLI)s agent route\`. Don't write CI
439
+ config.
440
+ - **Unrecognized CI** → produce a generic \`.taskless/ci/check.sh\`
441
+ script implementing the six universal steps. Be upfront it's a
442
+ starting point.
443
+ - **Target CI file exists, user declines overwrite** → stop. Show
444
+ what you would have written; let them reconcile manually.
445
+
446
+ ## See Also
447
+
448
+ - \`%(TASKLESS_CLI)s agent check\`: the command being wired into CI
449
+ - \`%(TASKLESS_CLI)s agent route\`: required if no rules exist yet
450
+ `, X = `# Topic: create-legacy-rule (CLI v%(CLI_VERSION)s / topic v1)
451
+
452
+ ## You are here
453
+ This is \`create-legacy-rule\`. It helps you write a rule for a linter the
454
+ repository already runs (ESLint, Ruff, RuboCop, Stylelint) in that
455
+ tool's own dialect, so that tool enforces it.
456
+ If that is not the kind of check you need, re-run \`%(TASKLESS_CLI)s agent route\`
457
+ and follow its decision rather than adapting this recipe.
458
+
459
+ ## Goal
460
+ Author a rule in a linter the repository ALREADY uses, expressed in that
461
+ tool's own dialect (an ESLint rule, a Ruff rule selection, a RuboCop cop,
462
+ a Stylelint rule, etc.). Taskless does not maintain a catalog of linter
463
+ rules, you source the knowledge from the repo first and the web second,
464
+ then write the rule where that tool expects it.
465
+
466
+ ## Preconditions
467
+ - The repo has a detected linter (confirm via \`%(TASKLESS_CLI)s detect --json\`).
468
+ - The agent can read/write files and fetch web pages.
469
+ - No auth required.
470
+
471
+ ## Steps
472
+
473
+ 1. **Confirm the target tool.** Use \`%(TASKLESS_CLI)s detect --json\` to identify
474
+ which linter is configured. If more than one could host this rule,
475
+ ask the user which tool should own it.
476
+
477
+ 2. **Mine the repo's own rules first.** This is the highest-signal
478
+ source for house style. Look at how the repo already writes rules of
479
+ this kind:
480
+ - existing config (e.g. \`.eslintrc*\`/\`eslint.config.*\`, \`ruff.toml\`
481
+ or \`[tool.ruff]\`, \`.rubocop.yml\`, \`.stylelintrc*\`);
482
+ - any custom/local rules the repo authored (the \`detect\` output's
483
+ rule styles point at these);
484
+ - the conventions, severity choices, and naming they use.
485
+ Match that style.
486
+
487
+ 3. **Fall back to the web only if the repo signal is thin.** If the repo
488
+ doesn't show how to express this rule, fetch the linter's CURRENT
489
+ documentation (WebFetch/WebSearch). Prefer the latest version's docs
490
+ and, when present, an \`llms.txt\` on the tool's docs site. Confirm the
491
+ installed version where it matters so you target the right syntax.
492
+
493
+ 4. **Author the rule in the tool's dialect.** Write or extend the
494
+ linter's config / custom-rule file the way that tool expects. Keep it
495
+ consistent with the repo's existing entries from step 2.
496
+
497
+ 5. **Report, and be explicit about who runs it.** Show the file(s) you
498
+ changed. Make clear that the user's OWN toolchain runs this rule:
499
+ \`%(TASKLESS_CLI)s check\` does NOT execute external linters. Tell the user how
500
+ to run their linter to see it fire (e.g. their existing lint script).
501
+
502
+ ## Important Notes
503
+
504
+ - Do not invent linter rules from memory: verify against the repo's
505
+ usage and the tool's current docs.
506
+ - This path is author-only. Taskless does not aggregate or run external
507
+ linters; it writes the rule in the tool's dialect and hands off.
508
+
509
+ ## See Also
510
+
511
+ - \`%(TASKLESS_CLI)s agent route\`: re-decide the destination if this no longer fits
512
+ - \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a local ast-grep rule instead
513
+ - \`%(TASKLESS_CLI)s agent create-remote-rule\`: generate via the service (login)
514
+ `, J = `# Topic: create-remote-rule (CLI v%(CLI_VERSION)s / topic v3)
515
+
516
+ ## You are here
517
+ This is \`create-remote-rule\`. It helps you have the Taskless service
518
+ write a rule, when the rule is beyond what you can express on-device or
519
+ the user has chosen to spend a generation on it.
520
+ If that is not the kind of check you need, re-run \`%(TASKLESS_CLI)s agent route\`
521
+ and follow its decision rather than adapting this recipe.
522
+
523
+ ## Goal
524
+ Enrich the user's description into a request the service can act on,
525
+ submit it, and report what came back. The service generates the rule and
526
+ writes the rule files; your job is everything on either side of that
527
+ call.
528
+
529
+ This recipe is the whole path, the boundary and the procedure. Do not
530
+ go looking for a second topic to perform the submission.
531
+
532
+ ## Preconditions
533
+ - \`.taskless/\` directory exists.
534
+ - The user is logged in. This path requires auth.
535
+ - The project has a GitHub owner. **Check this yourself before collecting
536
+ anything**, see below.
537
+ - The request reached here through \`route\`, or through a local attempt
538
+ that failed and a user who confirmed the escalation.
539
+
540
+ ### Check the GitHub owner first
541
+
542
+ Generation runs against a verifiable org, which comes from a GitHub
543
+ \`origin\` remote. Run:
544
+ \`\`\`
545
+ %(TASKLESS_CLI)s info --json
546
+ \`\`\`
547
+ If \`ghOwner\` is the literal \`[unknown]\`, **stop here**. Do not enrich the
548
+ request, do not collect examples, and do not submit: the call cannot
549
+ succeed, and everything before it is wasted work for the user.
550
+
551
+ Say the tier is unavailable and why, then route to a local recipe:
552
+ \`create-sg-rule\`, \`create-vale-rule\`, or \`create-legacy-rule\` by the
553
+ rule's shape, or \`create-runtime-rule\` if it is only expressible at
554
+ runtime.
555
+
556
+ \`route\` already drops this destination when there is no owner, so
557
+ normally you never arrive here without one. This check is for the times
558
+ that does not hold: reached directly, from a cached plan, or from a copy
559
+ of the routing guidance older than this constraint. An omission upstream
560
+ is advisory; this is the point of use.
561
+
562
+ **\`auth login\` does not fix it.** No GitHub owner is a property of the
563
+ project, not the session, so do not send the user to log in for it. The
564
+ causes are: not a git repository, no \`origin\` remote, an \`origin\` that is
565
+ not GitHub, and git not being installed at all. The first three each have
566
+ their own error code; a missing \`git\` binary cannot be told apart from
567
+ the first, because the probe that would distinguish them is itself a
568
+ \`git\` call, so it reports \`NOT_A_GIT_REPOSITORY\`. If the user says the
569
+ directory *is* a repository, check whether \`git\` is on their \`PATH\`
570
+ before believing the code.
571
+
572
+ ## Cost, and when this path is right
573
+
574
+ A generation costs the user something and requires an account. That is
575
+ why \`route\` prefers a local rule it can write and verify, and why this
576
+ recipe is not a default.
577
+
578
+ Two ways to legitimately be here:
579
+
580
+ - **\`route\` sent you.** Either the rule is not expressible on-device, or
581
+ it is expressible and the user was offered the choice and took it.
582
+ - **A local attempt failed and the user confirmed.** If you came from
583
+ \`create-sg-rule\`'s failure path, that recipe has already deleted the
584
+ broken candidate and asked. If it has not been asked, ask now, a
585
+ silent fall-through from a failed local attempt to a paid service call
586
+ is not acceptable, even when the service would do better.
587
+
588
+ ## Steps
589
+
590
+ 1. **Confirm auth.** Run:
591
+ \`\`\`
592
+ %(TASKLESS_CLI)s info --json
593
+ \`\`\`
594
+ Check \`loggedIn\`. If false, fetch \`%(TASKLESS_CLI)s agent auth\` and follow the
595
+ login recipe before continuing. Do not build a request you cannot
596
+ submit.
597
+
598
+ 2. **Check for a rule that already covers this.** Scan
599
+ \`.taskless/rules/sg/\` and read each rule's \`message\`, \`note\`, and
600
+ \`rule\` fields. If one overlaps, show the user and ask whether they
601
+ would rather iterate on it, \`%(TASKLESS_CLI)s agent improve-rule\` refines an
602
+ existing rule and is usually the better answer than a second rule
603
+ that half-overlaps the first.
604
+
605
+ 3. **Gather the request.** Even when the user supplied a description,
606
+ ask:
607
+ - What exact pattern should be flagged? Get concrete examples.
608
+ - In what language?
609
+ - Where is the pattern acceptable: are there contexts to exclude?
610
+
611
+ If you arrived from a failed local attempt, reuse the cases you
612
+ already gathered rather than asking again.
613
+
614
+ 4. **Enrich it.** This is the part that decides the quality of what
615
+ comes back:
616
+ - Search the codebase for real instances and show the user what you
617
+ found. Real matches beat invented ones.
618
+ - Ask for further success and failure cases, especially near-misses.
619
+ - Read \`.gitignore\`, the linter configs, and \`tsconfig.json\`'s
620
+ \`exclude\` for exclusion patterns the repo already believes in.
621
+ Present them as proposed defaults rather than applying them.
622
+ - Infer the primary language from the codebase and confirm it. Put it
623
+ in the \`prompt\` field.
624
+
625
+ 5. **Confirm before spending the generation.** Summarize the full
626
+ prompt, the success cases, the failure cases, and the exclusions.
627
+ This is the last point at which a misunderstanding is free.
628
+
629
+ 6. **Write the payload** to \`.taskless/.tmp-rule-request.json\`, matching
630
+ the input schema below.
631
+
632
+ 7. **Submit.** Run:
633
+ \`\`\`
634
+ %(TASKLESS_CLI)s rule create --from .taskless/.tmp-rule-request.json --json
635
+ \`\`\`
636
+ This may take 30-60 seconds while the service generates the rule.
637
+
638
+ 8. **Clean up.** Delete \`.taskless/.tmp-rule-request.json\` whether the
639
+ call succeeded or failed.
640
+
641
+ 9. **Report.** The service writes the rule to
642
+ \`.taskless/rules/sg/<id>/<id>.yml\` and its tests to
643
+ \`.taskless/rules/sg/<id>/.tests/<id>-YYYYMMDD-test.yml\`. These are
644
+ the same paths and the same shape a locally authored rule uses, so
645
+ \`check\`, \`improve-rule\`, \`verify\`, and \`test\` treat them
646
+ identically. Nothing is written under \`.taskless/rule-metadata/\`.
647
+ Show the user the paths and suggest \`%(TASKLESS_CLI)s agent check\`.
648
+
649
+ **Record the \`ruleId\` from the \`--json\` output.** It is the ticket
650
+ id the iterate endpoint is addressed by, \`improve-rule\` asks for it,
651
+ and no file on disk carries it.
652
+
653
+ **Read \`notices\` if it is present.** It is an optional array of
654
+ advisory messages about a delivery that was written anyway. A rule
655
+ arriving with no \`.tests/\` fixtures is the one to expect. That rule works, but
656
+ nothing exercises it: \`test\` reports a pass over zero cases, which
657
+ reads exactly like a rule proven to work. Surface any notice to the
658
+ user rather than reporting an unqualified success. Under \`--json\`
659
+ these do not go to stderr, so the envelope is the only place they
660
+ appear.
661
+
662
+ ## Input schema
663
+
664
+ The \`--from\` JSON file conforms to:
665
+
666
+ \`\`\`json
667
+ %(INPUT_SCHEMA)s
668
+ \`\`\`
669
+
670
+ Each entry in \`successCases\` and \`failureCases\` is a separate string.
671
+ Multi-line code goes in one string with literal newlines.
672
+
673
+ ## Important Notes
674
+
675
+ - Do NOT pre-build the rule yourself here. Submit the request and let
676
+ the service generate it; a hand-written rule attached to a generation
677
+ request is neither reviewed nor used.
678
+ - The service owns rule-type selection. Today it generates ast-grep
679
+ rules under \`.taskless/rules/sg/\`, the same shape \`create-sg-rule\`
680
+ produces locally.
681
+
682
+ ## Errors
683
+
684
+ With \`--json\`, failures emit \`{ ok: false, code, message }\`:
685
+
686
+ | code | meaning | fix |
687
+ |--------------------------|---------------------------------|----------------------------------------------|
688
+ | \`AUTH_REQUIRED\` | not logged in | fetch \`%(TASKLESS_CLI)s agent auth\` |
689
+ | \`NO_GITHUB_REMOTE\` | no GitHub origin remote | retained for compatibility; treat as below |
690
+ | \`NOT_A_GIT_REPOSITORY\` | not a git repository | route to a local recipe; \`auth login\` cannot fix it |
691
+ | \`NO_ORIGIN_REMOTE\` | git repository, no \`origin\` | route to a local recipe; \`auth login\` cannot fix it |
692
+ | \`UNSUPPORTED_REMOTE_HOST\`| \`origin\` is not GitHub | route to a local recipe; \`auth login\` cannot fix it |
693
+ | \`INVALID_INPUT\` | \`--from\` JSON failed validation | re-read the input schema, fix, retry |
694
+ | \`NETWORK_ERROR\` | submit/poll failed | report and suggest retry |
695
+ | \`RULE_GENERATION_FAILED\` | the service failed to generate | report the message; suggest enriching prompt |
696
+ | \`RULE_UNSUPPORTED\` | plan lacks this generation type | tell the user to enable it; do not retry |
697
+
698
+ ## See Also
699
+
700
+ - \`%(TASKLESS_CLI)s agent route\`: the routing decision that leads here
701
+ - \`%(TASKLESS_CLI)s agent auth\`: log in before generating
702
+ - \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a rule locally instead
703
+ - \`%(TASKLESS_CLI)s agent improve-rule\`: iterate on a rule that already exists
704
+ - \`%(TASKLESS_CLI)s agent check\`: validate the generated rule
705
+ `, Q = `# Topic: create-runtime-rule (CLI v%(CLI_VERSION)s / topic v2)
706
+
707
+ ## You are here
708
+ This is \`create-runtime-rule\`. It helps you write a runtime rule: a
709
+ check that runs your own code, because answering it needs more than one
710
+ file, the repository graph, git metadata, build output, a resolved
711
+ config chain.
712
+ If that is not the kind of check you need, re-run \`%(TASKLESS_CLI)s agent route\`
713
+ and follow its decision rather than adapting this recipe.
714
+
715
+ You are reading this recipe rather than \`create-remote-rule\` because the
716
+ user is **not logged in**. That is the whole difference, and this recipe
717
+ explains it once.
718
+
719
+ ## Goal
720
+ Explain what a runtime rule is, why this tier is gated when the static
721
+ tiers are not, and what the user has to do before one can run.
722
+
723
+ ## Preconditions
724
+ - \`.taskless/\` directory exists.
725
+ - The user is **not** logged in. If \`%(TASKLESS_CLI)s info --json\` reports
726
+ \`loggedIn: true\`, you are in the wrong recipe, re-run
727
+ \`%(TASKLESS_CLI)s agent route\`.
728
+
729
+ ## What a runtime rule is
730
+
731
+ A rule directory under \`.taskless/rules/runtime/<name>/\` holding two
732
+ kinds of file:
733
+
734
+ | Path | Role |
735
+ |------------------|---------------------------------------------------------------------------|
736
+ | \`captures/*.yml\` | ast-grep capture rules that narrow which files the check looks at |
737
+ | \`check.ts\` | a module whose default export receives those matches and returns findings |
738
+ | \`.tests/\` | fixtures, in \`pass/\` and \`fail/\` buckets |
739
+
740
+ A fixture case is a DIRECTORY, not a file: \`.tests/pass/case-1/\`, and its
741
+ path is the \`root\` the harness hands the check, with every \`match.file\`
742
+ relative to it. So a case may hold as many files as the case needs.
743
+
744
+ That is not a detail. A runtime rule exists because its evidence spans
745
+ more than one file, so a layout allowing one file per case could not
746
+ express the rules the tier is for. \`pass/case-1/src/a.ts\` alongside
747
+ \`pass/case-1/src/b.ts\` is the ordinary shape, not the exotic one.
748
+
749
+ \`pass\`/\`fail\` rather than \`valid\`/\`invalid\`, matching Vale's buckets.
750
+ \`valid\`/\`invalid\` are key names inside an ast-grep test YAML and are
751
+ right for \`sg\` for one reason only: that file is handed to \`sg test\`
752
+ verbatim. Nothing hands a runtime fixture to ast-grep, so borrowing the
753
+ word here would make it mean two things.
754
+
755
+ \`verify\` fails a runtime rule with no capture rule in \`captures/\`,
756
+ because \`check.ts\` would then never be invoked.
757
+
758
+ The capture rules do the cheap work, if nothing matches, \`check.ts\` is
759
+ never invoked. \`check.ts\` does the part no static engine can: hold two
760
+ files at once, read git, resolve an alias, look outside the repository.
761
+
762
+ That is also exactly why it is gated.
763
+
764
+ ## Why this tier is gated
765
+
766
+ **Because it executes code, not because of what it can express.**
767
+
768
+ \`sg\` and \`vale\` rules are inert data. Whatever is in them, the worst a
769
+ malicious rule achieves is a wrong finding, \`%(TASKLESS_CLI)s check\` runs them
770
+ with no login, no network, and nothing to verify, because there is
771
+ nothing to verify.
772
+
773
+ A runtime rule's \`check.ts\` is a program that runs on the developer's
774
+ machine with the developer's permissions. A rule file that arrives in a
775
+ pull request is code that arrives in a pull request. So \`check\`:
776
+
777
+ 1. **Signs** each \`check.ts\`, a signature over the exact bytes on disk.
778
+ 2. **Reconciles** those signatures with the Taskless service, which
779
+ answers with the set it will vouch for.
780
+ 3. **Runs only what came back blessed.** Anything else is reported as
781
+ skipped, with the reason.
782
+
783
+ Reconciliation is what makes the signature mean something: it is the
784
+ service saying "this rule, these exact bytes, for this repository."
785
+ Without a login there is nobody to ask, so there is no answer, so
786
+ nothing runs.
787
+
788
+ This is a property of executing code. It says nothing about whether a
789
+ runtime rule is more or less capable, and it is not a quality tier:
790
+ \`sg\` and \`vale\` rules are not less trusted, they are unexecuted.
791
+
792
+ ## What happens today, logged out
793
+
794
+ Nothing breaks. \`%(TASKLESS_CLI)s check\` still runs every static rule; each
795
+ runtime rule it finds is listed as skipped with the reason
796
+ \`not authenticated, runtime rules were not verified and did not run\`.
797
+
798
+ So a runtime rule you write now is inert until the user logs in. Say
799
+ that plainly rather than letting them discover it from a silent check.
800
+
801
+ ## Testing a rule you wrote yourself needs the flag
802
+
803
+ \`%(TASKLESS_CLI)s test\` runs a runtime rule's fixtures by executing its
804
+ \`check.ts\` against each case directory. The flag is the whole gate, for
805
+ every rule, always:
806
+
807
+ \`\`\`
808
+ %(TASKLESS_CLI)s test .taskless/rules/runtime/<name> --dangerously-run-scripts
809
+ \`\`\`
810
+
811
+ **\`test\` does not talk to the rule service at all**, and there is no
812
+ blessed-signature path that runs fixtures without the flag. That makes
813
+ it stricter than \`check\`, which will execute a blessed rule with no flag
814
+ because it runs rules as a side effect of scanning a repository. \`test\`
815
+ runs them because you asked it to, so the verb is the consent and the
816
+ flag is the confirmation; nothing is asked of a server in between.
817
+
818
+ **A locally authored rule has no signature and never will**, so this was
819
+ never going to be avoidable for a rule you wrote. Blessing is recording:
820
+ a signature is blessed because a reconcile reported it and the service
821
+ recorded it, and nothing recorded a rule that has not left the working
822
+ tree. Do not send anyone to \`auth login\` over this. It changes nothing
823
+ here, and the message does not mention it.
824
+
825
+ The friction is the correct friction. \`sg\` and \`vale\` authors do not
826
+ face it because their rules are unexecuted. A delivered rule and one you
827
+ wrote a minute ago are indistinguishable on disk, so an exemption for
828
+ "my own rule" would be an exemption for any rule, and the warning the
829
+ flag prints is accurate in both cases.
830
+
831
+ Without the flag \`test\` prints \`○\` for the rule, says the fixtures did
832
+ not run, names the flag, and exits 0. That is neither a pass nor a
833
+ failure, and it is deliberately not a failure: nothing the holder of an
834
+ unblessed rule can do would make a failure green.
835
+
836
+ Both buckets have to be populated for the rule to pass, and every case
837
+ has to match at least one capture. A case the captures do not match
838
+ never reaches \`check.ts\`, and is reported as a defect in the case rather
839
+ than in the rule, in \`pass/\` as well as \`fail/\`: such a case proves the
840
+ capture did not match, which is a fact about the fixture.
841
+
842
+ ## Steps
843
+
844
+ 1. **Tell the user what their rule needs, and why it is gated.** Name
845
+ the evidence, "this has to compare two files", "this reads git
846
+ history", and then the consequence: it has to run code, so it needs
847
+ an account. Do not present this as a limitation of the rule.
848
+
849
+ 2. **Point them at login.** Obtaining access is \`auth\`'s job, and this
850
+ recipe deliberately does not restate it:
851
+
852
+ \`\`\`
853
+ %(TASKLESS_CLI)s agent auth
854
+ \`\`\`
855
+
856
+ Follow that recipe with the user. If they do not want an account,
857
+ stop here, say the rule cannot run without one, and offer to
858
+ reconsider whether a narrower version of the request could be
859
+ answered by a static rule instead. That is a new routing decision,
860
+ not a fallback you take on their behalf.
861
+
862
+ 3. **Once they are logged in, re-route.** Run
863
+ \`%(TASKLESS_CLI)s info --json\` to confirm \`loggedIn: true\`, then
864
+ re-run \`%(TASKLESS_CLI)s agent route\` with the original request. The
865
+ destination changes now that the gate is open.
866
+
867
+ ## Important Notes
868
+
869
+ - \`--dangerously-run-scripts\` makes \`check\` and \`test\` execute runtime
870
+ rules without server verification. It exists for local iteration on a
871
+ rule you wrote yourself and just read, which includes running its
872
+ fixtures. On \`test\` it is the only mechanism there is. It is not a way
873
+ to ship a rule to a team, and suggesting it to work around a login
874
+ turns a deliberate gate into an unreviewed code-execution path on
875
+ someone else's machine.
876
+ - Do not author a \`check.ts\` and leave it in the repository unmentioned.
877
+ A skipped runtime rule reports nothing, which reads exactly like a
878
+ passing one.
879
+
880
+ ## See Also
881
+
882
+ - \`%(TASKLESS_CLI)s agent auth\`: log in, log out, check status
883
+ - \`%(TASKLESS_CLI)s agent route\`: re-decide once the login state changes
884
+ - \`%(TASKLESS_CLI)s agent check\`: see which rules ran and which were skipped
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)
886
+
887
+ ## Goal
888
+ Remove a rule and its associated test files from \`.taskless/\`. Does not
889
+ contact the Taskless API; purely a local filesystem operation.
890
+
891
+ \`rule delete\` takes a bare id, not a path, and resolves which engine
892
+ holds it: it searches \`.taskless/rules/sg/\`, \`vale/\` and \`runtime/\`.
893
+ Deleting the directory by hand instead skips the metadata sidecar the
894
+ CLI also removes, and skips the ambiguity check below.
895
+
896
+ An id is not unique across engines. Nothing enforces uniqueness, so two
897
+ engines can hold the same id, and the CLI refuses that case rather than
898
+ picking one.
899
+
900
+ ## Preconditions
901
+ - \`.taskless/\` directory exists.
902
+ - The target rule directory exists under \`.taskless/rules/<engine>/<id>/\`
903
+ for exactly one engine.
904
+ - No auth required.
905
+
906
+ ## Steps
907
+
908
+ 1. **Identify the rule.** If the user named one, use it. Otherwise,
909
+ list the engine directories under \`.taskless/rules/\` and ask which
910
+ rule. Confirm the user's intent, deletion is destructive.
911
+
912
+ 2. **Invoke the CLI.** Run:
913
+ \`\`\`
914
+ %(TASKLESS_CLI)s rule delete <id>
915
+ \`\`\`
916
+
917
+ The CLI removes:
918
+ - \`.taskless/rules/<engine>/<id>/\`: the whole directory, which holds
919
+ the rule, its \`.tests/\`, and any per-rule config
920
+ - \`.taskless/rule-metadata/<id>.yml\`, a sidecar the CLI never
921
+ writes. The removal is there for a directory a person created by
922
+ hand; expect it to be absent.
923
+
924
+ 3. **Report results.** Show the user what was deleted.
925
+
926
+ ## Errors
927
+
928
+ When \`--json\` is set, failures emit \`{ ok: false, code, message }\` on
929
+ stdout; on success the command exits 0 silently (no envelope).
930
+
931
+ | code | meaning | fix |
932
+ |---------------------|----------------------------------------|----------------------------------------------|
933
+ | \`RULE_NOT_FOUND\` | No engine holds that id | Confirm the ID; list rules first |
934
+ | \`RULE_ID_AMBIGUOUS\` | Two engines hold it, so nothing was deleted | Remove the one you mean by path; the message names both |
935
+
936
+ \`RULE_ID_AMBIGUOUS\` is not a retry. The id is well-formed and correct, and it
937
+ matches more than one rule, so sending it again produces the same answer.
938
+ Nothing is deleted when it is reported: the command refuses rather than picking
939
+ one, because picking one removed a rule the caller may not have meant and
940
+ reported success.
941
+
942
+ The rule ID is required as a positional argument; citty rejects a
943
+ missing ID before the command body runs, so \`INVALID_INPUT\` is not
944
+ emitted from this command.
945
+
946
+ ## See Also
947
+
948
+ - \`%(TASKLESS_CLI)s agent route\`: make a new rule
949
+ - \`%(TASKLESS_CLI)s agent check\`: run remaining rules to confirm nothing broke
950
+ `, te = `# Topic: detect (CLI v%(CLI_VERSION)s / topic v1)
951
+
952
+ ## Goal
953
+ Scan the working directory for the linters it configures, the
954
+ languages it uses, and the styles of any rules the repo already
955
+ authors. Offline and deterministic, no network, no auth, no state
956
+ change. This is the discovery step that feeds rule-authoring: the
957
+ routing flow reads \`detect\` to decide where a new rule should live.
958
+
959
+ ## Preconditions
960
+ - None. Works in any directory; doesn't require \`.taskless/\`.
961
+
962
+ ## Steps
963
+
964
+ 1. **Invoke the CLI** with JSON output:
965
+ \`\`\`
966
+ %(TASKLESS_CLI)s detect --json
967
+ \`\`\`
968
+
969
+ 2. **Parse the response.** Shape:
970
+ \`\`\`json
971
+ {
972
+ "success": true,
973
+ "linters": [
974
+ {
975
+ "name": "eslint",
976
+ "evidence": ["eslint.config.js", "dependency eslint (package.json)"]
977
+ }
978
+ ],
979
+ "languages": ["typescript", "javascript"],
980
+ "ruleStyles": [
981
+ {
982
+ "source": ".taskless/rules/sg",
983
+ "description": "ast-grep rules with YAML metadata sidecars"
984
+ }
985
+ ]
986
+ }
987
+ \`\`\`
988
+ - \`linters\`: each has a \`name\` and \`evidence\` (config-file paths,
989
+ a pyproject table marker, or a dependency marker from the
990
+ language's package file; not every entry is a path).
991
+ - \`languages\`: inferred from manifests and the detected linters.
992
+ - \`ruleStyles\`: how the repo authors its own rules, surfaced for
993
+ downstream reuse.
994
+
995
+ 3. **Use the signals to route.** Feed the output into rule authoring:
996
+ - A detected linter the repo already uses → author the rule there
997
+ (\`%(TASKLESS_CLI)s agent route\`).
998
+ - No suitable linter, local-only → \`%(TASKLESS_CLI)s agent create-sg-rule\`.
999
+ - See \`%(TASKLESS_CLI)s agent route\` for the full decision.
1000
+
1001
+ ## Errors
1002
+
1003
+ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
1004
+
1005
+ | code | meaning | fix |
1006
+ |------------------|----------------------------|--------------------------|
1007
+ | \`INTERNAL_ERROR\` | Internal schema validation | Report; likely a CLI bug |
1008
+
1009
+ ## See Also
1010
+
1011
+ - \`%(TASKLESS_CLI)s agent route\`: decide where to author a rule from these signals
1012
+ - \`%(TASKLESS_CLI)s agent check\`: run rules against the codebase
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)
1156
+
1157
+ ## Goal
1158
+ Iterate on an existing ast-grep rule **locally** without contacting
1159
+ the Taskless API. You (the agent) edit the rule YAML directly, then
1160
+ validate with \`verify\` and \`test\` in a feedback loop.
1161
+
1162
+ ## Preconditions
1163
+ - \`.taskless/\` directory exists and contains the target rule.
1164
+ - The agent can read/write files and run shell commands.
1165
+ - No auth required.
1166
+
1167
+ ## Steps
1168
+
1169
+ 1. **Identify the target rule.** If the user named one, use it.
1170
+ Otherwise, list \`.taskless/rules/sg/\` and ask which one. Read the
1171
+ rule file and any test file under
1172
+ \`.taskless/rules/sg/<id>/.tests/\`.
1173
+
1174
+ 2. **Gather improvement guidance.** Ask:
1175
+ - Are there false positives (cases currently flagged but
1176
+ shouldn't be)? Get concrete examples.
1177
+ - Are there false negatives (cases passing but should be
1178
+ caught)? Get concrete examples.
1179
+ - Should the message, severity, or note change?
1180
+
1181
+ 3. **Plan the change.** Decide whether to:
1182
+ - Tighten the \`rule\` (add \`inside\`, \`not\`, more specific \`kind\`)
1183
+ - Loosen the \`rule\` (relax constraints, add \`any\` alternatives)
1184
+ - Add \`ignores\` patterns for files/paths to skip
1185
+ - Adjust \`message\`/\`severity\`/\`note\`
1186
+ Summarize the planned change before editing.
1187
+
1188
+ 4. **Edit the rule file.** Make the change directly in
1189
+ \`.taskless/rules/sg/<id>/<id>.yml\`. Preserve the existing \`id\`,
1190
+ \`language\`, and \`severity\` unless the user explicitly asked to
1191
+ change them.
1192
+
1193
+ 5. **Update test cases.** Add the new false-positive examples to the
1194
+ \`valid:\` list and the new false-negative examples to the
1195
+ \`invalid:\` list in \`.taskless/rules/sg/<id>/.tests/<id>-*.yml\`.
1196
+
1197
+ 6. **Run the verify feedback loop.** Run:
1198
+ \`\`\`
1199
+ %(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json
1200
+ \`\`\`
1201
+ - If \`success: true\`: report success.
1202
+ - If \`success: false\`: read the per-layer errors, fix, re-run.
1203
+ Repeat up to 3 times. After 3 failed attempts, report to the
1204
+ user with the latest errors and ask for guidance.
1205
+
1206
+ Common fixes:
1207
+ - New tests fail → the rule still doesn't catch the case;
1208
+ refine the pattern.
1209
+ - Old tests broke → the new rule is too aggressive; add an
1210
+ \`ignores\` clause or constrain \`kind\`.
1211
+ - Schema errors → the YAML structure drifted; check fields.
1212
+
1213
+ 7. **Report results.** Show:
1214
+ - The updated rule file path
1215
+ - The updated test file path
1216
+ - A diff-style summary of what changed
1217
+ Suggest fetching \`%(TASKLESS_CLI)s agent check\` to validate against the
1218
+ broader codebase.
1219
+
1220
+ ## Important Notes
1221
+
1222
+ - Rules created in anonymous mode have no metadata sidecar: that's
1223
+ fine. This recipe doesn't need or write metadata.
1224
+ - If the rule was originally created via the API path (has metadata),
1225
+ you can still use this anonymous recipe to iterate locally. The
1226
+ metadata sidecar is preserved untouched.
1227
+ - Do NOT make any HTTP requests to taskless.io.
1228
+
1229
+ ## Errors
1230
+
1231
+ The verify primitive returns structured errors per layer:
1232
+
1233
+ | layer | what failure means | fix |
1234
+ |----------------|------------------------------------|---------------------------------------|
1235
+ | \`schema\` | YAML doesn't match ast-grep schema | Fix rule structure |
1236
+ | \`requirements\` | Missing Taskless-required field | Add \`id\`/\`language\`/\`severity\`/etc. |
1237
+ | \`tests\` | A test case behaved unexpectedly | Fix the rule pattern OR the test case |
1238
+
1239
+ ## See Also
1240
+
1241
+ - \`%(TASKLESS_CLI)s agent improve-rule\`: API-backed flow (auth required)
1242
+ - \`%(TASKLESS_CLI)s agent create-sg-rule\`: make a new rule locally
1243
+ - \`%(TASKLESS_CLI)s agent check\`: validate the updated rule
1244
+ `, ie = `# Topic: improve-rule (CLI v%(CLI_VERSION)s / topic v5)
1245
+
1246
+ ## Goal
1247
+ Iterate on an existing Taskless rule. The CLI submits the user's
1248
+ guidance to the Taskless API iterate endpoint, which returns an
1249
+ updated rule that overwrites the original on disk. The agent's job
1250
+ is to gather the right ruleId + guidance + supporting references and
1251
+ to report the result.
1252
+
1253
+ If the user wants the local-only flow (no API call), fetch
1254
+ \`%(TASKLESS_CLI)s agent improve-rule --anonymous\` instead.
1255
+
1256
+ ## Preconditions
1257
+ - User is logged in.
1258
+ - The project has a GitHub owner. Improvement runs through the same
1259
+ service path as generation, so it carries the same constraint. Check
1260
+ \`ghOwner\` from \`%(TASKLESS_CLI)s info --json\`: if it is the literal
1261
+ \`[unknown]\`, stop and say the tier is unavailable rather than
1262
+ submitting. \`auth login\` does not fix it, no GitHub owner is a
1263
+ property of the project, not the session.
1264
+ - The target rule exists at \`.taskless/rules/sg/<id>/<id>.yml\`.
1265
+ - You have the rule's **ticket id**: the value \`%(TASKLESS_CLI)s rule
1266
+ create --json\` printed as \`ruleId\` when the rule was generated. The
1267
+ iterate endpoint is addressed by that id. Nothing on disk holds it,
1268
+ so it comes from the create output or from the user. Without it,
1269
+ fetch the anonymous variant instead.
1270
+
1271
+ ## Steps
1272
+
1273
+ 1. **Confirm auth.** Run \`%(TASKLESS_CLI)s info --json\` and check
1274
+ \`loggedIn\`. If false, fetch \`%(TASKLESS_CLI)s agent auth\`.
1275
+
1276
+ 2. **Identify the rule to improve.** If the user named one, use it.
1277
+ Otherwise, list rules in \`.taskless/rules/sg/\` and ask which one.
1278
+ Read the existing rule file so you can summarize what it does.
1279
+
1280
+ 3. **Get the ticket id.** This is the id the iterate endpoint is
1281
+ addressed by, and it is the \`ruleId\` field from that rule's
1282
+ \`%(TASKLESS_CLI)s rule create --json\` output. Take it from the
1283
+ session that created the rule, or ask the user for it.
1284
+
1285
+ **Do not run \`%(TASKLESS_CLI)s rule meta <id>\` to get it.** That
1286
+ command reads \`.taskless/rule-metadata/<id>.yml\`, a sidecar this CLI
1287
+ never writes, so it exits 1 with \`RULE_META_UNAVAILABLE\` for every
1288
+ rule. If no one has the ticket id, fetch
1289
+ \`%(TASKLESS_CLI)s agent improve-rule --anonymous\` and iterate
1290
+ locally.
1291
+
1292
+ 4. **Gather improvement guidance.** Ask the user what should change:
1293
+ - Are there false positives we need to exclude?
1294
+ - Are there missed cases we need to catch?
1295
+ - Is the message confusing or misleading?
1296
+ - Should the severity change?
1297
+
1298
+ **"It doesn't fire on X" is often the \`$$$\` separator, not a pattern
1299
+ that is merely too narrow.** Read the rule's pattern for a \`$$$\`
1300
+ sitting next to a comma before you write that guidance. The comma is
1301
+ itself an AST node that has to match, so \`f($A, $$$)\` never sees a
1302
+ one-argument call and \`f($$$, $A)\` sees only one-argument calls, the
1303
+ pattern reads as variadic and is not. \`%(TASKLESS_CLI)s agent create-sg-rule\`
1304
+ carries the arity table and both remedies. Guidance that asks to
1305
+ widen the rule when the fix is \`strictness: ast\` sends the iterate
1306
+ endpoint after the wrong change, and it will oblige.
1307
+
1308
+ 5. **Collect supporting references.** Ask the user for any code
1309
+ examples that should be:
1310
+ - **Not flagged** (currently flagged but shouldn't be): false
1311
+ positive references.
1312
+ - **Flagged** (currently passing but should be caught): false
1313
+ negative references.
1314
+
1315
+ Each reference is \`{ filename: string, content: string }\`. Multiple
1316
+ references are an array.
1317
+
1318
+ 6. **Confirm the request.** Summarize the rule, the guidance, and
1319
+ the references before submitting.
1320
+
1321
+ 7. **Write the JSON payload.** Build a JSON object matching the input
1322
+ schema below and write it to \`.taskless/.tmp-improve-request.json\`.
1323
+
1324
+ 8. **Invoke the CLI.** Run:
1325
+ \`\`\`
1326
+ %(TASKLESS_CLI)s rule improve --from .taskless/.tmp-improve-request.json --json
1327
+ \`\`\`
1328
+ This may take 30-60 seconds while the API generates the update.
1329
+
1330
+ 9. **Clean up.** Delete \`.taskless/.tmp-improve-request.json\`
1331
+ regardless of success or failure.
1332
+
1333
+ 10. **Report results.** The CLI overwrites the rule file (and its
1334
+ test file) with the updated version. Show the file paths and a
1335
+ summary of what changed. Suggest fetching \`%(TASKLESS_CLI)s agent check\`
1336
+ to validate.
1337
+
1338
+ **Read \`notices\` if it is present.** It is an optional array of
1339
+ advisory messages about a delivery that was written anyway. A rule
1340
+ arriving with no \`.tests/\` fixtures is the one to expect. Surface any notice
1341
+ rather than reporting an unqualified success. Under \`--json\` these
1342
+ do not go to stderr, so the envelope is the only place they appear.
1343
+
1344
+ **Confirm both fixture buckets are still non-empty.** \`test\` fails
1345
+ an ast-grep rule whose \`valid:\` or \`invalid:\` array is empty, and
1346
+ narrowing a pattern to kill a false positive is exactly how
1347
+ \`invalid:\` ends up with nothing the rule still matches. Run
1348
+ \`\`\`
1349
+ %(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json
1350
+ \`\`\`
1351
+ on the returned files rather than trusting them, and add a fixture
1352
+ the new pattern does match if that bucket came back empty.
1353
+
1354
+ ## Input schema
1355
+
1356
+ The \`--from\` JSON file conforms to:
1357
+
1358
+ \`\`\`json
1359
+ %(INPUT_SCHEMA)s
1360
+ \`\`\`
1361
+
1362
+ \`ruleId\` is the original rule's ticket ID, printed as \`ruleId\` by
1363
+ \`%(TASKLESS_CLI)s rule create --json\`. It is not the YAML file name,
1364
+ and it is not readable from anything under \`.taskless/\`.
1365
+
1366
+ ## Errors
1367
+
1368
+ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
1369
+
1370
+ | code | meaning | fix |
1371
+ |--------------------------|-------------------------------------|-----------------------------------------------|
1372
+ | \`AUTH_REQUIRED\` | not logged in | fetch \`%(TASKLESS_CLI)s agent auth\` |
1373
+ | \`NO_GITHUB_REMOTE\` | no GitHub origin remote | retained for compatibility; treat as below |
1374
+ | \`NOT_A_GIT_REPOSITORY\` | not a git repository | tell the user; \`auth login\` cannot fix it |
1375
+ | \`NO_ORIGIN_REMOTE\` | git repository, no \`origin\` | tell the user; \`auth login\` cannot fix it |
1376
+ | \`UNSUPPORTED_REMOTE_HOST\`| \`origin\` is not GitHub | tell the user; \`auth login\` cannot fix it |
1377
+ | \`INVALID_INPUT\` | \`--from\` JSON failed validation | re-read input schema, fix, retry |
1378
+ | \`RULE_NOT_FOUND\` | the service has no such ticket id | re-check the id from \`rule create --json\` |
1379
+ | \`NETWORK_ERROR\` | API submit/poll failed | report and suggest retry |
1380
+ | \`RULE_GENERATION_FAILED\` | API returned a generation failure | report; suggest enriching guidance/references |
1381
+ | \`RULE_UNSUPPORTED\` | plan lacks this generation type | tell the user to enable it; do not retry |
1382
+
1383
+ ## See Also
1384
+
1385
+ - \`%(TASKLESS_CLI)s agent improve-rule --anonymous\`: local-only flow
1386
+ - \`%(TASKLESS_CLI)s agent route\`: make a new rule from scratch
1387
+ - \`%(TASKLESS_CLI)s agent check\`: validate the updated rule
1388
+ `, re = `# Topic: info (CLI v%(CLI_VERSION)s / topic v1)
1389
+
1390
+ ## Goal
1391
+ Report local Taskless state: CLI version, installed skill versions
1392
+ per detected tool, and (unless \`--anonymous\`) the user's auth state.
1393
+ Used as a health check, for staleness detection, and to confirm
1394
+ which version of the CLI/skills the agent is talking to.
1395
+
1396
+ ## Preconditions
1397
+ - None. Works in any directory; doesn't require \`.taskless/\`.
1398
+
1399
+ ## Steps
1400
+
1401
+ 1. **Invoke the CLI** with JSON output:
1402
+ \`\`\`
1403
+ %(TASKLESS_CLI)s info --json
1404
+ \`\`\`
1405
+ For an offline/local-only state report (no auth probe), pass
1406
+ \`--anonymous\`:
1407
+ \`\`\`
1408
+ %(TASKLESS_CLI)s info --json --anonymous
1409
+ \`\`\`
1410
+
1411
+ 2. **Parse the response.** Shape:
1412
+ \`\`\`json
1413
+ {
1414
+ "success": true,
1415
+ "version": "0.7.0",
1416
+ "tools": [
1417
+ {
1418
+ "name": "Claude Code",
1419
+ "skills": [
1420
+ { "name": "taskless", "installedVersion": "0.7.0",
1421
+ "currentVersion": "0.7.0", "current": true }
1422
+ ]
1423
+ }
1424
+ ],
1425
+ "loggedIn": true,
1426
+ "auth": { "user": "...", "email": "...", "orgs": ["..."] }
1427
+ }
1428
+ \`\`\`
1429
+
1430
+ 3. **Report to the user.** Summarize:
1431
+ - CLI version
1432
+ - For each tool: number of installed skills, count out-of-date
1433
+ - Auth: logged in as <user> (orgs) OR not logged in
1434
+
1435
+ 4. **Suggest reinit on staleness.** If any skill has \`current: false\`,
1436
+ suggest \`%(TASKLESS_CLI)s\` to reinstall and pull the latest
1437
+ bundle.
1438
+
1439
+ ## Errors
1440
+
1441
+ When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
1442
+
1443
+ | code | meaning | fix |
1444
+ |------------------|----------------------------|--------------------------|
1445
+ | \`INTERNAL_ERROR\` | Internal schema validation | Report; likely a CLI bug |
1446
+
1447
+ (Network errors during the auth probe are silently swallowed:
1448
+ \`info\` falls back to reporting \`loggedIn: false\` rather than
1449
+ failing.)
1450
+
1451
+ ## See Also
1452
+
1453
+ - \`%(TASKLESS_CLI)s agent auth\`: log in / log out / status detail
1454
+ - \`%(TASKLESS_CLI)s agent check\`: run rules against the codebase
1455
+ `, le = `# Topic: init (CLI v%(CLI_VERSION)s / topic v2)
1456
+
1457
+ ## Goal
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.
1467
+
1468
+ ## Preconditions
1469
+ - None at the project level. The command works in any directory and
1470
+ bootstraps \`.taskless/\` on first run.
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.
1474
+
1475
+ ## Steps
1476
+
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.
1553
+
1554
+ ## Errors
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.
1560
+ - Wizard cancelled (Ctrl-C) → no filesystem writes. Re-run when ready.
1561
+
1562
+ ## See Also
1563
+
1564
+ - \`%(TASKLESS_CLI)s update\`: what an upgrade changed for existing rules
1565
+ - \`%(TASKLESS_CLI)s agent info\`: verify what's installed and check staleness
1566
+ - \`%(TASKLESS_CLI)s agent auth\`: authenticate after installing
1567
+ `, he = `# Topic: onboard (CLI v%(CLI_VERSION)s / topic v3)
1568
+
1569
+ ## Goal
1570
+ Help a user who has just installed Taskless go from zero rules to a
1571
+ useful starter set by mining their codebase, agent-memory files, PR
1572
+ review history, and issue tracker for high-signal rule candidates.
1573
+ This is a conversational discovery flow, not a script, the agent
1574
+ collaborates with the user on what to scan and surfaces hypothetical
1575
+ rules as a bullet list the user can choose to materialize via the
1576
+ \`rule create\` flow.
1577
+
1578
+ ## Preconditions
1579
+ - \`.taskless/\` directory exists (Taskless is installed). The
1580
+ \`%(TASKLESS_CLI)s onboard\` subcommand bootstraps it on first run, so this
1581
+ is automatically satisfied.
1582
+ - A working repository the agent can read.
1583
+ - No auth required to surface candidates. (Materializing a rule via
1584
+ \`rule create\` may require auth. Fetch \`%(TASKLESS_CLI)s agent auth\` if
1585
+ needed at that point.)
1586
+ - The criterion for where a rule belongs lives in
1587
+ \`%(TASKLESS_CLI)s agent route\` and is not restated here. This recipe
1588
+ reads it; it does not duplicate it.
1589
+
1590
+ ## Steps
1591
+
1592
+ 1. **Read the manifest first.** Run
1593
+ \`%(TASKLESS_CLI)s info --json\` and check whether
1594
+ \`.taskless/taskless.json\` reports \`install.onboarded: true\`. The
1595
+ \`%(TASKLESS_CLI)s onboard\` subcommand has already gated on this for you,
1596
+ but if the user is invoking the recipe directly via the skill,
1597
+ confirm with the user before running a long discovery pass.
1598
+
1599
+ 2. **Learn the routing surface before you propose anything.** A
1600
+ candidate this repository cannot express is not a candidate, and
1601
+ you cannot tell which is which until you know two things:
1602
+
1603
+ \`\`\`
1604
+ %(TASKLESS_CLI)s agent route
1605
+ %(TASKLESS_CLI)s detect --json
1606
+ \`\`\`
1607
+
1608
+ Read \`route\` for the destination criterion, which rules are
1609
+ decided by one file's syntax tree, by a document's words, by a
1610
+ tool the repo already runs, or by evidence outside the files.
1611
+ Read \`detect --json\` for what this repository actually has:
1612
+ \`linters\`, \`languages\`, and \`ruleStyles\`.
1613
+
1614
+ Do this before you synthesize the bullet list, not after.
1615
+ Proposing first and routing afterwards is how a bullet list ends
1616
+ up carrying candidates that nothing can enforce. You are not
1617
+ choosing a destination command here. That happens per accepted
1618
+ bullet when you offer materialization.
1619
+
1620
+ 3. **Open the conversation about sources.** Tell the user you can mine
1621
+ several places for rule candidates and ask which ones they want to
1622
+ include. Default sources you should always offer:
1623
+
1624
+ - **Codebase TODOs / FIXMEs**: search for \`TODO\`, \`FIXME\`, \`XXX\`,
1625
+ and \`HACK\` comments. Many of these are latent rules ("don't do
1626
+ this", "remove when X").
1627
+ - **Agent-memory files**: read \`CLAUDE.md\`, \`AGENTS.md\`,
1628
+ \`.cursorrules\`, \`.opencode/AGENTS.md\`, and similar agent-context
1629
+ files for explicit rules and conventions stated in prose.
1630
+ - **Recent PR review comments**: only if the \`gh\` CLI is
1631
+ available. Probe with \`command -v gh\`. Suggest scanning the last
1632
+ 30 days of merged PRs for repeated reviewer feedback patterns.
1633
+ - **Issue tracker tickets**: only if a relevant MCP is wired in
1634
+ (Linear, Jira, GitHub issues via \`gh issue list\`, etc.). Use
1635
+ whatever issue-tracker tools you have available.
1636
+
1637
+ Then explicitly ask: "Are there other places I should look, a
1638
+ team wiki, an internal docs site, a specific design doc, a Slack
1639
+ channel export?" The user often knows about sources you don't.
1640
+
1641
+ 4. **Probe tool availability before promising a scan.** For each
1642
+ source the user picks, verify the tool exists before committing
1643
+ to it. Don't tell the user "I'll scan PR comments" if \`gh\` isn't
1644
+ installed, say "PR comments need the GitHub CLI, or equivalent; want me to skip
1645
+ this or wait while you install these tools?"
1646
+
1647
+ 5. **Scan with high-signal filtering.** For each chosen source:
1648
+
1649
+ - **Filter for repeated patterns.** A reviewer comment that
1650
+ appears across multiple PRs is a much stronger rule candidate
1651
+ than a one-off nit.
1652
+ - **Prefer comments that cite a doc or style guide.** "Per our
1653
+ style guide, no direct DB calls in controllers" is a rule.
1654
+ "looks good" is not.
1655
+ - **Prefer merge-blocking feedback.** Comments that gated a PR
1656
+ merge are higher-signal than passing remarks.
1657
+ - **Filter out one-off nits and pure formatting.** Linters and
1658
+ formatters already handle those; Taskless rules earn their
1659
+ keep on semantic patterns.
1660
+
1661
+ 6. **Synthesize the bullet list.** Present findings as a single
1662
+ bullet list the user can scan quickly. One bullet per
1663
+ hypothetical rule, in this format:
1664
+
1665
+ \`\`\`
1666
+ - <kebab-case-name> [<destination>]: <one-line description of what it would enforce>
1667
+ \`\`\`
1668
+
1669
+ \`<destination>\` is the routing call you reached when you learned
1670
+ the routing surface, one of
1671
+ \`legacy\`, \`sg\`, \`vale\`, or \`runtime\`. Example:
1672
+
1673
+ \`\`\`
1674
+ - no-direct-db-access [sg]: Prevent controllers from importing the DB module directly; route through the service layer.
1675
+ - require-zod-validation [sg]: Flag API handlers that read request bodies without parsing them through a Zod schema first.
1676
+ - comments-record-not-forecast [vale]: Flag comments that promise future work instead of recording what is true now.
1677
+ - exports-must-be-used [runtime]: Flag exported symbols nothing in the repo imports.
1678
+ \`\`\`
1679
+
1680
+ The annotation is provisional. It tells the user what kind of rule
1681
+ this would be, and it is why an unroutable candidate never reaches
1682
+ the list. \`route\` adjudicates for real at materialization time, and
1683
+ may land somewhere else once it has the rule's full description.
1684
+
1685
+ Order the bullets by your judgment of impact (frequency in the
1686
+ source × severity of the issue × ease of enforcement). Don't
1687
+ inflate the list, three high-quality candidates beat ten
1688
+ speculative ones.
1689
+
1690
+ 7. **Offer materialization per bullet.** For each bullet, ask
1691
+ whether the user wants to turn it into a real Taskless rule. On
1692
+ yes, follow the \`%(TASKLESS_CLI)s agent route\` recipe you already
1693
+ fetched when you learned the routing surface, the user's accepted
1694
+ bullet becomes the rule description input. Re-fetch it only if it
1695
+ has fallen out of context.
1696
+
1697
+ 8. **Ask before marking onboarding complete.** When the user signals
1698
+ they're done (they've materialized everything they want, or
1699
+ they've said "that's enough for now"), explicitly ask: "Do you
1700
+ want me to mark Taskless as onboarded? You won't be re-asked to
1701
+ onboard until you pass \`--force\`." On explicit yes, and only
1702
+ on explicit yes, run:
1703
+
1704
+ \`\`\`
1705
+ %(TASKLESS_CLI)s onboard --mark-complete
1706
+ \`\`\`
1707
+
1708
+ Do NOT mark onboarding complete on your own initiative. Do NOT
1709
+ run \`--mark-complete\` if the user is ambiguous, says "maybe
1710
+ later", or stops responding. The flag is consent-gated.
1711
+
1712
+ ## Errors
1713
+
1714
+ | code | meaning | fix |
1715
+ |---------------------|----------------------------------------------|--------------------------------------|
1716
+ | \`ALREADY_ONBOARDED\` | \`install.onboarded\` is true and no \`--force\` | suggest \`--force\` or skip onboarding |
1717
+
1718
+ ## See Also
1719
+
1720
+ - \`%(TASKLESS_CLI)s agent route\`: the destination criterion; read it
1721
+ before proposing candidates, and follow it to materialize an accepted one
1722
+ - \`%(TASKLESS_CLI)s agent detect\`: what this repository already lints
1723
+ and authors, which bounds what a candidate can be
1724
+ - \`%(TASKLESS_CLI)s agent check\`: validate newly created rules against the codebase
1725
+ - \`%(TASKLESS_CLI)s agent info\`: inspect the current \`.taskless/taskless.json\` state
1726
+ `, ce = `# Topic: route (CLI v%(CLI_VERSION)s / topic v5)
1727
+
1728
+ ## Goal
1729
+ Turn "write me a rule that…" into one command to run. This is the front
1730
+ door for every rule-authoring request, and it makes one decision (which
1731
+ of five recipes authors this rule) from one reading of the evidence.
1732
+
1733
+ That single decision covers both halves that used to be asked
1734
+ separately: whether the rule can be built here, and which engine can
1735
+ express it. They are answered from the same signals, so they are
1736
+ answered together.
1737
+
1738
+ ## Preconditions
1739
+ - A working repository the agent can read.
1740
+ - No auth required to route. Login state is an *input* to the decision,
1741
+ not a requirement of making it.
1742
+
1743
+ ## Steps
1744
+
1745
+ 1. **Read the repository.** %(DETECT_EVIDENCE)s the configured linters, languages, and the repo's own
1746
+ rule styles. It is deterministic and offline. Use it as ground truth
1747
+ instead of guessing the repo's tooling. The scan is monorepo-aware, so
1748
+ evidence may carry a sub-package path. The output shape:
1749
+ \`\`\`json
1750
+ {
1751
+ "success": true,
1752
+ "linters": [{ "name": "eslint", "evidence": ["packages/api/.eslintrc.json"] }],
1753
+ "languages": ["JavaScript", "TypeScript"],
1754
+ "ruleStyles": [
1755
+ { "source": ".taskless/rules/sg", "description": "Existing Taskless ast-grep rules." }
1756
+ ]
1757
+ }
1758
+ \`\`\`
1759
+
1760
+ 2. **Read the login state and the repository context.** %(LOGIN_EVIDENCE)s \`loggedIn\` and \`ghOwner\`. Do this now, not later: they change
1761
+ which destinations exist, so classifying first means classifying
1762
+ against a set that may be wrong.
1763
+
1764
+ \`ghOwner\` is the GitHub owner this project belongs to, or the literal
1765
+ \`[unknown]\` when there is none to find: not a git repository, no
1766
+ \`origin\` remote, an \`origin\` that is not GitHub, or no git installed.
1767
+ Read it from this payload rather than running \`git\` yourself, so the
1768
+ destinations you offer match what the CLI will actually allow.
1769
+
1770
+ Reading it is not the same as asking about it. Do **not** open by
1771
+ offering service generation. At this point neither you nor the user
1772
+ knows whether this is a two-line pattern or something local authoring
1773
+ cannot express, so the question costs a turn and cannot be answered
1774
+ well.
1775
+
1776
+ 3. **State the evidence before you name a destination.** Write one
1777
+ sentence: *"to decide this, you must look at ___."* Then a short
1778
+ rationale covering what \`detect\` showed, whether a linter the repo
1779
+ already runs could express this, and what the rule needs to read.
1780
+
1781
+ Do this first, every time. Naming a destination and justifying it
1782
+ afterwards is how prose-about-code ends up in \`sg\` and cross-file
1783
+ questions end up in \`vale\`.
1784
+
1785
+ 4. **Match the evidence to a destination.** The comparison is made here
1786
+ and only here. The destination recipes describe their own scope and
1787
+ deliberately do not restate this table.
1788
+
1789
+ | The rule is decided by… | Destination | Login |
1790
+ |------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|------------|
1791
+ | a tool the repo already runs, in that tool's dialect | \`create-legacy-rule\` | no |
1792
+ | **one file's syntax tree**: a call, an import, a JSX attribute, a type annotation | \`create-sg-rule\` | no |
1793
+ | **a document's words**: docs, README, comments, commit bodies | \`create-vale-rule\` | no |
1794
+ | **more than one file, or something outside the files**: the repo graph, git metadata, build output, a resolved config chain | \`create-runtime-rule\` / \`create-remote-rule\` | see step 5 |
1795
+
1796
+ Sharpening the three engine rows, because most wrong answers are one
1797
+ of these:
1798
+
1799
+ - **Relational correlation inside one file is still \`sg\`.** "A
1800
+ \`useEffect\` whose dependency array omits a value used in its body"
1801
+ is one file's tree. If exactly one file settles it, it is \`sg\`.
1802
+ - **Prose about code is still prose.** "Comments must not say
1803
+ 'TBD'" is \`vale\`. The evidence is the words. "Every exported
1804
+ function has a doc comment" is \`sg\`. The evidence is whether a node
1805
+ exists above a declaration. Ask what you would have to *read* to
1806
+ decide, not what the subject matter is.
1807
+ - **Vale sees one document at a time.** "This term is spelled
1808
+ consistently ACROSS the docs directory" is a graph question, so it
1809
+ is runtime, even though it is entirely about prose.
1810
+ - **Trust tier is not a destination.** \`sg\` and \`vale\` are both
1811
+ static-tier: inert data, always run, no login, no reconcile, no
1812
+ signing. Only runtime executes code. "Static vs runtime" is a
1813
+ different axis from "which engine", and conflating them is what
1814
+ makes a prose rule look like it needs an account.
1815
+
1816
+ **What each local engine can actually read.** Both lists are rendered
1817
+ from the pinned engine versions rather than written out here, so they
1818
+ cannot go stale against the binaries: if one looks wrong, an engine
1819
+ was bumped and a vendor-contract test is already red.
1820
+
1821
+ - **ast-grep (v%(AST_GREP_VERSION)s) parses:** %(AST_GREP_LANGUAGES)s.
1822
+
1823
+ Those spellings are ast-grep's own and go into a rule's \`language:\`
1824
+ field verbatim. Nothing local validates that field, the vendored
1825
+ schema types it as a bare string with no enum, so the first thing
1826
+ with an opinion is the binary. It accepts some off-list aliases
1827
+ (\`C++\` and \`cpp\` both reach the Cpp parser), but a name it does not
1828
+ know at all, like \`C#\` for \`CSharp\`, aborts config parsing and
1829
+ takes every other rule's report down with it. Copy from the list.
1830
+
1831
+ **A GitHub Actions workflow is \`Yaml\`, and \`Yaml\` is on that list.**
1832
+ A rule about \`.github/workflows/*.yml\` is an \`sg\` rule; sending it
1833
+ to runtime spends a login on a check that builds here.
1834
+
1835
+ **\`Markdown\` is on that list, and it is narrower than the name
1836
+ suggests.** tree-sitter-markdown splits its grammar in two, block
1837
+ and inline, and ast-grep exposes only the block tree. Measured at
1838
+ v%(AST_GREP_VERSION)s against a document holding a heading, a
1839
+ subheading, a list, a fenced block and a link:
1840
+
1841
+ - *Block structure works.* \`document\`, \`section\`, \`atx_heading\`,
1842
+ \`setext_heading\`, \`fenced_code_block\`, \`list_item\` and
1843
+ \`paragraph\` all match, and headings discriminate by level:
1844
+ \`# $T\` matches only the h1, \`## $T\` only the h2.
1845
+ - *Everything inside a line does not.* A paragraph's contents are
1846
+ one opaque \`inline\` node. There is no \`link\` node, no \`emphasis\`,
1847
+ no \`strong_emphasis\`. \`kind: link\` does not quietly find nothing:
1848
+ it is a config error (\`Kind 'link' is invalid\`) that exits 8 and
1849
+ takes every other rule's report down with it, the same shape as
1850
+ \`C#\` above. The quiet version is the pattern form, where
1851
+ \`[$T]($U)\` parses, runs, and matches nothing forever.
1852
+ - So "no bare URLs", "link text must not read 'click here'", "no
1853
+ bold inside a heading" feel structural and are not \`sg\` rules.
1854
+ The evidence is words inside a line, which is Vale's.
1855
+
1856
+ **Neither static engine counts, and neither sees absence.**
1857
+ ast-grep fires once per match, \`not\` scopes to a node rather than
1858
+ to a document, and there is no count assertion. "Every doc has an
1859
+ h1" and "at most one h1" are runtime however structural they look.
1860
+
1861
+ **\`.md\` is the first extension both static engines claim, so do
1862
+ not route it by extension.** \`sg\` answers "does this block-level
1863
+ shape occur in this file"; Vale answers "do these words appear in
1864
+ this file's prose". Fences, heading levels and list shape are \`sg\`.
1865
+ The wording inside a heading, a list item or a link is Vale.
1866
+
1867
+ - **Vale (v%(VALE_VERSION)s) reads three tiers, and hard-fails on a fourth.**
1868
+ The tier is decided by the file's extension:
1869
+
1870
+ - *markup*: the whole document is prose, and the format's own
1871
+ non-prose constructs are skipped:
1872
+ %(VALE_MARKUP_FORMATS)s
1873
+ - *comments only*: the comment text is linted and the code body is
1874
+ invisible:
1875
+ %(VALE_COMMENT_FORMATS)s
1876
+ - *plaintext fallback*: everything else, \`.yml\` \`.toml\` \`.sh\`
1877
+ \`.sql\` and every unnamed extension included. There is no parser,
1878
+ so the file is linted as one block of prose, and a Vale rule
1879
+ scoped to YAML flags the code as readily as the comments. That is
1880
+ rarely what was asked for. Say so before writing it. These read
1881
+ like markup and are not: %(VALE_PLAINTEXT_FORMATS)s
1882
+ - *not supported*: Vale parses these only by shelling out to an
1883
+ external program, and this build does not support any format that
1884
+ needs one:
1885
+ %(VALE_CONVERTER_FORMATS)s
1886
+
1887
+ Installing the program does not change this. Taskless excludes
1888
+ these files from the Vale run whatever is on the machine, so that
1889
+ a repository checks the same way everywhere rather than depending
1890
+ on what a given host happens to have available.
1891
+
1892
+ **One such file fails the entire Vale pass, not just that file.**
1893
+ Vale exits 2 with an \`E100\` runtime error, \`--no-exit\` does not
1894
+ suppress it, and every other Vale rule over every other file goes
1895
+ unreported. A matcher written as \`[*.{md,typ}]\` is not a wider
1896
+ \`[*.md]\`. It is a broken one.
1897
+
1898
+ **\`.mdx\` is supported** as of Vale v3.18.0, which parses it
1899
+ natively. It needs no external program and belongs with the
1900
+ other markup formats above. \`.typ\` moved the other way in the
1901
+ same release: Typst now parses through \`typst2vast\`, so a Typst
1902
+ file is excluded rather than read as prose the way it was
1903
+ before. Both are measured, not assumed.
1904
+
1905
+ As of v3.19.0 an MDX component's children are read as Markdown
1906
+ too, so prose wrapped in \`<Steps>\` or \`<Aside>\` is linted where
1907
+ it previously was not. That widens what a Vale rule covers in
1908
+ \`.mdx\`; it does not change which extensions are safe to match.
1909
+
1910
+ **A language on neither list does not route to runtime by default.**
1911
+ Check \`create-legacy-rule\` first: the repo may already run a linter
1912
+ that speaks it, and that linter's own dialect is a local destination
1913
+ with no login. Escalate only once nothing local can see the evidence.
1914
+
1915
+ Worked examples:
1916
+
1917
+ | Rule intent | Evidence needed | Destination |
1918
+ |----------------------------------------------------------|-------------------------------------------|--------------------|
1919
+ | No \`eval(...)\` anywhere | one file's call expressions | \`create-sg-rule\` |
1920
+ | Actions workflows must pin actions to a SHA | one workflow file's tree (\`Yaml\`) | \`create-sg-rule\` |
1921
+ | \`useEffect\` deps must include what the body reads | one file's tree, correlated within it | \`create-sg-rule\` |
1922
+ | Don't write "TBD" or "coming soon" in docs | a document's words | \`create-vale-rule\` |
1923
+ | Comments must not say "for now" | a document's words (comments are prose) | \`create-vale-rule\` |
1924
+ | Headings use sentence case | a document's markup | \`create-vale-rule\` |
1925
+ | Every code fence declares a language | one \`.md\` file's block tree | \`create-sg-rule\` |
1926
+ | Docs must not use setext (\`===\`) headings | one \`.md\` file's block tree | \`create-sg-rule\` |
1927
+ | Link text must not read "click here" | words inside a line (no inline tree) | \`create-vale-rule\` |
1928
+ | Every doc has exactly one h1 | a count over a document | runtime |
1929
+ | Exported symbols must be used somewhere in the repo | every file, correlated | runtime |
1930
+ | Product name spelled the same across all docs | many documents, compared | runtime |
1931
+ | Files changed in the last release need a changelog entry | git metadata, not file contents | runtime |
1932
+ | Imports must resolve through the tsconfig path aliases | config chain resolution, outside the file | runtime |
1933
+
1934
+ 5. **Split the runtime row on login state.** Runtime rules execute code,
1935
+ so they run only against a server-verified signature:
1936
+ - **logged in** → \`create-remote-rule\`. Go there directly; no recipe
1937
+ forwards to another.
1938
+ - **logged out** → \`create-runtime-rule\`, which explains the gate.
1939
+
1940
+ With \`ghOwner\` of \`[unknown]\`, \`create-remote-rule\` is not reachable
1941
+ at all: see step 6.
1942
+
1943
+ 6. **Drop remote generation when \`ghOwner\` is \`[unknown]\`.** Generation
1944
+ runs against a verifiable org, which comes from a GitHub \`origin\`.
1945
+ Without one the tier cannot complete, so it is not a destination and
1946
+ must not be presented as one. Route to the local recipe the rule's
1947
+ shape calls for, and if the rule is only expressible at runtime, go
1948
+ to \`create-runtime-rule\`.
1949
+
1950
+ Say the tier is unavailable and why, in one line. A reader who is not
1951
+ told will read the omission as an oversight and ask for it, which
1952
+ costs a turn and ends where this step already is.
1953
+
1954
+ This is **not fixed by \`auth login\`**. It is a property of the
1955
+ project, not the session, so do not send them there for it.
1956
+
1957
+ Everything else still works: local authoring, \`verify\`, \`test\` and
1958
+ \`check\` need no remote of any kind.
1959
+
1960
+ 7. **Offer the service only where it is a genuine choice.** When the
1961
+ rule is expressible locally, the user is logged in, **and** \`ghOwner\`
1962
+ is not \`[unknown]\`, present both and let them pick: a local rule you
1963
+ write and verify now, or a generation that costs against their plan.
1964
+ Say which you would choose and why.
1965
+
1966
+ Not logged in, no GitHub owner, or not locally expressible are not
1967
+ choices. Do not pose them as one.
1968
+
1969
+ 8. **Name the command.** Finish by telling the user, or running, the
1970
+ exact fetch for the destination you chose:
1971
+ \`\`\`
1972
+ %(TASKLESS_CLI)s agent create-vale-rule
1973
+ \`\`\`
1974
+ A destination that is not a runnable command is a category, and a
1975
+ category is not an answer.
1976
+
1977
+ ## When two destinations both fit
1978
+
1979
+ - **A linter the repo already runs AND a local Taskless rule both fit** →
1980
+ do not silently pick. Present both with trade-offs and let the user
1981
+ choose.
1982
+ - **A rule that seems to need two engines is usually two rules.** Split
1983
+ it and route each half, rather than forcing one engine to cover
1984
+ evidence it cannot see.
1985
+ - **You genuinely cannot tell** → ask the user a clarifying question.
1986
+ Uncertainty is a reason to ask; it is never a reason to spend a
1987
+ generation.
1988
+
1989
+ ## Failure fallback (try-verify-escalate)
1990
+
1991
+ If you committed to a local rule on reasonable confidence and it then
1992
+ fails against the user's cases:
1993
+
1994
+ - Tell the user the local rule could not capture them.
1995
+ - State that generating via the service uses a generation and requires
1996
+ login.
1997
+ - Call the service only after the user confirms. On yes, fetch
1998
+ \`%(TASKLESS_CLI)s agent create-remote-rule\`.
1999
+
2000
+ Never fall through silently from a failed local attempt to a service
2001
+ call. A developer who watches a local attempt fail reads it as a
2002
+ Taskless failure, so only attempt local when you reasonably believe it
2003
+ will work.
2004
+
2005
+ ## Important Notes
2006
+
2007
+ - **Reason first, route second.** Do not name a destination before the
2008
+ rationale in step 3 is written.
2009
+ - **What the code actually looks like outranks how the request was
2010
+ phrased.** Where you can see the code or the diff, weigh the concrete
2011
+ syntactic form present in the repository above the wording. The same
2012
+ sentence routes to \`create-sg-rule\` when the repository contains a
2013
+ form that correlates statically, and to runtime when the equivalent
2014
+ check would have to normalize a captured value against a declaration
2015
+ elsewhere.
2016
+ - **Engine reach and engine availability are two different questions.**
2017
+ Step 4's lists say what each engine can *parse*; they say nothing
2018
+ about whether the binary resolved on this host. A language on the list
2019
+ is still unusable where the platform build is missing.
2020
+ - **When it stays ambiguous, choose an engine whose availability you can
2021
+ assert, and say that is why.** There is no fixed fallback. \`sg\` and
2022
+ \`vale\` both ship as platform binaries, so either can be the missing one
2023
+ on an unsupported architecture or where an install was blocked;
2024
+ server-side the constraint is different again. A named default is
2025
+ wrong in whichever situation it failed to anticipate.
2026
+ - **Stay local when you reasonably can.** Reasonable confidence is
2027
+ enough to commit to a local rule, the failure fallback backstops a
2028
+ wrong-but-reasonable bet.
2029
+
2030
+ ## See Also
2031
+
2032
+ - \`%(TASKLESS_CLI)s agent create-legacy-rule\`: author in a linter the repo already uses
2033
+ - \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a local ast-grep rule (no login)
2034
+ - \`%(TASKLESS_CLI)s agent create-vale-rule\`: author a local Vale rule (no login)
2035
+ - \`%(TASKLESS_CLI)s agent create-runtime-rule\`: the runtime tier, logged out
2036
+ - \`%(TASKLESS_CLI)s agent create-remote-rule\`: generate via the service (login)
2037
+ - \`%(TASKLESS_CLI)s agent check\`: run every engine over the repo
2038
+ `, de = `# Topic: rule-meta (CLI v%(CLI_VERSION)s / topic v3)
2039
+
2040
+ ## Goal
2041
+ Report what \`%(TASKLESS_CLI)s rule meta\` does today, so no recipe and no
2042
+ agent builds a step on top of it.
2043
+
2044
+ ## The sidecar does not exist
2045
+
2046
+ \`.taskless/rule-metadata/<id>.yml\` is written from the \`meta\` block of a
2047
+ rule status response. The rule service does not populate that block, so
2048
+ this CLI has never written a sidecar for any rule, under any tier, in
2049
+ any mode. \`rule meta <id>\` therefore has nothing to read and exits 1
2050
+ with \`RULE_META_UNAVAILABLE\` for every id, including ids whose rule is
2051
+ plainly on disk.
2052
+
2053
+ That code exists to keep the two cases apart. \`RULE_NOT_FOUND\` invites a
2054
+ retry with a different id; there is no id that works.
2055
+
2056
+ ## What to do instead
2057
+
2058
+ \`rule improve\` needs the ticket id, and that id comes from the machine
2059
+ that created the rule, not from disk:
2060
+
2061
+ - \`%(TASKLESS_CLI)s rule create --json\` prints it as \`ruleId\` on
2062
+ success. Record it when you create a rule you expect to iterate on.
2063
+ - If the id was not recorded, ask the user for it.
2064
+ - If nobody has it, iterate locally: fetch
2065
+ \`%(TASKLESS_CLI)s agent improve-rule --anonymous\`.
2066
+
2067
+ ## Errors
2068
+
2069
+ | code | meaning | fix |
2070
+ |-------------------------|------------------------------------------|---------------------------------------|
2071
+ | \`RULE_META_UNAVAILABLE\` | no sidecar exists, and none is written | Use the ticket id from \`rule create\` |
2072
+ | \`INVALID_INPUT\` | a sidecar exists and is malformed | Delete it; nothing here depends on it |
2073
+
2074
+ ## See Also
2075
+
2076
+ - \`%(TASKLESS_CLI)s agent improve-rule\`: how the ticket id is actually sourced
2077
+ `, ue = `# Topic: rule (CLI v%(CLI_VERSION)s / topic v2)
2078
+
2079
+ ## Goal
2080
+ Umbrella for rule operations. Fetch the topic for the action you want.
2081
+
2082
+ ## Topics
2083
+
2084
+ | Action | Recipe |
2085
+ |--------------------|-------------------------------------------------------|
2086
+ | Create a rule | \`%(TASKLESS_CLI)s agent route\` |
2087
+ | Improve a rule | \`%(TASKLESS_CLI)s agent improve-rule\` |
2088
+ | Delete a rule | \`%(TASKLESS_CLI)s agent delete-rule\` |
2089
+ | Verify a rule | \`%(TASKLESS_CLI)s agent verify-rule\` (agent-internal) |
2090
+ | Read rule metadata | \`%(TASKLESS_CLI)s agent rule-meta\` (agent-internal) |
2091
+
2092
+ \`rule meta\` reads a sidecar this CLI never writes, so it fails for every
2093
+ rule. Fetch \`rule-meta\` only to learn what to do instead. \`improve-rule\`
2094
+ takes the ticket id from \`rule create --json\`.
2095
+
2096
+ \`route\` is the entry point for authoring: it reads the request and
2097
+ names the \`create-*-rule\` topic that fits, so you do not pick an engine
2098
+ yourself.
2099
+
2100
+ For the local-only flow on improve, append \`--anonymous\`.
2101
+
2102
+ ## See Also
2103
+
2104
+ - \`%(TASKLESS_CLI)s agent check\`: run all configured rules
2105
+ `, pe = `# Topic: update (CLI v%(CLI_VERSION)s / topic v6)
2106
+
2107
+ ## You are here
2108
+ This is \`update\`. It tells you what an upgrade changed for the rules
2109
+ ALREADY IN THIS PROJECT, and what to do about them.
2110
+
2111
+ It is not about installing. Running \`%(TASKLESS_CLI)s\` migrates the
2112
+ \`.taskless/\` layout and refreshes the installed skills on its own. That
2113
+ handles the DIRECTORY. This recipe handles the RULES, which no migration
2114
+ can rewrite for you: a rewriter that now needs a \`fix\`, a rule whose
2115
+ matching semantics shifted under a new engine, a rule that could now be
2116
+ expressed better with a newly supported language.
2117
+
2118
+ An agent that has run a migration and watched it succeed will otherwise
2119
+ reasonably conclude the upgrade is done. It is not. The directory is
2120
+ migrated; the rules may still need work.
2121
+
2122
+ ## Goal
2123
+ Walk the ledger below from the version this project was last reconciled
2124
+ to, up to the installed CLI, doing what each section says. Then record
2125
+ that you finished.
2126
+
2127
+ ## Preconditions
2128
+ - \`.taskless/\` exists. If it does not, there are no rules to reconcile.
2129
+ - You can read \`.taskless/taskless.json\`.
2130
+
2131
+ ## Steps
2132
+
2133
+ 1. **Find where to start.** Run:
2134
+ \`\`\`
2135
+ %(TASKLESS_CLI)s info --json
2136
+ \`\`\`
2137
+ and read \`rules.reconciledTo\`, plus \`rules.engines.sg\` and
2138
+ \`rules.engines.vale\` for the engine versions the rules were built
2139
+ against.
2140
+
2141
+ **No \`rules.reconciledTo\` at all** means this project PREDATES the
2142
+ ledger, so treat it as \`0.0.0\` and walk every section. It does not
2143
+ mean the project is new: a project this CLI set up has the field
2144
+ stamped at creation, so an absent marker is the one case where none
2145
+ of the entries below have ever been applied.
2146
+
2147
+ \`rules.walk\` on that same payload is this decision already made:
2148
+ \`{ "from": ..., "to": ... }\` when there is something to walk, and
2149
+ \`null\` when there is not. Prefer it to re-deriving the boundary
2150
+ yourself, so the offer and the enforcement cannot disagree.
2151
+
2152
+ 2. **Walk forward, in order.** Start at the section after
2153
+ \`reconciledTo\` and continue to the installed version. Sections are
2154
+ cumulative, never replaceable: a later one can depend on an earlier
2155
+ one having run, so do not skip ahead to the newest.
2156
+
2157
+ 3. **Do the work each section names.** Each says what changed and what
2158
+ it means for existing rules. A section that says there is nothing to
2159
+ do means exactly that; it is a claim, not an oversight.
2160
+
2161
+ 4. **Record that you finished.** Run:
2162
+ \`\`\`
2163
+ %(TASKLESS_CLI)s update --rules
2164
+ \`\`\`
2165
+ Only after the walk is complete. There is no version to pass: the CLI
2166
+ stamps its own, because the only sensible endpoint of a walk is the
2167
+ version you are running.
2168
+
2169
+ A partial or abandoned walk must not be recorded. The next agent
2170
+ starts from what you wrote and skips everything you did not do, and
2171
+ it will look like there was nothing to do. Nothing can check this for
2172
+ you: the CLI refuses to move the marker backwards, but it cannot tell
2173
+ whether you read the sections.
2174
+
2175
+ This also records the engine versions the rules are now valid
2176
+ against, which is what lets a later upgrade say what moved.
2177
+
2178
+ ## The ledger
2179
+
2180
+ ### Migrating to 0.11.x
2181
+
2182
+ The vendored ast-grep moves from 0.41.0 to 0.45.2, and Vale from 3.18.0
2183
+ to 3.20.0. Nine things follow for existing rules.
2184
+
2185
+ **Elixir stopped being linted as prose.** Vale 3.19.0 reads \`.ex\` and
2186
+ \`.exs\` through a real parser, so it now sees comments and \`@doc\`
2187
+ attributes and nothing else. On 3.18.0 those files had no parser and
2188
+ were linted whole, so a Vale rule matching \`[*.ex]\` fired on function
2189
+ names, atoms, and string literals as well as on comments.
2190
+
2191
+ Those findings are gone, and nothing errors or warns. A rule that
2192
+ reported on Elixir code bodies now reports less, which is a drop you
2193
+ would otherwise notice only as a suspiciously clean run. If the rule was
2194
+ meant for prose, this is the behaviour you always wanted. If it was
2195
+ being used to catch something in the code itself, that is a job for an
2196
+ \`sg\` rule, and \`%(TASKLESS_CLI)s rule create\` will route it there.
2197
+
2198
+ **MDX components' children are prose now, and new findings appear.**
2199
+ As of Vale 3.19.0 a JSX element's children are read as the Markdown
2200
+ they are, so a rule over \`.mdx\` covers prose inside a wrapping
2201
+ component such as \`<Steps>\` or \`<Aside>\` that it previously skipped.
2202
+ Those children also carry the component name as a \`text.class.<name>\`
2203
+ scope, which a rule can target.
2204
+
2205
+ This is the opposite direction from Elixir: coverage grew, so a rule
2206
+ reports MORE than it did. Read the new findings as real, and narrow the
2207
+ rule's \`scope\` only if the component's prose was deliberately out of
2208
+ reach.
2209
+
2210
+ **An exception zone that did nothing now takes effect, and findings
2211
+ disappear.** Vale 3.20.0 records the region each \`<!-- vale <id>.<id> =
2212
+ NO -->\` and \`= YES\` pair covers, and suppresses any alert located
2213
+ inside it. Through 3.19.0 an *inline* pair (at a list item's
2214
+ continuation indent, with no blank line between it and the prose it
2215
+ wraps) was read once per block, so the two halves cancelled out and the
2216
+ zone did nothing at all.
2217
+
2218
+ So a marker already sitting in this project's documents was inert and
2219
+ is now live. Nothing errors: findings a document has reported for as
2220
+ long as the rule existed stop appearing, which reads as a rule that
2221
+ broke. Before treating a drop as a regression, look for the
2222
+ markers: \`git grep -n '<!-- vale'\`. Keep the zone if it was meant; if
2223
+ it was a half-finished experiment, delete the pair and the coverage
2224
+ comes back.
2225
+
2226
+ **A \`raw\`-scoped rule can be exempted in a document now.** Directives
2227
+ reach \`raw\` rules as of 3.20.0. Before, they were applied to the parsed
2228
+ document only and a \`raw\` rule fired straight through every zone, so a
2229
+ rule about a shell command, a flag, or a package name could only be
2230
+ removed, never exempted case by case. A rule that was narrowed or
2231
+ abandoned for that reason is worth revisiting.
2232
+
2233
+ One caveat comes with it: at \`raw\` scope the directive line is itself
2234
+ linted text, so a rule whose token appears in its own id reports a
2235
+ finding on the marker that silences it. Rename the rule, or scope it.
2236
+
2237
+ **\`sequence\` accepts \`exceptions\`.** The field is new in 3.20.0 and
2238
+ purely additive: no existing rule changes behaviour, and a sequence
2239
+ rule that was over-firing on a known phrase can now carry it. Upstream
2240
+ also changed how a negated sequence token is satisfied at a sentence
2241
+ boundary. No shape we tried reproduced a difference between 3.19.0 and
2242
+ 3.20.0, so there is nothing to do unless you see one.
2243
+
2244
+ **A rewriter now requires \`fix\`.** \`SerializableRewriter.required\` goes
2245
+ from \`["rule","id"]\` to \`["id","fix","rule"]\`, so a \`rewriters:\` entry
2246
+ without a \`fix:\` is now rejected where it was accepted before.
2247
+
2248
+ Run \`%(TASKLESS_CLI)s verify\` and it names the offending rewriter
2249
+ directly. It cannot be auto-fixed: \`fix\` is replacement text, which is
2250
+ authorial intent. A rewriter with no \`fix\` could never have done
2251
+ anything, so this surfaces a rule that was already dead rather than
2252
+ breaking one that worked.
2253
+
2254
+ **Markdown is now a language, with a narrow shape.** \`language:
2255
+ Markdown\` parses. What it sees is the BLOCK tree only: \`document\`,
2256
+ \`section\`, \`atx_heading\`, \`setext_heading\`, \`fenced_code_block\`,
2257
+ \`list_item\`, \`paragraph\`. Everything inside a line collapses into one
2258
+ opaque \`inline\` node, so there is no \`link\`, no \`emphasis\`, no
2259
+ \`strong_emphasis\`.
2260
+
2261
+ Two failure shapes, and they are opposites. \`kind: link\` is a HARD
2262
+ CONFIG ERROR: exit 8, \`Kind \\\`link\\\` is invalid\`, which aborts config
2263
+ parsing and takes every other rule's report down with it. A pattern like
2264
+ \`[$T]($U)\` is the quiet one: it parses, runs, exits 0, and matches
2265
+ nothing forever.
2266
+
2267
+ So "no bare URLs" and "link text must not say click here" are not
2268
+ ast-grep rules even now. Use Vale, which reads prose, and note that Vale
2269
+ DOES see YAML frontmatter: with \`scope: raw\` it sees the keys too, and
2270
+ \`extends: occurrence\` with \`min: 1\` can require a field to be present.
2271
+
2272
+ **\`sg run --lang\` accepts the alias spellings.** At 0.41.0 \`--lang C++\`
2273
+ was rejected while \`language: C++\` parsed. At 0.45.2 both are accepted.
2274
+ This affects the flag only, not a rule's \`language:\` field, so no rule
2275
+ file needs changing. It is recorded because the divergence used to be
2276
+ documented as a thing to work around.
2277
+
2278
+ **Matching semantics moved, and a valid, unchanged rule can now match a
2279
+ DIFFERENT SET OF NODES with no error and no warning.** Two shapes are
2280
+ affected. Both were measured against the two binaries, so this section
2281
+ says what to do rather than what to watch for.
2282
+
2283
+ **A rule was silently dead and now fires.** If a rule uses \`nthChild\`
2284
+ with an \`ofRule\` whose body binds a metavariable:
2285
+
2286
+ \`\`\`
2287
+ nthChild:
2288
+ position: 2
2289
+ ofRule: { pattern: $S }
2290
+ \`\`\`
2291
+
2292
+ then at 0.41.0 it matched **nothing**, because \`ofRule\` reused one
2293
+ environment across siblings: the first match committed \`$S\` and every
2294
+ later sibling failed the consistency check and went uncounted. It now
2295
+ counts correctly and reports.
2296
+
2297
+ So the rule is not broken, it was inert, and the findings it produces on
2298
+ this version have never been seen. Run \`%(TASKLESS_CLI)s check\` and read
2299
+ them as new: they are real matches the rule was always meant to make,
2300
+ and they may be numerous on a codebase that has never been checked
2301
+ against it. Fixtures are no help here, since a rule that matched nothing
2302
+ passed the \`pass/\` side of its own tests.
2303
+
2304
+ A rule whose \`ofRule\` used a non-binding matcher, \`kind:\` or \`regex:\`
2305
+ with no metavariable, is unaffected: only binding was broken.
2306
+
2307
+ **A metavariable is now empty where it used to carry a value.** If a
2308
+ rule binds a metavariable inside a negated \`not\` and then references it
2309
+ in \`message\`, \`fix\`, or a constraint:
2310
+
2311
+ \`\`\`
2312
+ follows:
2313
+ not:
2314
+ pattern: return $A
2315
+ stopBy: end
2316
+ message: "found after $A"
2317
+ \`\`\`
2318
+
2319
+ then at 0.41.0 \`$A\` rendered with a value leaked from the candidate the
2320
+ negation REJECTED. A \`not\` contributes no bindings by definition, since
2321
+ a successful negation means the inner rule did not match, so that value
2322
+ was never meaningful. It is now unbound and renders empty.
2323
+
2324
+ This one will not change your finding counts. The match is identical:
2325
+ same file, same range, same rule. Only the rendered output differs, so
2326
+ grep your rules for a metavariable that appears both inside a \`not\` and
2327
+ in a \`message\` or \`fix\`. A \`fix\` in that shape has been writing the
2328
+ leaked text into people's files.
2329
+
2330
+ **Root metavariables and comments** also changed upstream
2331
+ (ast-grep/ast-grep#2868), but no shape we tried reproduced a difference,
2332
+ including the TSX case that PR names. Nothing to do unless you see one.
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
+
2400
+ ## Errors
2401
+
2402
+ With \`--json\`, \`--rules\` failures emit \`{ ok: false, code, message }\`:
2403
+
2404
+ | code | meaning | fix |
2405
+ |-----------------|------------------------------------------------|--------------------------------------------|
2406
+ | \`INVALID_INPUT\` | no \`.taskless/\`, or an older CLI would rewind the marker | set the project up, or upgrade first |
2407
+
2408
+ ## See Also
2409
+
2410
+ - \`%(TASKLESS_CLI)s agent check\`: run every engine over the repo
2411
+ - \`%(TASKLESS_CLI)s agent improve-rule\`: rewrite a rule the walk flagged
2412
+ - \`%(TASKLESS_CLI)s agent info\`: what is installed, and staleness
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) {
2417
+ (function() {
2418
+ var t = {
2419
+ not_type: /[^T]/,
2420
+ not_primitive: /[^v]/,
2421
+ number: /[diefg]/,
2422
+ numeric_arg: /[bcdiefguxX]/,
2423
+ json: /[j]/,
2424
+ text: /^[^\x25]+/,
2425
+ modulo: /^\x25{2}/,
2426
+ placeholder: /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
2427
+ key: /^([a-z_][a-z_\d]*)/i,
2428
+ key_access: /^\.([a-z_][a-z_\d]*)/i,
2429
+ index_access: /^\[(\d+)\]/,
2430
+ sign: /^[+-]/
2431
+ };
2432
+ function s(h) {
2433
+ return l(k(h), arguments);
2434
+ }
2435
+ function o(h, u) {
2436
+ return s.apply(null, [h].concat(u || []));
2437
+ }
2438
+ function l(h, u) {
2439
+ var r = 1, w = h.length, n, p = "", d, c, a, b, S, T, I, y;
2440
+ for (d = 0; d < w; d++)
2441
+ if (typeof h[d] == "string")
2442
+ p += h[d];
2443
+ else if (typeof h[d] == "object") {
2444
+ if (a = h[d], a.keys)
2445
+ for (n = u[r], c = 0; c < a.keys.length; c++) {
2446
+ if (n == null)
2447
+ throw new Error(s('[sprintf] Cannot access property "%s" of undefined value "%s"', a.keys[c], a.keys[c - 1]));
2448
+ n = n[a.keys[c]];
2449
+ }
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) {
2454
+ case "b":
2455
+ n = parseInt(n, 10).toString(2);
2456
+ break;
2457
+ case "c":
2458
+ n = String.fromCharCode(parseInt(n, 10));
2459
+ break;
2460
+ case "d":
2461
+ case "i":
2462
+ n = parseInt(n, 10);
2463
+ break;
2464
+ case "j":
2465
+ n = JSON.stringify(n, null, a.width ? parseInt(a.width) : 0);
2466
+ break;
2467
+ case "e":
2468
+ n = a.precision ? parseFloat(n).toExponential(a.precision) : parseFloat(n).toExponential();
2469
+ break;
2470
+ case "f":
2471
+ n = a.precision ? parseFloat(n).toFixed(a.precision) : parseFloat(n);
2472
+ break;
2473
+ case "g":
2474
+ n = a.precision ? String(Number(n.toPrecision(a.precision))) : parseFloat(n);
2475
+ break;
2476
+ case "o":
2477
+ n = (parseInt(n, 10) >>> 0).toString(8);
2478
+ break;
2479
+ case "s":
2480
+ n = String(n), n = a.precision ? n.substring(0, a.precision) : n;
2481
+ break;
2482
+ case "t":
2483
+ n = String(!!n), n = a.precision ? n.substring(0, a.precision) : n;
2484
+ break;
2485
+ case "T":
2486
+ n = Object.prototype.toString.call(n).slice(8, -1).toLowerCase(), n = a.precision ? n.substring(0, a.precision) : n;
2487
+ break;
2488
+ case "u":
2489
+ n = parseInt(n, 10) >>> 0;
2490
+ break;
2491
+ case "v":
2492
+ n = n.valueOf(), n = a.precision ? n.substring(0, a.precision) : n;
2493
+ break;
2494
+ case "x":
2495
+ n = (parseInt(n, 10) >>> 0).toString(16);
2496
+ break;
2497
+ case "X":
2498
+ n = (parseInt(n, 10) >>> 0).toString(16).toUpperCase();
2499
+ break;
2500
+ }
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);
2502
+ }
2503
+ return p;
2504
+ }
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)
2519
+ for (p.push(c[1]); (d = d.substring(c[0].length)) !== ""; )
2520
+ if ((c = t.key_access.exec(d)) !== null)
2521
+ p.push(c[1]);
2522
+ else if ((c = t.index_access.exec(d)) !== null)
2523
+ p.push(c[1]);
2524
+ else
2525
+ throw new SyntaxError("[sprintf] failed to parse named argument key");
2526
+ else
2527
+ throw new SyntaxError("[sprintf] failed to parse named argument key");
2528
+ r[2] = p;
2529
+ } else
2530
+ n |= 2;
2531
+ if (n === 3)
2532
+ throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");
2533
+ w.push(
2534
+ {
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]
2544
+ }
2545
+ );
2546
+ } else
2547
+ throw new SyntaxError("[sprintf] unexpected placeholder");
2548
+ u = u.substring(r[0].length);
2549
+ }
2550
+ return g[h] = w;
2551
+ }
2552
+ e.sprintf = s, e.vsprintf = o, typeof window < "u" && (window.sprintf = s, window.vsprintf = o);
2553
+ })();
2554
+ })(A)), A;
2555
+ }
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(
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"
2568
+ )
2569
+ });
2570
+ f({
2571
+ error: i().describe("Error message")
2572
+ });
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(
2577
+ f({
2578
+ filename: i().describe("File path relative to .taskless/"),
2579
+ content: i().describe("File content")
2580
+ })
2581
+ ).optional().describe("Reference files to include as context")
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(
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"
2589
+ )
2590
+ });
2591
+ f({
2592
+ error: i().describe("Error message")
2593
+ });
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 = [
2638
+ "Bash",
2639
+ "C",
2640
+ "Cpp",
2641
+ "CSharp",
2642
+ "Css",
2643
+ "Dart",
2644
+ "Elixir",
2645
+ "Go",
2646
+ "Haskell",
2647
+ "Hcl",
2648
+ "Html",
2649
+ "Java",
2650
+ "JavaScript",
2651
+ "Json",
2652
+ "Kotlin",
2653
+ "Lua",
2654
+ "Markdown",
2655
+ "Nix",
2656
+ "Php",
2657
+ "Python",
2658
+ "Ruby",
2659
+ "Rust",
2660
+ "Scala",
2661
+ "Solidity",
2662
+ "Swift",
2663
+ "Tsx",
2664
+ "TypeScript",
2665
+ "Yaml"
2666
+ ], ve = {
2667
+ "c++": "Cpp",
2668
+ cc: "Cpp",
2669
+ cs: "CSharp",
2670
+ cxx: "Cpp",
2671
+ ex: "Elixir",
2672
+ golang: "Go",
2673
+ hs: "Haskell",
2674
+ js: "JavaScript",
2675
+ jsx: "JavaScript",
2676
+ kt: "Kotlin",
2677
+ py: "Python",
2678
+ rb: "Ruby",
2679
+ rs: "Rust",
2680
+ sol: "Solidity",
2681
+ ts: "TypeScript",
2682
+ yml: "Yaml"
2683
+ }, ke = new Map(
2684
+ P.map((e) => [e.toLowerCase(), e])
2685
+ );
2686
+ function an(e) {
2687
+ const t = e.toLowerCase();
2688
+ return ke.get(t) ?? ve[t];
2689
+ }
2690
+ const on = {
2691
+ TypeScript: "ts",
2692
+ Tsx: "tsx"
2693
+ }, Se = "3.21.0", R = "converter:", G = {
2694
+ // markup — parsed, the format's own constructs skipped
2695
+ ".htm": "markup",
2696
+ ".html": "markup",
2697
+ ".ipynb": "markup",
2698
+ ".markdown": "markup",
2699
+ ".md": "markup",
2700
+ ".mdx": "markup",
2701
+ ".mdown": "markup",
2702
+ ".myst": "markup",
2703
+ ".org": "markup",
2704
+ ".qmd": "markup",
2705
+ ".rmd": "markup",
2706
+ ".xhtml": "markup",
2707
+ // comment text only — the code body is invisible
2708
+ ".c": "comment",
2709
+ ".c++": "comment",
2710
+ ".bsh": "comment",
2711
+ ".cc": "comment",
2712
+ ".clj": "comment",
2713
+ ".cpp": "comment",
2714
+ ".cs": "comment",
2715
+ ".css": "comment",
2716
+ ".csx": "comment",
2717
+ ".cxx": "comment",
2718
+ ".ex": "comment",
2719
+ ".exs": "comment",
2720
+ ".go": "comment",
2721
+ ".h": "comment",
2722
+ ".h++": "comment",
2723
+ ".hpp": "comment",
2724
+ ".hs": "comment",
2725
+ ".java": "comment",
2726
+ ".jl": "comment",
2727
+ ".js": "comment",
2728
+ ".jsx": "comment",
2729
+ ".less": "comment",
2730
+ ".lua": "comment",
2731
+ ".php": "comment",
2732
+ ".pl": "comment",
2733
+ ".pm": "comment",
2734
+ ".pod": "comment",
2735
+ ".proto": "comment",
2736
+ ".ps1": "comment",
2737
+ ".qdoc": "comment",
2738
+ ".qml": "comment",
2739
+ ".py": "comment",
2740
+ ".py3": "comment",
2741
+ ".pyw": "comment",
2742
+ ".r": "comment",
2743
+ ".R": "comment",
2744
+ ".rb": "comment",
2745
+ ".rs": "comment",
2746
+ ".sass": "comment",
2747
+ ".scss": "comment",
2748
+ ".sbt": "comment",
2749
+ ".scala": "comment",
2750
+ ".swift": "comment",
2751
+ ".ts": "comment",
2752
+ ".tsx": "comment",
2753
+ // plaintext, and surprising about it — these look parsed and are not
2754
+ ".mkd": "plaintext",
2755
+ ".mkdn": "plaintext",
2756
+ ".tex": "plaintext",
2757
+ // plaintext HERE, though Vale's own docs list it as comment-tier. Measured on
2758
+ // the pinned 3.21.0 binary a bare non-comment line lints, which is the
2759
+ // plaintext signature. `.qml` and `.scss` sat here for the same reason until
2760
+ // 3.18.0 made the docs true for them; `.pyi` is the row where transcribing
2761
+ // the docs would still ship the wrong tier — the case for probing rather than
2762
+ // copying.
2763
+ ".pyi": "plaintext",
2764
+ // converter-dependent — Vale supports the format, we ship no converter
2765
+ ".adoc": "converter:asciidoctor",
2766
+ ".asc": "converter:asciidoctor",
2767
+ ".asciidoc": "converter:asciidoctor",
2768
+ ".dita": "converter:dita",
2769
+ ".typ": "converter:typst2vast",
2770
+ ".rest": "converter:rst2html",
2771
+ ".rst": "converter:rst2html",
2772
+ ".xml": "converter:xsltproc and an XSLT stylesheet"
2773
+ };
2774
+ function x(e) {
2775
+ return Object.entries(G).filter(([, t]) => t === e).map(([t]) => t);
2776
+ }
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)]] : []
2780
+ )
2781
+ );
2782
+ function xe() {
2783
+ const e = /* @__PURE__ */ new Map();
2784
+ for (const [t, s] of Object.entries(
2785
+ M
2786
+ )) {
2787
+ const o = e.get(s);
2788
+ o === void 0 ? e.set(s, [t]) : o.push(t);
2789
+ }
2790
+ return [...e].map(([t, s]) => ({
2791
+ converter: t,
2792
+ extensions: s
2793
+ }));
2794
+ }
2795
+ const Le = xe(), rn = Object.keys(M);
2796
+ function Ee() {
2797
+ return P.join(", ");
2798
+ }
2799
+ function _e() {
2800
+ return Te.join(", ");
2801
+ }
2802
+ function Ce() {
2803
+ return Ie.join(", ");
2804
+ }
2805
+ function Re() {
2806
+ return Ae.join(", ");
2807
+ }
2808
+ function Ne() {
2809
+ return Le.map(
2810
+ ({ extensions: e, converter: t }) => `${e.join("/")} (needs ${t})`
2811
+ ).join(", ");
2812
+ }
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
2836
+ });
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);
2845
+ } else
2846
+ e.set(g, l);
2847
+ }
2848
+ return { recipeMap: e, anonymousMap: t };
2849
+ }
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) {
2856
+ return `Run:
2857
+ \`\`\`
2858
+ ${e} ${t}
2859
+ \`\`\`
2860
+ ${s}`;
2861
+ }
2862
+ function v(e) {
2863
+ return e.invocation ?? Pe;
2864
+ }
2865
+ function qe(e, t, s = {}) {
2866
+ const o = {
2867
+ CLI_VERSION: "0.11.2",
2868
+ // Engine reach, from the pinned engine versions rather than transcribed
2869
+ // into a recipe. A recipe carrying these lists by hand would go stale on
2870
+ // the next binary bump with nothing to catch it, and stale prose about
2871
+ // what an engine can read is worse than the silence it replaced — an agent
2872
+ // acts on it. `src/rules/capabilities.ts` is the single place a bump edits,
2873
+ // and the two vendor-contract tests fail until it agrees with the binary.
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,
2882
+ // The two steps that gather evidence by running this CLI. Rendered as
2883
+ // whole blocks rather than stripped afterwards, because the default must
2884
+ // stay byte-identical to what `taskless agent route` has always printed
2885
+ // and a post-strip cannot promise that.
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"),
2888
+ // Three steps, in descending order of how much the resolver actually
2889
+ // knows: the caller was told how the CLI was launched; the build is a
2890
+ // nightly/dev/self that knows what it is; nobody knows, so ask the agent.
2891
+ TASKLESS_CLI: v(s)
2892
+ };
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)";
2896
+ }
2897
+ return o;
2898
+ }
2899
+ function q(e) {
2900
+ const t = /* @__PURE__ */ new Set(), s = new Proxy(
2901
+ {},
2902
+ {
2903
+ get(o, l) {
2904
+ return typeof l == "string" && t.add(l), "";
2905
+ },
2906
+ has() {
2907
+ return !0;
2908
+ }
2909
+ }
2910
+ );
2911
+ return V.sprintf(e, s), [...t];
2912
+ }
2913
+ function Ue(e, t, s = {}) {
2914
+ return U(e, t, s);
2915
+ }
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;
2919
+ }
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
+
2936
+ `);
2937
+ return t === -1 ? "" : e.slice(t + 2);
2938
+ }
2939
+ function ln(e, t = {}) {
2940
+ const s = E(e, t);
2941
+ if (s !== void 0)
2942
+ return Ue(s, e, t);
2943
+ }
2944
+ function E(e, t) {
2945
+ return t.anonymous ? je.get(e) ?? N.get(e) : N.get(e);
2946
+ }
2947
+ function hn(e, t = {}) {
2948
+ const s = E(e, t);
2949
+ if (s === void 0) return;
2950
+ const o = s, l = t.header === !1 ? L(o) : o;
2951
+ return {
2952
+ text: l,
2953
+ variables: q(l)
2954
+ };
2955
+ }
2956
+ function cn(e, t = {}) {
2957
+ const s = E(e, t);
2958
+ if (s === void 0) return;
2959
+ const o = s, l = t.header === !1 ? L(o) : o;
2960
+ return {
2961
+ text: U(o, e, t),
2962
+ variables: q(l)
2963
+ };
2964
+ }
2965
+ function Fe(e) {
2966
+ return e.replaceAll(/^[ \t]*<!--\s*vale\b.*?-->[ \t]*\r?\n?/gm, "");
2967
+ }
2968
+ export {
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
2994
+ };