@taskless/cli 0.11.0 → 0.11.1
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.
- package/assets/reference.json +213 -0
- package/dist/binary-DSACXL6n.js +107 -0
- package/dist/index.js +5650 -4550
- package/dist/layout/index.d.ts +31 -0
- package/dist/layout.js +43 -0
- package/dist/node/runtimes/index.d.ts +102 -0
- package/dist/node/runtimes.js +10 -0
- package/dist/prompts/index.d.ts +25 -11
- package/dist/prompts/recipes.d.ts +37 -12
- package/dist/prompts.js +12 -12
- package/dist/recipes-w68swLg4.js +2662 -0
- package/dist/rules/ast-grep-binary.d.ts +26 -0
- package/dist/rules/capabilities.d.ts +46 -6
- package/dist/rules/constraints.d.ts +132 -0
- package/dist/rules/layout.d.ts +152 -0
- package/dist/rules/platform-binary.d.ts +113 -0
- package/dist/rules/vale/binary.d.ts +35 -0
- package/dist/schemas/index.d.ts +93 -0
- package/dist/schemas/rules-create.d.ts +1 -0
- package/dist/schemas/rules-improve.d.ts +1 -0
- package/dist/schemas/rules-verify.d.ts +67 -0
- package/dist/schemas/verify-test.d.ts +22 -0
- package/dist/schemas-OhdGAhu_.js +4512 -0
- package/dist/schemas.js +65 -0
- package/dist/types/errors.d.ts +20 -0
- package/dist/verify-test-BaZPwJqC.js +34 -0
- package/package.json +31 -12
- package/dist/recipes-C_i98mTO.js +0 -6975
|
@@ -0,0 +1,2662 @@
|
|
|
1
|
+
import { o as f, a as g, s as o, l as K, t as q } from "./schemas-OhdGAhu_.js";
|
|
2
|
+
const Pe = "npx @taskless/cli";
|
|
3
|
+
function Ve() {
|
|
4
|
+
return "npx @taskless/cli";
|
|
5
|
+
}
|
|
6
|
+
function De(t) {
|
|
7
|
+
return t;
|
|
8
|
+
}
|
|
9
|
+
function Me(t) {
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
12
|
+
const U = `# Topic: auth (CLI v%(CLI_VERSION)s / topic v1)
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
Manage Taskless authentication. Three branches:
|
|
16
|
+
- **Login**: start the device-code flow and wait for the user to
|
|
17
|
+
approve in their browser.
|
|
18
|
+
- **Logout**: remove the saved token.
|
|
19
|
+
- **Status**: check whether a token is present and whose identity
|
|
20
|
+
it represents.
|
|
21
|
+
|
|
22
|
+
## Preconditions
|
|
23
|
+
- \`.taskless/\` directory exists.
|
|
24
|
+
- For login: the user has a browser to approve the device code.
|
|
25
|
+
|
|
26
|
+
## Steps
|
|
27
|
+
|
|
28
|
+
Pick the branch matching the user's intent.
|
|
29
|
+
|
|
30
|
+
### Login
|
|
31
|
+
|
|
32
|
+
1. Run:
|
|
33
|
+
\`\`\`
|
|
34
|
+
%(TASKLESS_CLI)s auth login
|
|
35
|
+
\`\`\`
|
|
36
|
+
2. The CLI prints a URL and a device code. Tell the user to open the
|
|
37
|
+
URL and enter the code.
|
|
38
|
+
3. The CLI polls until the token is approved. On success, the token
|
|
39
|
+
is written to \`.taskless/.env.local.json\` and a confirmation is
|
|
40
|
+
printed.
|
|
41
|
+
4. Report success. Suggest \`%(TASKLESS_CLI)s info\` to verify identity.
|
|
42
|
+
|
|
43
|
+
The \`--anonymous\` flag is rejected on \`auth login\`, it errors with
|
|
44
|
+
"auth commands cannot be anonymous". Don't pass it.
|
|
45
|
+
|
|
46
|
+
### Logout
|
|
47
|
+
|
|
48
|
+
1. Run:
|
|
49
|
+
\`\`\`
|
|
50
|
+
%(TASKLESS_CLI)s auth logout
|
|
51
|
+
\`\`\`
|
|
52
|
+
2. The CLI removes the saved token (or reports "Not logged in" if
|
|
53
|
+
none was present).
|
|
54
|
+
3. Report the outcome.
|
|
55
|
+
|
|
56
|
+
### Status (no subcommand)
|
|
57
|
+
|
|
58
|
+
1. Run:
|
|
59
|
+
\`\`\`
|
|
60
|
+
%(TASKLESS_CLI)s auth
|
|
61
|
+
\`\`\`
|
|
62
|
+
2. Output is one of:
|
|
63
|
+
- "Not logged in." (with hint to run \`auth login\`)
|
|
64
|
+
- "Logged in as <user> (<orgs>)."
|
|
65
|
+
- "Logged in, but unable to verify identity." (token expired or
|
|
66
|
+
revoked, suggest re-login)
|
|
67
|
+
3. Report to the user.
|
|
68
|
+
|
|
69
|
+
## Errors
|
|
70
|
+
|
|
71
|
+
\`auth login\` and \`auth logout\` accept \`--json\`. On error, the
|
|
72
|
+
standardized \`{ ok: false, code, message }\` envelope is written to
|
|
73
|
+
stdout (and human text on stderr is suppressed). On success in
|
|
74
|
+
\`--json\` mode, the commands exit 0 silently, no success envelope is
|
|
75
|
+
emitted. The status path (\`%(TASKLESS_CLI)s auth\` with no subcommand) accepts
|
|
76
|
+
\`--json\` for forward-compat but currently has no error paths to
|
|
77
|
+
report.
|
|
78
|
+
|
|
79
|
+
| code | meaning | fix |
|
|
80
|
+
|-----------------|--------------------------------------------------------------------------------------|--------------------------------------|
|
|
81
|
+
| \`INVALID_INPUT\` | \`--anonymous\` passed to \`auth login\` (rejected: auth commands cannot be anonymous) | Don't pass \`--anonymous\` |
|
|
82
|
+
| \`NETWORK_ERROR\` | Device flow / token endpoint unreachable, or the device code expired before approval | Check connectivity; retry |
|
|
83
|
+
| \`AUTH_REQUIRED\` | The user denied the authorization request in their browser | Re-run \`%(TASKLESS_CLI)s auth login\` |
|
|
84
|
+
|
|
85
|
+
## See Also
|
|
86
|
+
|
|
87
|
+
- \`%(TASKLESS_CLI)s agent info\`: see auth state and skill versions
|
|
88
|
+
- \`%(TASKLESS_CLI)s agent route\`: first action that requires auth
|
|
89
|
+
`, F = `# Topic: check (CLI v%(CLI_VERSION)s / topic v2)
|
|
90
|
+
|
|
91
|
+
## Goal
|
|
92
|
+
Run the applicable rules against the codebase and report matches. Two
|
|
93
|
+
rule kinds run: **static** ast-grep rules in \`.taskless/rules/sg/\`, and
|
|
94
|
+
**runtime** rules in \`.taskless/rules/runtime/\` (a directory of ast-grep
|
|
95
|
+
capture rules plus a \`check.ts\`). Used standalone (full project scan),
|
|
96
|
+
in CI (diff-only scan), or after rule create/improve to validate.
|
|
97
|
+
|
|
98
|
+
## Preconditions
|
|
99
|
+
- \`.taskless/\` directory exists.
|
|
100
|
+
- At least one rule exists in \`.taskless/rules/sg/\` or
|
|
101
|
+
\`.taskless/rules/runtime/\`. (If none exist, the CLI exits 0 with a
|
|
102
|
+
friendly message suggesting \`%(TASKLESS_CLI)s rule create\`.)
|
|
103
|
+
- No auth required. Static rules always run; whether runtime rules run
|
|
104
|
+
depends on auth state. See "What runs".
|
|
105
|
+
|
|
106
|
+
## What runs
|
|
107
|
+
|
|
108
|
+
\`check\` never requires auth. The two rule kinds run differently:
|
|
109
|
+
|
|
110
|
+
- **Static rules** (\`.taskless/rules/sg/<id>/<id>.yml\`) are inert ast-grep patterns
|
|
111
|
+
and **always run**, in every mode, with no network call. The offline
|
|
112
|
+
linter posture.
|
|
113
|
+
- **Runtime rules** (\`.taskless/rules/runtime/<name>/\`) execute a
|
|
114
|
+
\`check.ts\` (arbitrary code), so they run ONLY when that code is
|
|
115
|
+
verified:
|
|
116
|
+
- **Logged in** (token or API key): each rule's \`check.ts\` is
|
|
117
|
+
reconciled against the Taskless service; rules the server blessed
|
|
118
|
+
(\`run\`) execute, and the rest are withheld and reported (advisory).
|
|
119
|
+
- **Logged out, \`--anonymous\`, no GitHub remote, or service
|
|
120
|
+
unavailable**, runtime rules are **skipped** (reported, never run).
|
|
121
|
+
Static rules still run.
|
|
122
|
+
- **\`--dangerously-run-scripts\`**: runs every runtime rule trusting
|
|
123
|
+
local signatures, with no network call, behind a prominent warning.
|
|
124
|
+
This is the only way to run runtime rules unverified.
|
|
125
|
+
|
|
126
|
+
Notices about skipped/withheld runtime rules are human-readable stderr
|
|
127
|
+
only: they never change the exit code. Under \`--json\` they do NOT appear
|
|
128
|
+
as warnings; instead an additive optional \`skipped: [{ rule, reason }]\`
|
|
129
|
+
array is included alongside the unchanged \`{ success, results }\`. The
|
|
130
|
+
authoritative allow-list is the server's; the CI backstop
|
|
131
|
+
(\`%(TASKLESS_CLI)s agent ci\`) is the enforcement point for runtime rules.
|
|
132
|
+
|
|
133
|
+
## Flags
|
|
134
|
+
- \`--json\`: machine output (\`{ success, results, skipped? }\`).
|
|
135
|
+
- \`--anonymous\`: run only static rules; skip runtime rules.
|
|
136
|
+
- \`--dangerously-run-scripts\`: run runtime \`check.ts\` unverified.
|
|
137
|
+
- \`--timeout <seconds>\`: per-runtime-check wall-clock bound (default 10).
|
|
138
|
+
|
|
139
|
+
## Steps
|
|
140
|
+
|
|
141
|
+
1. **Decide scope.** Default is a full project scan. If the user
|
|
142
|
+
specified files (or you're in a CI context with a known diff),
|
|
143
|
+
pass them as positional arguments.
|
|
144
|
+
|
|
145
|
+
2. **Invoke the CLI.** Either:
|
|
146
|
+
\`\`\`
|
|
147
|
+
%(TASKLESS_CLI)s check --json
|
|
148
|
+
\`\`\`
|
|
149
|
+
or, scoped to specific paths:
|
|
150
|
+
\`\`\`
|
|
151
|
+
%(TASKLESS_CLI)s check --json src/foo.ts src/bar.ts
|
|
152
|
+
\`\`\`
|
|
153
|
+
or, against a git diff:
|
|
154
|
+
\`\`\`
|
|
155
|
+
%(TASKLESS_CLI)s check --json $(git diff --name-only main...HEAD)
|
|
156
|
+
\`\`\`
|
|
157
|
+
Paths that don't exist on disk are silently filtered, so you can
|
|
158
|
+
pipe raw \`git diff\` output directly without pre-filtering.
|
|
159
|
+
|
|
160
|
+
When runtime rules were present but not run (e.g. logged out), the
|
|
161
|
+
JSON also carries \`"skipped": [{ "rule": "<name>", "reason": "…" }]\`
|
|
162
|
+
alongside \`success\`/\`results\`. Surface it so CI can tell that runtime
|
|
163
|
+
rules did not execute; it never affects the exit code.
|
|
164
|
+
|
|
165
|
+
3. **Parse the JSON output.** Shape:
|
|
166
|
+
\`\`\`json
|
|
167
|
+
{
|
|
168
|
+
"success": false,
|
|
169
|
+
"results": [
|
|
170
|
+
{
|
|
171
|
+
"source": "ast-grep",
|
|
172
|
+
"ruleId": "no-eval",
|
|
173
|
+
"severity": "error",
|
|
174
|
+
"message": "Avoid eval()",
|
|
175
|
+
"note": null,
|
|
176
|
+
"file": "src/foo.ts",
|
|
177
|
+
"range": {
|
|
178
|
+
"start": { "line": 12, "column": 4 },
|
|
179
|
+
"end": { "line": 12, "column": 14 }
|
|
180
|
+
},
|
|
181
|
+
"matchedText": "eval(input)",
|
|
182
|
+
"fix": null
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
4. **Report findings to the user.** Group by \`file\`. Show \`severity\`,
|
|
189
|
+
\`message\`, and \`ruleId\` for each finding; the \`range.start\` is the
|
|
190
|
+
useful line/column to surface. The \`success\` field reflects only
|
|
191
|
+
error-severity findings: \`success: false\` means at least one
|
|
192
|
+
\`severity: "error"\` finding exists (exit code 1); \`success: true\`
|
|
193
|
+
with a non-empty \`results\` array means there are only
|
|
194
|
+
warning/info/hint findings (exit code 0); \`success: true\` with an
|
|
195
|
+
empty \`results\` array means the codebase is clean. Findings are
|
|
196
|
+
never reported via the \`{ ok: false, code, message }\` envelope:
|
|
197
|
+
the envelope only appears when the scan itself fails (e.g.
|
|
198
|
+
\`SCAN_FAILED\`) and the normal results payload is absent.
|
|
199
|
+
|
|
200
|
+
## Exit codes
|
|
201
|
+
|
|
202
|
+
- \`0\`: All checks passed, no rules configured, or all supplied
|
|
203
|
+
paths missing
|
|
204
|
+
- \`1\`: Errors detected or scan failed
|
|
205
|
+
|
|
206
|
+
## Errors
|
|
207
|
+
|
|
208
|
+
When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
|
|
209
|
+
|
|
210
|
+
| code | meaning | fix |
|
|
211
|
+
|---------------|-----------------------|----------------------------------|
|
|
212
|
+
| \`SCAN_FAILED\` | ast-grep scan errored | Report; check rule YAML validity |
|
|
213
|
+
|
|
214
|
+
## See Also
|
|
215
|
+
|
|
216
|
+
- \`%(TASKLESS_CLI)s agent route\`: add a rule if none exist
|
|
217
|
+
- \`%(TASKLESS_CLI)s agent ci\`: wire \`check\` into a CI pipeline
|
|
218
|
+
`, W = `# Topic: ci (CLI v%(CLI_VERSION)s / topic v1)
|
|
219
|
+
|
|
220
|
+
## Goal
|
|
221
|
+
Wire \`%(TASKLESS_CLI)s check\` into the user's existing CI so rules run
|
|
222
|
+
automatically on pushes and pull requests. Integrate with what they
|
|
223
|
+
already have, never replace or edit their main pipeline.
|
|
224
|
+
|
|
225
|
+
This recipe teaches two patterns (full scan and diff scan) that
|
|
226
|
+
translate to any CI system. Common systems are listed as hints; if
|
|
227
|
+
you recognize one not on the list, apply the same patterns.
|
|
228
|
+
|
|
229
|
+
## Preconditions
|
|
230
|
+
- \`.taskless/\` directory exists and contains at least one rule. (If
|
|
231
|
+
no rules exist, instruct the user to fetch
|
|
232
|
+
\`%(TASKLESS_CLI)s agent route\` first, wiring CI with zero rules
|
|
233
|
+
produces an always-green check that gives false confidence.)
|
|
234
|
+
- A local \`%(TASKLESS_CLI)s check\` succeeds (or fails with real findings the
|
|
235
|
+
user is OK with seeing in CI's first run).
|
|
236
|
+
- No auth required for CI: \`check\` is unauthenticated by default.
|
|
237
|
+
(Optionally tokenized as a server-enforced backstop; see step 7.)
|
|
238
|
+
|
|
239
|
+
## Steps
|
|
240
|
+
|
|
241
|
+
### 1. Discover the user's CI system
|
|
242
|
+
|
|
243
|
+
Scan the repo root for CI config files. Hints (not exhaustive):
|
|
244
|
+
|
|
245
|
+
| File / directory | CI system |
|
|
246
|
+
|---------------------------|---------------------|
|
|
247
|
+
| \`.github/workflows/*.yml\` | GitHub Actions |
|
|
248
|
+
| \`.gitlab-ci.yml\` | GitLab CI |
|
|
249
|
+
| \`.circleci/config.yml\` | CircleCI |
|
|
250
|
+
| \`Jenkinsfile\` | Jenkins |
|
|
251
|
+
| \`azure-pipelines.yml\` | Azure Pipelines |
|
|
252
|
+
| \`bitbucket-pipelines.yml\` | Bitbucket Pipelines |
|
|
253
|
+
| \`.buildkite/\` | Buildkite |
|
|
254
|
+
| \`.drone.yml\` | Drone |
|
|
255
|
+
| \`.travis.yml\` | Travis CI |
|
|
256
|
+
|
|
257
|
+
Sum up what you found and confirm with the user. If zero match, ask
|
|
258
|
+
which CI they use. If multiple match, ask which should run Taskless.
|
|
259
|
+
|
|
260
|
+
### 2. Agree on the scan pattern
|
|
261
|
+
|
|
262
|
+
- **Full scan**: \`%(TASKLESS_CLI)s check\`. Scans everything. Best for runs
|
|
263
|
+
on the main/default branch.
|
|
264
|
+
- **Diff scan**: \`%(TASKLESS_CLI)s check $(git diff --name-only ...)\`.
|
|
265
|
+
Faster for PR builds. Per-CI diff target var:
|
|
266
|
+
|
|
267
|
+
| CI | Target branch variable |
|
|
268
|
+
|---------------------|-----------------------------------------------|
|
|
269
|
+
| GitHub Actions | \`github.base_ref\` |
|
|
270
|
+
| GitLab CI | \`CI_MERGE_REQUEST_TARGET_BRANCH_NAME\` |
|
|
271
|
+
| CircleCI | \`CIRCLE_BRANCH\` (fetch main and diff against) |
|
|
272
|
+
| Jenkins | \`env.CHANGE_TARGET\` |
|
|
273
|
+
| Azure Pipelines | \`System.PullRequest.TargetBranch\` |
|
|
274
|
+
| Bitbucket Pipelines | \`BITBUCKET_PR_DESTINATION_BRANCH\` |
|
|
275
|
+
|
|
276
|
+
\`%(TASKLESS_CLI)s check\` silently filters paths that don't exist, so raw
|
|
277
|
+
\`git diff --name-only\` output can pipe in directly.
|
|
278
|
+
|
|
279
|
+
**Recommended default:** diff scan on PRs, full scan on pushes to
|
|
280
|
+
main.
|
|
281
|
+
|
|
282
|
+
### 3. Verify locally first
|
|
283
|
+
|
|
284
|
+
Run \`%(TASKLESS_CLI)s check\`:
|
|
285
|
+
- Clean pass → proceed.
|
|
286
|
+
- "No rules configured" → stop. Fetch \`%(TASKLESS_CLI)s agent route\`.
|
|
287
|
+
- Findings → tell the user CI will fail; ask whether to fix,
|
|
288
|
+
suppress, or proceed knowing the first CI run will be red.
|
|
289
|
+
|
|
290
|
+
### 4. Generate the config
|
|
291
|
+
|
|
292
|
+
Rules:
|
|
293
|
+
1. Write a NEW standalone file. Never modify the user's existing
|
|
294
|
+
CI config.
|
|
295
|
+
2. Prefer the CI's native include mechanism. Tell the user the one
|
|
296
|
+
line they need to add to their main config.
|
|
297
|
+
3. If the file you'd write already exists, ask before overwriting.
|
|
298
|
+
|
|
299
|
+
Canonical paths:
|
|
300
|
+
|
|
301
|
+
| CI | File path |
|
|
302
|
+
|---------------------|---------------------------------------------------------------------|
|
|
303
|
+
| GitHub Actions | \`.github/workflows/taskless.yml\` (standalone, no include needed) |
|
|
304
|
+
| GitLab CI | \`.taskless/ci/gitlab.yml\` (user adds \`include\`) |
|
|
305
|
+
| CircleCI | \`.taskless/ci/circleci-job.yml\` (no include, user copies job) |
|
|
306
|
+
| Jenkins | \`.taskless/ci/taskless.Jenkinsfile\` (user \`load()\`s) |
|
|
307
|
+
| Azure Pipelines | \`.taskless/ci/azure-taskless.yml\` (user references via \`template:\`) |
|
|
308
|
+
| Bitbucket Pipelines | \`.taskless/ci/bitbucket-pipelines.yml\` (user merges manually) |
|
|
309
|
+
| Other | \`.taskless/ci/<system>.<ext>\` + clear wiring instructions |
|
|
310
|
+
|
|
311
|
+
### 5. GitHub Actions reference template
|
|
312
|
+
|
|
313
|
+
The reference template, translate the same shape (checkout with
|
|
314
|
+
full history, set up Node, conditional check) for other CIs.
|
|
315
|
+
|
|
316
|
+
Substitute \`%(PACKAGE_MANAGER_DLX)s\` with the CI runner's own complete
|
|
317
|
+
invocation, \`npx @taskless/cli\`, \`pnpm dlx @taskless/cli\`,
|
|
318
|
+
\`yarn dlx @taskless/cli\`, or \`bunx @taskless/cli\`, picking the launcher
|
|
319
|
+
from \`pnpm-lock.yaml\`/\`yarn.lock\`/\`bun.lockb\`. It is a whole command on
|
|
320
|
+
its own, exactly as the template uses it; never combine it with
|
|
321
|
+
\`%(TASKLESS_CLI)s\`. Which launcher the repository you are wiring up should
|
|
322
|
+
run is a different question from how this recipe was fetched, which is why
|
|
323
|
+
it stays a separate placeholder.
|
|
324
|
+
|
|
325
|
+
\`\`\`yaml
|
|
326
|
+
name: Taskless
|
|
327
|
+
|
|
328
|
+
on:
|
|
329
|
+
push:
|
|
330
|
+
branches: [main]
|
|
331
|
+
pull_request:
|
|
332
|
+
|
|
333
|
+
permissions:
|
|
334
|
+
contents: read
|
|
335
|
+
|
|
336
|
+
jobs:
|
|
337
|
+
check:
|
|
338
|
+
runs-on: ubuntu-latest
|
|
339
|
+
steps:
|
|
340
|
+
- uses: actions/checkout@v4
|
|
341
|
+
with:
|
|
342
|
+
fetch-depth: 0
|
|
343
|
+
|
|
344
|
+
- uses: actions/setup-node@v4
|
|
345
|
+
with:
|
|
346
|
+
node-version: 20
|
|
347
|
+
|
|
348
|
+
- name: Taskless check
|
|
349
|
+
run: |
|
|
350
|
+
if [ "\${{ github.event_name }}" = "pull_request" ]; then
|
|
351
|
+
git fetch origin "\${{ github.base_ref }}" --depth=1
|
|
352
|
+
FILES=$(git diff --name-only "origin/\${{ github.base_ref }}...HEAD")
|
|
353
|
+
if [ -z "$FILES" ]; then
|
|
354
|
+
echo "No changed files."
|
|
355
|
+
exit 0
|
|
356
|
+
fi
|
|
357
|
+
%(PACKAGE_MANAGER_DLX)s check $FILES
|
|
358
|
+
else
|
|
359
|
+
%(PACKAGE_MANAGER_DLX)s check
|
|
360
|
+
fi
|
|
361
|
+
\`\`\`
|
|
362
|
+
|
|
363
|
+
Simplifications:
|
|
364
|
+
- Full-scan only → drop the \`if\`, just run \`%(PACKAGE_MANAGER_DLX)s check\`.
|
|
365
|
+
- Diff-scan only → remove the \`push:\` trigger.
|
|
366
|
+
|
|
367
|
+
### 6. Translate to other CIs
|
|
368
|
+
|
|
369
|
+
The six universal steps:
|
|
370
|
+
1. Set up Node (v20 default; match the user's existing CI version).
|
|
371
|
+
2. Fetch with full depth (or enough to reach the target branch).
|
|
372
|
+
3. Fetch the target branch.
|
|
373
|
+
4. Compute changed files with \`git diff --name-only "origin/<target>...HEAD"\`.
|
|
374
|
+
5. Exit early if the diff is empty.
|
|
375
|
+
6. Call \`%(PACKAGE_MANAGER_DLX)s check $FILES\` for PR builds, or
|
|
376
|
+
\`%(PACKAGE_MANAGER_DLX)s check\` for main-branch builds.
|
|
377
|
+
|
|
378
|
+
YAML for GitHub/GitLab/Azure/Bitbucket; Groovy for Jenkins;
|
|
379
|
+
different structure for CircleCI. The six steps stay the same.
|
|
380
|
+
|
|
381
|
+
### 7. Authentication in CI (optional backstop)
|
|
382
|
+
|
|
383
|
+
\`%(TASKLESS_CLI)s check\` does NOT require authentication. The generated CI
|
|
384
|
+
config works out of the box with no secrets and scans all local rules.
|
|
385
|
+
|
|
386
|
+
Static ast-grep rules always run in CI with no secrets. **Runtime
|
|
387
|
+
rules** (\`.taskless/rules/runtime/\`, which execute a \`check.ts\`) only
|
|
388
|
+
run when their code is server-verified, so an unauthenticated CI job
|
|
389
|
+
runs the static rules and skips the runtime ones.
|
|
390
|
+
|
|
391
|
+
Exposing a \`TASKLESS_TOKEN\` secret turns CI into the **backstop** for
|
|
392
|
+
runtime rules: an authenticated \`check\` reconciles each runtime rule's
|
|
393
|
+
\`check.ts\` against the Taskless service and runs exactly the
|
|
394
|
+
server-blessed set, withholding any that drift or were never issued.
|
|
395
|
+
This is the enforcement point for runtime rules, local developer runs
|
|
396
|
+
skip them unless \`--dangerously-run-scripts\` is passed. To wire it, set
|
|
397
|
+
the token as an env var on the check step (GitHub Actions):
|
|
398
|
+
|
|
399
|
+
\`\`\`yaml
|
|
400
|
+
- name: Taskless check
|
|
401
|
+
env:
|
|
402
|
+
TASKLESS_TOKEN: \${{ secrets.TASKLESS_TOKEN }}
|
|
403
|
+
run: |
|
|
404
|
+
...
|
|
405
|
+
\`\`\`
|
|
406
|
+
|
|
407
|
+
Add this only when the user wants server-enforced rules in CI; the
|
|
408
|
+
unauthenticated default remains fully supported. Also mention auth if
|
|
409
|
+
the user explicitly asks to run authenticated commands (e.g.
|
|
410
|
+
\`rule create\`/\`rule improve\`) in CI, uncommon.
|
|
411
|
+
|
|
412
|
+
### 8. Package manager caveats
|
|
413
|
+
|
|
414
|
+
- **pnpm**: \`pnpm dlx\` works but has slow cold starts. If the user's
|
|
415
|
+
pipeline already sets up pnpm, suggest adding \`@taskless/cli\` as a
|
|
416
|
+
dev dep and calling it via \`pnpm taskless check\`. That \`taskless\` is the
|
|
417
|
+
binary pnpm resolves from the wired repo's own \`node_modules/.bin\`, not
|
|
418
|
+
this CLI's invocation, so it is not \`%(TASKLESS_CLI)s\`.
|
|
419
|
+
- **Yarn v1 (classic)**: doesn't support \`yarn dlx\`. Use \`npx\`.
|
|
420
|
+
- **Bun**: \`bunx\` works.
|
|
421
|
+
|
|
422
|
+
### 9. Report back
|
|
423
|
+
|
|
424
|
+
Show:
|
|
425
|
+
1. The path written and a 10-15 line excerpt.
|
|
426
|
+
2. For CIs needing manual wiring, the exact \`include:\` / reference
|
|
427
|
+
line for their main config.
|
|
428
|
+
3. \`git status\` so they can review before committing.
|
|
429
|
+
4. A note that the first CI run exercises rules, if existing
|
|
430
|
+
matches exist, CI will fail until fixed or suppressed.
|
|
431
|
+
|
|
432
|
+
## Errors
|
|
433
|
+
|
|
434
|
+
- **No rules** → fetch \`%(TASKLESS_CLI)s agent route\`. Don't write CI
|
|
435
|
+
config.
|
|
436
|
+
- **Unrecognized CI** → produce a generic \`.taskless/ci/check.sh\`
|
|
437
|
+
script implementing the six universal steps. Be upfront it's a
|
|
438
|
+
starting point.
|
|
439
|
+
- **Target CI file exists, user declines overwrite** → stop. Show
|
|
440
|
+
what you would have written; let them reconcile manually.
|
|
441
|
+
|
|
442
|
+
## See Also
|
|
443
|
+
|
|
444
|
+
- \`%(TASKLESS_CLI)s agent check\`: the command being wired into CI
|
|
445
|
+
- \`%(TASKLESS_CLI)s agent route\`: required if no rules exist yet
|
|
446
|
+
`, $ = `# Topic: create-legacy-rule (CLI v%(CLI_VERSION)s / topic v1)
|
|
447
|
+
|
|
448
|
+
## You are here
|
|
449
|
+
This is \`create-legacy-rule\`. It helps you write a rule for a linter the
|
|
450
|
+
repository already runs (ESLint, Ruff, RuboCop, Stylelint) in that
|
|
451
|
+
tool's own dialect, so that tool enforces it.
|
|
452
|
+
If that is not the kind of check you need, re-run \`%(TASKLESS_CLI)s agent route\`
|
|
453
|
+
and follow its decision rather than adapting this recipe.
|
|
454
|
+
|
|
455
|
+
## Goal
|
|
456
|
+
Author a rule in a linter the repository ALREADY uses, expressed in that
|
|
457
|
+
tool's own dialect (an ESLint rule, a Ruff rule selection, a RuboCop cop,
|
|
458
|
+
a Stylelint rule, etc.). Taskless does not maintain a catalog of linter
|
|
459
|
+
rules, you source the knowledge from the repo first and the web second,
|
|
460
|
+
then write the rule where that tool expects it.
|
|
461
|
+
|
|
462
|
+
## Preconditions
|
|
463
|
+
- The repo has a detected linter (confirm via \`%(TASKLESS_CLI)s detect --json\`).
|
|
464
|
+
- The agent can read/write files and fetch web pages.
|
|
465
|
+
- No auth required.
|
|
466
|
+
|
|
467
|
+
## Steps
|
|
468
|
+
|
|
469
|
+
1. **Confirm the target tool.** Use \`%(TASKLESS_CLI)s detect --json\` to identify
|
|
470
|
+
which linter is configured. If more than one could host this rule,
|
|
471
|
+
ask the user which tool should own it.
|
|
472
|
+
|
|
473
|
+
2. **Mine the repo's own rules first.** This is the highest-signal
|
|
474
|
+
source for house style. Look at how the repo already writes rules of
|
|
475
|
+
this kind:
|
|
476
|
+
- existing config (e.g. \`.eslintrc*\`/\`eslint.config.*\`, \`ruff.toml\`
|
|
477
|
+
or \`[tool.ruff]\`, \`.rubocop.yml\`, \`.stylelintrc*\`);
|
|
478
|
+
- any custom/local rules the repo authored (the \`detect\` output's
|
|
479
|
+
rule styles point at these);
|
|
480
|
+
- the conventions, severity choices, and naming they use.
|
|
481
|
+
Match that style.
|
|
482
|
+
|
|
483
|
+
3. **Fall back to the web only if the repo signal is thin.** If the repo
|
|
484
|
+
doesn't show how to express this rule, fetch the linter's CURRENT
|
|
485
|
+
documentation (WebFetch/WebSearch). Prefer the latest version's docs
|
|
486
|
+
and, when present, an \`llms.txt\` on the tool's docs site. Confirm the
|
|
487
|
+
installed version where it matters so you target the right syntax.
|
|
488
|
+
|
|
489
|
+
4. **Author the rule in the tool's dialect.** Write or extend the
|
|
490
|
+
linter's config / custom-rule file the way that tool expects. Keep it
|
|
491
|
+
consistent with the repo's existing entries from step 2.
|
|
492
|
+
|
|
493
|
+
5. **Report, and be explicit about who runs it.** Show the file(s) you
|
|
494
|
+
changed. Make clear that the user's OWN toolchain runs this rule:
|
|
495
|
+
\`%(TASKLESS_CLI)s check\` does NOT execute external linters. Tell the user how
|
|
496
|
+
to run their linter to see it fire (e.g. their existing lint script).
|
|
497
|
+
|
|
498
|
+
## Important Notes
|
|
499
|
+
|
|
500
|
+
- Do not invent linter rules from memory: verify against the repo's
|
|
501
|
+
usage and the tool's current docs.
|
|
502
|
+
- This path is author-only. Taskless does not aggregate or run external
|
|
503
|
+
linters; it writes the rule in the tool's dialect and hands off.
|
|
504
|
+
|
|
505
|
+
## See Also
|
|
506
|
+
|
|
507
|
+
- \`%(TASKLESS_CLI)s agent route\`: re-decide the destination if this no longer fits
|
|
508
|
+
- \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a local ast-grep rule instead
|
|
509
|
+
- \`%(TASKLESS_CLI)s agent create-remote-rule\`: generate via the service (login)
|
|
510
|
+
`, z = `# Topic: create-remote-rule (CLI v%(CLI_VERSION)s / topic v3)
|
|
511
|
+
|
|
512
|
+
## You are here
|
|
513
|
+
This is \`create-remote-rule\`. It helps you have the Taskless service
|
|
514
|
+
write a rule, when the rule is beyond what you can express on-device or
|
|
515
|
+
the user has chosen to spend a generation on it.
|
|
516
|
+
If that is not the kind of check you need, re-run \`%(TASKLESS_CLI)s agent route\`
|
|
517
|
+
and follow its decision rather than adapting this recipe.
|
|
518
|
+
|
|
519
|
+
## Goal
|
|
520
|
+
Enrich the user's description into a request the service can act on,
|
|
521
|
+
submit it, and report what came back. The service generates the rule and
|
|
522
|
+
writes the rule files; your job is everything on either side of that
|
|
523
|
+
call.
|
|
524
|
+
|
|
525
|
+
This recipe is the whole path, the boundary and the procedure. Do not
|
|
526
|
+
go looking for a second topic to perform the submission.
|
|
527
|
+
|
|
528
|
+
## Preconditions
|
|
529
|
+
- \`.taskless/\` directory exists.
|
|
530
|
+
- The user is logged in. This path requires auth.
|
|
531
|
+
- The project has a GitHub owner. **Check this yourself before collecting
|
|
532
|
+
anything**, see below.
|
|
533
|
+
- The request reached here through \`route\`, or through a local attempt
|
|
534
|
+
that failed and a user who confirmed the escalation.
|
|
535
|
+
|
|
536
|
+
### Check the GitHub owner first
|
|
537
|
+
|
|
538
|
+
Generation runs against a verifiable org, which comes from a GitHub
|
|
539
|
+
\`origin\` remote. Run:
|
|
540
|
+
\`\`\`
|
|
541
|
+
%(TASKLESS_CLI)s info --json
|
|
542
|
+
\`\`\`
|
|
543
|
+
If \`ghOwner\` is the literal \`[unknown]\`, **stop here**. Do not enrich the
|
|
544
|
+
request, do not collect examples, and do not submit: the call cannot
|
|
545
|
+
succeed, and everything before it is wasted work for the user.
|
|
546
|
+
|
|
547
|
+
Say the tier is unavailable and why, then route to a local recipe:
|
|
548
|
+
\`create-sg-rule\`, \`create-vale-rule\`, or \`create-legacy-rule\` by the
|
|
549
|
+
rule's shape, or \`create-runtime-rule\` if it is only expressible at
|
|
550
|
+
runtime.
|
|
551
|
+
|
|
552
|
+
\`route\` already drops this destination when there is no owner, so
|
|
553
|
+
normally you never arrive here without one. This check is for the times
|
|
554
|
+
that does not hold: reached directly, from a cached plan, or from a copy
|
|
555
|
+
of the routing guidance older than this constraint. An omission upstream
|
|
556
|
+
is advisory; this is the point of use.
|
|
557
|
+
|
|
558
|
+
**\`auth login\` does not fix it.** No GitHub owner is a property of the
|
|
559
|
+
project, not the session, so do not send the user to log in for it. The
|
|
560
|
+
causes are: not a git repository, no \`origin\` remote, an \`origin\` that is
|
|
561
|
+
not GitHub, and git not being installed at all. The first three each have
|
|
562
|
+
their own error code; a missing \`git\` binary cannot be told apart from
|
|
563
|
+
the first, because the probe that would distinguish them is itself a
|
|
564
|
+
\`git\` call, so it reports \`NOT_A_GIT_REPOSITORY\`. If the user says the
|
|
565
|
+
directory *is* a repository, check whether \`git\` is on their \`PATH\`
|
|
566
|
+
before believing the code.
|
|
567
|
+
|
|
568
|
+
## Cost, and when this path is right
|
|
569
|
+
|
|
570
|
+
A generation costs the user something and requires an account. That is
|
|
571
|
+
why \`route\` prefers a local rule it can write and verify, and why this
|
|
572
|
+
recipe is not a default.
|
|
573
|
+
|
|
574
|
+
Two ways to legitimately be here:
|
|
575
|
+
|
|
576
|
+
- **\`route\` sent you.** Either the rule is not expressible on-device, or
|
|
577
|
+
it is expressible and the user was offered the choice and took it.
|
|
578
|
+
- **A local attempt failed and the user confirmed.** If you came from
|
|
579
|
+
\`create-sg-rule\`'s failure path, that recipe has already deleted the
|
|
580
|
+
broken candidate and asked. If it has not been asked, ask now, a
|
|
581
|
+
silent fall-through from a failed local attempt to a paid service call
|
|
582
|
+
is not acceptable, even when the service would do better.
|
|
583
|
+
|
|
584
|
+
## Steps
|
|
585
|
+
|
|
586
|
+
1. **Confirm auth.** Run:
|
|
587
|
+
\`\`\`
|
|
588
|
+
%(TASKLESS_CLI)s info --json
|
|
589
|
+
\`\`\`
|
|
590
|
+
Check \`loggedIn\`. If false, fetch \`%(TASKLESS_CLI)s agent auth\` and follow the
|
|
591
|
+
login recipe before continuing. Do not build a request you cannot
|
|
592
|
+
submit.
|
|
593
|
+
|
|
594
|
+
2. **Check for a rule that already covers this.** Scan
|
|
595
|
+
\`.taskless/rules/sg/\` and read each rule's \`message\`, \`note\`, and
|
|
596
|
+
\`rule\` fields. If one overlaps, show the user and ask whether they
|
|
597
|
+
would rather iterate on it, \`%(TASKLESS_CLI)s agent improve-rule\` refines an
|
|
598
|
+
existing rule and is usually the better answer than a second rule
|
|
599
|
+
that half-overlaps the first.
|
|
600
|
+
|
|
601
|
+
3. **Gather the request.** Even when the user supplied a description,
|
|
602
|
+
ask:
|
|
603
|
+
- What exact pattern should be flagged? Get concrete examples.
|
|
604
|
+
- In what language?
|
|
605
|
+
- Where is the pattern acceptable: are there contexts to exclude?
|
|
606
|
+
|
|
607
|
+
If you arrived from a failed local attempt, reuse the cases you
|
|
608
|
+
already gathered rather than asking again.
|
|
609
|
+
|
|
610
|
+
4. **Enrich it.** This is the part that decides the quality of what
|
|
611
|
+
comes back:
|
|
612
|
+
- Search the codebase for real instances and show the user what you
|
|
613
|
+
found. Real matches beat invented ones.
|
|
614
|
+
- Ask for further success and failure cases, especially near-misses.
|
|
615
|
+
- Read \`.gitignore\`, the linter configs, and \`tsconfig.json\`'s
|
|
616
|
+
\`exclude\` for exclusion patterns the repo already believes in.
|
|
617
|
+
Present them as proposed defaults rather than applying them.
|
|
618
|
+
- Infer the primary language from the codebase and confirm it. Put it
|
|
619
|
+
in the \`prompt\` field.
|
|
620
|
+
|
|
621
|
+
5. **Confirm before spending the generation.** Summarize the full
|
|
622
|
+
prompt, the success cases, the failure cases, and the exclusions.
|
|
623
|
+
This is the last point at which a misunderstanding is free.
|
|
624
|
+
|
|
625
|
+
6. **Write the payload** to \`.taskless/.tmp-rule-request.json\`, matching
|
|
626
|
+
the input schema below.
|
|
627
|
+
|
|
628
|
+
7. **Submit.** Run:
|
|
629
|
+
\`\`\`
|
|
630
|
+
%(TASKLESS_CLI)s rule create --from .taskless/.tmp-rule-request.json --json
|
|
631
|
+
\`\`\`
|
|
632
|
+
This may take 30-60 seconds while the service generates the rule.
|
|
633
|
+
|
|
634
|
+
8. **Clean up.** Delete \`.taskless/.tmp-rule-request.json\` whether the
|
|
635
|
+
call succeeded or failed.
|
|
636
|
+
|
|
637
|
+
9. **Report.** The service writes the rule to
|
|
638
|
+
\`.taskless/rules/sg/<id>/<id>.yml\` and its tests to
|
|
639
|
+
\`.taskless/rules/sg/<id>/.tests/<id>-YYYYMMDD-test.yml\`. These are
|
|
640
|
+
the same paths and the same shape a locally authored rule uses, so
|
|
641
|
+
\`check\`, \`improve-rule\`, \`verify\`, and \`test\` treat them
|
|
642
|
+
identically. Nothing is written under \`.taskless/rule-metadata/\`.
|
|
643
|
+
Show the user the paths and suggest \`%(TASKLESS_CLI)s agent check\`.
|
|
644
|
+
|
|
645
|
+
**Record the \`ruleId\` from the \`--json\` output.** It is the ticket
|
|
646
|
+
id the iterate endpoint is addressed by, \`improve-rule\` asks for it,
|
|
647
|
+
and no file on disk carries it.
|
|
648
|
+
|
|
649
|
+
**Read \`notices\` if it is present.** It is an optional array of
|
|
650
|
+
advisory messages about a delivery that was written anyway. A rule
|
|
651
|
+
arriving with no \`.tests/\` fixtures is the one to expect. That rule works, but
|
|
652
|
+
nothing exercises it: \`test\` reports a pass over zero cases, which
|
|
653
|
+
reads exactly like a rule proven to work. Surface any notice to the
|
|
654
|
+
user rather than reporting an unqualified success. Under \`--json\`
|
|
655
|
+
these do not go to stderr, so the envelope is the only place they
|
|
656
|
+
appear.
|
|
657
|
+
|
|
658
|
+
## Input schema
|
|
659
|
+
|
|
660
|
+
The \`--from\` JSON file conforms to:
|
|
661
|
+
|
|
662
|
+
\`\`\`json
|
|
663
|
+
%(INPUT_SCHEMA)s
|
|
664
|
+
\`\`\`
|
|
665
|
+
|
|
666
|
+
Each entry in \`successCases\` and \`failureCases\` is a separate string.
|
|
667
|
+
Multi-line code goes in one string with literal newlines.
|
|
668
|
+
|
|
669
|
+
## Important Notes
|
|
670
|
+
|
|
671
|
+
- Do NOT pre-build the rule yourself here. Submit the request and let
|
|
672
|
+
the service generate it; a hand-written rule attached to a generation
|
|
673
|
+
request is neither reviewed nor used.
|
|
674
|
+
- The service owns rule-type selection. Today it generates ast-grep
|
|
675
|
+
rules under \`.taskless/rules/sg/\`, the same shape \`create-sg-rule\`
|
|
676
|
+
produces locally.
|
|
677
|
+
|
|
678
|
+
## Errors
|
|
679
|
+
|
|
680
|
+
With \`--json\`, failures emit \`{ ok: false, code, message }\`:
|
|
681
|
+
|
|
682
|
+
| code | meaning | fix |
|
|
683
|
+
|--------------------------|---------------------------------|----------------------------------------------|
|
|
684
|
+
| \`AUTH_REQUIRED\` | not logged in | fetch \`%(TASKLESS_CLI)s agent auth\` |
|
|
685
|
+
| \`NO_GITHUB_REMOTE\` | no GitHub origin remote | retained for compatibility; treat as below |
|
|
686
|
+
| \`NOT_A_GIT_REPOSITORY\` | not a git repository | route to a local recipe; \`auth login\` cannot fix it |
|
|
687
|
+
| \`NO_ORIGIN_REMOTE\` | git repository, no \`origin\` | route to a local recipe; \`auth login\` cannot fix it |
|
|
688
|
+
| \`UNSUPPORTED_REMOTE_HOST\`| \`origin\` is not GitHub | route to a local recipe; \`auth login\` cannot fix it |
|
|
689
|
+
| \`INVALID_INPUT\` | \`--from\` JSON failed validation | re-read the input schema, fix, retry |
|
|
690
|
+
| \`NETWORK_ERROR\` | submit/poll failed | report and suggest retry |
|
|
691
|
+
| \`RULE_GENERATION_FAILED\` | the service failed to generate | report the message; suggest enriching prompt |
|
|
692
|
+
| \`RULE_UNSUPPORTED\` | plan lacks this generation type | tell the user to enable it; do not retry |
|
|
693
|
+
|
|
694
|
+
## See Also
|
|
695
|
+
|
|
696
|
+
- \`%(TASKLESS_CLI)s agent route\`: the routing decision that leads here
|
|
697
|
+
- \`%(TASKLESS_CLI)s agent auth\`: log in before generating
|
|
698
|
+
- \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a rule locally instead
|
|
699
|
+
- \`%(TASKLESS_CLI)s agent improve-rule\`: iterate on a rule that already exists
|
|
700
|
+
- \`%(TASKLESS_CLI)s agent check\`: validate the generated rule
|
|
701
|
+
`, H = `# Topic: create-runtime-rule (CLI v%(CLI_VERSION)s / topic v2)
|
|
702
|
+
|
|
703
|
+
## You are here
|
|
704
|
+
This is \`create-runtime-rule\`. It helps you write a runtime rule: a
|
|
705
|
+
check that runs your own code, because answering it needs more than one
|
|
706
|
+
file, the repository graph, git metadata, build output, a resolved
|
|
707
|
+
config chain.
|
|
708
|
+
If that is not the kind of check you need, re-run \`%(TASKLESS_CLI)s agent route\`
|
|
709
|
+
and follow its decision rather than adapting this recipe.
|
|
710
|
+
|
|
711
|
+
You are reading this recipe rather than \`create-remote-rule\` because the
|
|
712
|
+
user is **not logged in**. That is the whole difference, and this recipe
|
|
713
|
+
explains it once.
|
|
714
|
+
|
|
715
|
+
## Goal
|
|
716
|
+
Explain what a runtime rule is, why this tier is gated when the static
|
|
717
|
+
tiers are not, and what the user has to do before one can run.
|
|
718
|
+
|
|
719
|
+
## Preconditions
|
|
720
|
+
- \`.taskless/\` directory exists.
|
|
721
|
+
- The user is **not** logged in. If \`%(TASKLESS_CLI)s info --json\` reports
|
|
722
|
+
\`loggedIn: true\`, you are in the wrong recipe, re-run
|
|
723
|
+
\`%(TASKLESS_CLI)s agent route\`.
|
|
724
|
+
|
|
725
|
+
## What a runtime rule is
|
|
726
|
+
|
|
727
|
+
A rule directory under \`.taskless/rules/runtime/<name>/\` holding two
|
|
728
|
+
kinds of file:
|
|
729
|
+
|
|
730
|
+
| Path | Role |
|
|
731
|
+
|------------------|---------------------------------------------------------------------------|
|
|
732
|
+
| \`captures/*.yml\` | ast-grep capture rules that narrow which files the check looks at |
|
|
733
|
+
| \`check.ts\` | a module whose default export receives those matches and returns findings |
|
|
734
|
+
| \`.tests/\` | fixtures, in \`pass/\` and \`fail/\` buckets |
|
|
735
|
+
|
|
736
|
+
A fixture case is a DIRECTORY, not a file: \`.tests/pass/case-1/\`, and its
|
|
737
|
+
path is the \`root\` the harness hands the check, with every \`match.file\`
|
|
738
|
+
relative to it. So a case may hold as many files as the case needs.
|
|
739
|
+
|
|
740
|
+
That is not a detail. A runtime rule exists because its evidence spans
|
|
741
|
+
more than one file, so a layout allowing one file per case could not
|
|
742
|
+
express the rules the tier is for. \`pass/case-1/src/a.ts\` alongside
|
|
743
|
+
\`pass/case-1/src/b.ts\` is the ordinary shape, not the exotic one.
|
|
744
|
+
|
|
745
|
+
\`pass\`/\`fail\` rather than \`valid\`/\`invalid\`, matching Vale's buckets.
|
|
746
|
+
\`valid\`/\`invalid\` are key names inside an ast-grep test YAML and are
|
|
747
|
+
right for \`sg\` for one reason only: that file is handed to \`sg test\`
|
|
748
|
+
verbatim. Nothing hands a runtime fixture to ast-grep, so borrowing the
|
|
749
|
+
word here would make it mean two things.
|
|
750
|
+
|
|
751
|
+
\`verify\` fails a runtime rule with no capture rule in \`captures/\`,
|
|
752
|
+
because \`check.ts\` would then never be invoked.
|
|
753
|
+
|
|
754
|
+
The capture rules do the cheap work, if nothing matches, \`check.ts\` is
|
|
755
|
+
never invoked. \`check.ts\` does the part no static engine can: hold two
|
|
756
|
+
files at once, read git, resolve an alias, look outside the repository.
|
|
757
|
+
|
|
758
|
+
That is also exactly why it is gated.
|
|
759
|
+
|
|
760
|
+
## Why this tier is gated
|
|
761
|
+
|
|
762
|
+
**Because it executes code, not because of what it can express.**
|
|
763
|
+
|
|
764
|
+
\`sg\` and \`vale\` rules are inert data. Whatever is in them, the worst a
|
|
765
|
+
malicious rule achieves is a wrong finding, \`%(TASKLESS_CLI)s check\` runs them
|
|
766
|
+
with no login, no network, and nothing to verify, because there is
|
|
767
|
+
nothing to verify.
|
|
768
|
+
|
|
769
|
+
A runtime rule's \`check.ts\` is a program that runs on the developer's
|
|
770
|
+
machine with the developer's permissions. A rule file that arrives in a
|
|
771
|
+
pull request is code that arrives in a pull request. So \`check\`:
|
|
772
|
+
|
|
773
|
+
1. **Signs** each \`check.ts\`, a signature over the exact bytes on disk.
|
|
774
|
+
2. **Reconciles** those signatures with the Taskless service, which
|
|
775
|
+
answers with the set it will vouch for.
|
|
776
|
+
3. **Runs only what came back blessed.** Anything else is reported as
|
|
777
|
+
skipped, with the reason.
|
|
778
|
+
|
|
779
|
+
Reconciliation is what makes the signature mean something: it is the
|
|
780
|
+
service saying "this rule, these exact bytes, for this repository."
|
|
781
|
+
Without a login there is nobody to ask, so there is no answer, so
|
|
782
|
+
nothing runs.
|
|
783
|
+
|
|
784
|
+
This is a property of executing code. It says nothing about whether a
|
|
785
|
+
runtime rule is more or less capable, and it is not a quality tier:
|
|
786
|
+
\`sg\` and \`vale\` rules are not less trusted, they are unexecuted.
|
|
787
|
+
|
|
788
|
+
## What happens today, logged out
|
|
789
|
+
|
|
790
|
+
Nothing breaks. \`%(TASKLESS_CLI)s check\` still runs every static rule; each
|
|
791
|
+
runtime rule it finds is listed as skipped with the reason
|
|
792
|
+
\`not authenticated, runtime rules were not verified and did not run\`.
|
|
793
|
+
|
|
794
|
+
So a runtime rule you write now is inert until the user logs in. Say
|
|
795
|
+
that plainly rather than letting them discover it from a silent check.
|
|
796
|
+
|
|
797
|
+
## Testing a rule you wrote yourself needs the flag
|
|
798
|
+
|
|
799
|
+
\`%(TASKLESS_CLI)s test\` runs a runtime rule's fixtures by executing its
|
|
800
|
+
\`check.ts\` against each case directory. The flag is the whole gate, for
|
|
801
|
+
every rule, always:
|
|
802
|
+
|
|
803
|
+
\`\`\`
|
|
804
|
+
%(TASKLESS_CLI)s test .taskless/rules/runtime/<name> --dangerously-run-scripts
|
|
805
|
+
\`\`\`
|
|
806
|
+
|
|
807
|
+
**\`test\` does not talk to the rule service at all**, and there is no
|
|
808
|
+
blessed-signature path that runs fixtures without the flag. That makes
|
|
809
|
+
it stricter than \`check\`, which will execute a blessed rule with no flag
|
|
810
|
+
because it runs rules as a side effect of scanning a repository. \`test\`
|
|
811
|
+
runs them because you asked it to, so the verb is the consent and the
|
|
812
|
+
flag is the confirmation; nothing is asked of a server in between.
|
|
813
|
+
|
|
814
|
+
**A locally authored rule has no signature and never will**, so this was
|
|
815
|
+
never going to be avoidable for a rule you wrote. Blessing is recording:
|
|
816
|
+
a signature is blessed because a reconcile reported it and the service
|
|
817
|
+
recorded it, and nothing recorded a rule that has not left the working
|
|
818
|
+
tree. Do not send anyone to \`auth login\` over this. It changes nothing
|
|
819
|
+
here, and the message does not mention it.
|
|
820
|
+
|
|
821
|
+
The friction is the correct friction. \`sg\` and \`vale\` authors do not
|
|
822
|
+
face it because their rules are unexecuted. A delivered rule and one you
|
|
823
|
+
wrote a minute ago are indistinguishable on disk, so an exemption for
|
|
824
|
+
"my own rule" would be an exemption for any rule, and the warning the
|
|
825
|
+
flag prints is accurate in both cases.
|
|
826
|
+
|
|
827
|
+
Without the flag \`test\` prints \`○\` for the rule, says the fixtures did
|
|
828
|
+
not run, names the flag, and exits 0. That is neither a pass nor a
|
|
829
|
+
failure, and it is deliberately not a failure: nothing the holder of an
|
|
830
|
+
unblessed rule can do would make a failure green.
|
|
831
|
+
|
|
832
|
+
Both buckets have to be populated for the rule to pass, and every case
|
|
833
|
+
has to match at least one capture. A case the captures do not match
|
|
834
|
+
never reaches \`check.ts\`, and is reported as a defect in the case rather
|
|
835
|
+
than in the rule, in \`pass/\` as well as \`fail/\`: such a case proves the
|
|
836
|
+
capture did not match, which is a fact about the fixture.
|
|
837
|
+
|
|
838
|
+
## Steps
|
|
839
|
+
|
|
840
|
+
1. **Tell the user what their rule needs, and why it is gated.** Name
|
|
841
|
+
the evidence, "this has to compare two files", "this reads git
|
|
842
|
+
history", and then the consequence: it has to run code, so it needs
|
|
843
|
+
an account. Do not present this as a limitation of the rule.
|
|
844
|
+
|
|
845
|
+
2. **Point them at login.** Obtaining access is \`auth\`'s job, and this
|
|
846
|
+
recipe deliberately does not restate it:
|
|
847
|
+
|
|
848
|
+
\`\`\`
|
|
849
|
+
%(TASKLESS_CLI)s agent auth
|
|
850
|
+
\`\`\`
|
|
851
|
+
|
|
852
|
+
Follow that recipe with the user. If they do not want an account,
|
|
853
|
+
stop here, say the rule cannot run without one, and offer to
|
|
854
|
+
reconsider whether a narrower version of the request could be
|
|
855
|
+
answered by a static rule instead. That is a new routing decision,
|
|
856
|
+
not a fallback you take on their behalf.
|
|
857
|
+
|
|
858
|
+
3. **Once they are logged in, re-route.** Run
|
|
859
|
+
\`%(TASKLESS_CLI)s info --json\` to confirm \`loggedIn: true\`, then
|
|
860
|
+
re-run \`%(TASKLESS_CLI)s agent route\` with the original request. The
|
|
861
|
+
destination changes now that the gate is open.
|
|
862
|
+
|
|
863
|
+
## Important Notes
|
|
864
|
+
|
|
865
|
+
- \`--dangerously-run-scripts\` makes \`check\` and \`test\` execute runtime
|
|
866
|
+
rules without server verification. It exists for local iteration on a
|
|
867
|
+
rule you wrote yourself and just read, which includes running its
|
|
868
|
+
fixtures. On \`test\` it is the only mechanism there is. It is not a way
|
|
869
|
+
to ship a rule to a team, and suggesting it to work around a login
|
|
870
|
+
turns a deliberate gate into an unreviewed code-execution path on
|
|
871
|
+
someone else's machine.
|
|
872
|
+
- Do not author a \`check.ts\` and leave it in the repository unmentioned.
|
|
873
|
+
A skipped runtime rule reports nothing, which reads exactly like a
|
|
874
|
+
passing one.
|
|
875
|
+
|
|
876
|
+
## See Also
|
|
877
|
+
|
|
878
|
+
- \`%(TASKLESS_CLI)s agent auth\`: log in, log out, check status
|
|
879
|
+
- \`%(TASKLESS_CLI)s agent route\`: re-decide once the login state changes
|
|
880
|
+
- \`%(TASKLESS_CLI)s agent check\`: see which rules ran and which were skipped
|
|
881
|
+
`, Y = '# Topic: create-sg-rule (CLI v%(CLI_VERSION)s / topic v5)\n\n## You are here\nThis is `create-sg-rule`. It helps you write an ast-grep rule: a check\nover the structure of a single source file, authored on this machine.\nIf that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route`\nand follow its decision rather than adapting this recipe.\n\n## Goal\nProduce a verified ast-grep rule and its test file, locally, without\ncontacting the Taskless service. You derive the rule yourself, write it\nin the canonical on-disk shape, and validate it with `verify` and\n`test` in a feedback loop. The files you produce match exactly what the\nservice writes, so `check`, `improve-rule`, `verify`, and `test` treat\nthem identically.\n\n## Preconditions\n- `.taskless/` directory exists.\n- The agent can read/write files and run shell commands.\n- No auth required.\n\n## One rule, one directory\n\nEverything defining a rule lives in `.taskless/rules/sg/<id>/`. Writing\na rule means creating that directory; deleting a rule means deleting\nit. No file outside it is touched either way, so two agents authoring\ntwo rules never collide.\n\n```\n.taskless/rules/sg/no-eval/\n no-eval.yml the rule\n .tests/no-eval-YYYYMMDD-test.yml its test cases\n```\n\n**The leading dot on `.tests/` is required.** ast-grep walks the rules\ntree and parses every `.yml` it reaches as a rule, so a plain `tests/`\ndirectory makes it read your test file as a rule and fail the scan for\nthe whole project. A dot-directory is skipped by that walk, and the\ntest runner still finds it. Do not rename it.\n\nUnlike Vale, an `sg` rule needs no second config file. It carries its\nown `files` and `ignores` inside the rule, so the rule file is the\nwhole rule.\n\n## Steps\n\n1. **Learn the ast-grep rule format.** Consult the ast-grep rule\n reference at https://ast-grep.github.io/guide/rule-config.html for\n valid fields and operators (`pattern`, `kind`, `regex`,\n `any`/`all`/`has`/`inside`/`not`) and meta-variable syntax. This\n recipe does not embed the schema. Read it from upstream rather than\n writing a rule from memory.\n\n2. **Gather and confirm the pattern.** Make sure you have concrete\n success cases (code that should pass) and failure cases (code that\n should be flagged), the target language, and any exceptions. Search\n the codebase for real instances and confirm exclusions with the user.\n\n3. **Check for an existing rule that already covers this.** Scan\n `.taskless/rules/sg/` and read each rule\'s `message`, `note`, and\n `rule` fields. If one overlaps, show the user and ask whether they\n would rather improve it, `%(TASKLESS_CLI)s agent improve-rule --anonymous`\n iterates a rule locally.\n\n4. **Author the rule in the canonical shape.** Write the rule to\n `.taskless/rules/sg/<id>/<id>.yml`, where `<id>` is kebab-case and\n names both the directory and the file. At minimum:\n - `id`: kebab-case, matching the filename (e.g. `no-eval`)\n - `language`: the target language, in ast-grep\'s spelling (below)\n - `severity`: `error`, `warning`, `info`, or `hint`\n - `message`: a concise single-line explanation\n - `rule`: the ast-grep rule object\n\n Optional but useful: `note` (multi-line guidance, supports markdown),\n `fix` (auto-fix pattern), `ignores` (file patterns to skip).\n\n **`language` is ast-grep\'s vocabulary.** The vendored rule schema\n types the field as a bare string with no enum, so the schema layer\n accepts anything. `verify` checks it separately: an off-list spelling\n is an error naming the accepted list, and a spelling ast-grep resolves\n but does not itself use (`typescript` for `TypeScript`) is a notice.\n Get it right anyway rather than leaning on that check. An unrecognized\n name aborts config parsing, which takes every other sg rule in the\n project down with it and reports nothing.\n ast-grep (v%(AST_GREP_VERSION)s) parses:\n\n %(AST_GREP_LANGUAGES)s\n\n Copy a spelling from that list rather than typing one that looks\n right. Off-list spellings fail two different ways and neither is\n caught locally: one ast-grep does not recognize at all takes the\n whole scan down (`did not match any variant of untagged enum\n SgLang`, so every other rule goes unreported too), and one it\n recognizes but that names the wrong parser reports nothing and looks\n like a clean codebase.\n\n Two specific traps:\n - **Do not copy from `detect --json`.** It reports the\n *repository\'s* languages in a different vocabulary. It says\n `C++` where the list above says `Cpp`.\n - **`Tsx` and `TypeScript` are two parsers, not aliases.** A rule\n over `.tsx` files that declares `TypeScript` does not match JSX\n syntax; it does not read those files at all.\n - **`Markdown` sees blocks only.** tree-sitter-markdown splits\n block and inline grammars and ast-grep exposes only the block\n tree, so `atx_heading`, `setext_heading`, `fenced_code_block`,\n `list_item`, `paragraph`, `section` and `document` are real kinds\n and everything inside a line is one opaque `inline` node. There is\n no `link`, `emphasis` or `strong_emphasis` kind; naming one is a\n config error that exits 8 and takes the whole scan with it, and\n writing it as a pattern (`[$T]($U)`) matches nothing forever with\n no error at all. A rule about the words inside a line belongs to\n Vale, not here. ast-grep also has no count and no absence\n assertion, so "every doc has an h1" is not expressible as an `sg`\n rule at any severity.\n\n5. **Check any variadic pattern against the separator trap.** A `$$$`\n next to a comma does not mean "zero or more". The `,` in the pattern\n is itself an AST node, and under ast-grep\'s default `smart`\n strictness every node in the pattern must match, so a call with no\n comma cannot match a pattern that has one. Measured against the\n ast-grep this CLI ships (v%(AST_GREP_VERSION)s), given the four calls\n `foo()`, `foo(1)`, `foo(1,2)`, and `foo(1,2,3)`:\n\n| pattern | what it matches |\n|--------------------|----------------------------------------------|\n| `foo($$$)` | all four, `foo()` included |\n| `foo($A, $$$)` | `foo(1,2)` and `foo(1,2,3)`, never `foo(1)` |\n| `foo($$$, $A)` | `foo(1)` alone |\n| `foo($A, $$$, $B)` | `foo(1,2)` alone |\n\n A standalone `$$$` needs none of this. It is the comma beside it\n that narrows the pattern. The two remedies are not the same:\n\n - **Trailing `$$$`**: write the pattern as an object with\n `strictness: ast`, which compares named AST nodes and ignores the\n separator. An object pattern also requires `context` and\n `selector`:\n ```yaml\n rule:\n pattern:\n context: foo($A, $$$)\n selector: call_expression\n strictness: ast\n ```\n This moves the boundary from two arguments to one, **not to\n zero**, `$A` still has to bind something, so `foo()` is still\n unmatched. And `strictness` is valid only inside the pattern\n object: at rule level ast-grep rejects it as an unknown field and\n fails the whole scan.\n - **Leading `$$$`**: `strictness: ast` does not rescue it. Use\n `any` with one branch per arity you mean to cover.\n\n This is upstream\'s intended behaviour (ast-grep/ast-grep#1365,\n closed as working-as-intended), not a bug waiting on a release:\n 0.45.2 behaves identically, so there is no version to wait for.\n\n6. **Write the tests.** Write\n `.taskless/rules/sg/<id>/.tests/<id>-YYYYMMDD-test.yml` with the matching\n `id` field plus `valid` and `invalid` arrays, at least two of each,\n drawn from real code where you can. The `id` must match the rule\'s\n `id` so ast-grep test filtering pairs them. These paths and this\n shape are the same ones the service writes; do not invent a different\n layout.\n\n **Both arrays must be non-empty, and `test` fails the rule if either\n one is.** An empty `invalid:` is not a neutral starting point to fill\n in later: ast-grep reports `1 passed; 0 failed` and exits zero over\n no cases at all, so a rule that matches nothing anywhere looks\n exactly like a rule that works. The `invalid:` bucket is the only\n thing that demonstrates the rule can fire.\n\n Where the rule has an arity boundary (anything from step 5), put a\n case on each side of it. A pattern that starts at two arguments when\n it was meant to start at one passes a test suite whose fixtures all\n have two.\n\n7. **Run the verify feedback loop.** Both commands take the rule\'s\n directory as their argument:\n ```\n %(TASKLESS_CLI)s verify .taskless/rules/sg/<id> --json\n %(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json\n ```\n `verify` asks whether the rule is well-formed: the YAML matches the\n ast-grep schema and every Taskless-required field is present. It\n needs no test file, so run it the moment the rule exists. `test` runs\n the cases, after running `verify` and stopping if that fails, so a\n malformed rule reports the malformation rather than a test complaint.\n\n Both answer in the same shape:\n ```json\n {"ok":true,"rules":[{"engine":"sg","ruleId":"no-eval",\n "ok":true,"errors":[],"ran":true}]}\n ```\n - `ok: true` → go to step 8.\n - `ok: false` → read `errors` and fix. Repeat up to 3 times.\n\n| what `errors` says | fix |\n|--------------------------------------|-------------------------------------------------|\n| the YAML doesn\'t match the schema | check field types against the upstream schema |\n| a Taskless-required field is missing | add `id`/`language`/`severity`/`message`/`rule` |\n| a case didn\'t behave as expected | fix the rule pattern OR the test case |\n\n A `regex` without an accompanying `kind` fails verification, the two\n always travel together.\n\n Pass a directory above a rule and every rule beneath it is checked,\n one entry per rule in the report. `.taskless/rules/sg` covers every\n ast-grep rule; no argument at all covers the project.\n\n8. **On success, report.** Show the rule directory and what is in it,\n plus a one-line summary of what the rule detects. Suggest\n `%(TASKLESS_CLI)s agent check` to validate against the broader codebase.\n\n9. **On failure, escalate, with confirmation.** If after the feedback\n loop the rule still cannot capture the user\'s cases:\n - Delete the candidate `.taskless/rules/sg/<id>/` directory so the\n repo is not left with a broken rule. One `rm -rf` removes the rule\n and its tests together.\n - Tell the user the local rule could not capture the cases, and that\n generating via the Taskless service uses a generation and requires\n login.\n - Only after the user confirms, fetch `%(TASKLESS_CLI)s agent\n create-remote-rule` and follow it. Do not call the service silently.\n\n## Two ways a relational rule matches nothing\n\nBoth of these parse, verify, run, and exit 0. Neither reports anything\never, which reads exactly like a clean codebase.\n\n**`follows` and `precedes` need `stopBy: end` to cross punctuation.**\nA sibling relation walks the immediate siblings, and in most grammars\nthe separators are nodes too. Between two array elements sits a `,`, so\nthe previous element is not the previous SIBLING. Measured on\nTypeScript, `[10, 10, 30]` with\n\n```\nrule:\n kind: number\n follows: { kind: number }\n```\n\nmatches nothing, and the same rule with `stopBy: end` inside `follows`\nmatches. If a sibling relation reports zero on input you believe should\nmatch, add `stopBy: end` before doubting the rest of the rule.\n\n**A `not` containing a bare metavariable excludes everything.** Writing\n"the name is not used in the initializer" as\n\n```\nrule:\n kind: variable_declarator\n has: { field: name, pattern: $A }\n not:\n has: { pattern: $A, stopBy: end }\n```\n\nmatches nothing at all, because `$A` is bound to the name node and that\nnode is its own descendant: the `not` finds it inside every declarator\nand rejects all of them. Scope the negation to the part you meant, in\nthis case the value:\n\n```\n not:\n has:\n field: value\n has: { pattern: $A, stopBy: end }\n```\n\nThe general shape: when a `not` searches the same subtree that bound the\nmetavariable, it always finds it.\n\n## Important Notes\n\n- Do NOT make any HTTP requests to taskless.io on this path.\n- Do NOT write to `.taskless/rule-metadata/`: no rule has a metadata\n sidecar, service-generated ones included, because the CLI never\n writes that directory. Locally authored rules iterate via file edits.\n- The verify loop is the quality gate. A clean failure is a legitimate\n reason to escalate, but only with the user\'s confirmation (step 9).\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent route`: re-decide the destination\n- `%(TASKLESS_CLI)s agent verify-rule`: the `verify` and `test` commands step 7 calls\n- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists\n- `%(TASKLESS_CLI)s agent create-remote-rule`: generate via the service (login)\n- `%(TASKLESS_CLI)s agent check`: validate the new rule against the codebase\n', B = '# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v6)\n\n## You are here\nThis is `create-vale-rule`. It helps you write a Vale rule: a check over\nthe words of a document, prose, markup, and the prose parts of code.\nIf that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route`\nand follow its decision rather than adapting this recipe.\n\n## Goal\nProduce a Vale rule that fires on the prose it should flag and stays\nquiet on the prose it should not, scoped to the files it is about.\n\n## Preconditions\n- `.taskless/` directory exists.\n- The agent can read/write files and run shell commands.\n- No auth required.\n\n## One rule, one directory\n\nEverything that defines a Vale rule lives in\n`.taskless/rules/vale/<id>/`. Writing a rule means creating that\ndirectory. Deleting a rule means deleting it. No file outside it is\ntouched either way, so two agents authoring two rules never collide.\n\n```\n.taskless/rules/vale/no-simply/\n no-simply.yml the style: what the rule looks for\n .vale.ini the scope: which files it applies to\n .tests/fail/bad.md prose it must flag\n .tests/pass/ok.md prose it must leave alone\n```\n\nThe rule is **incomplete until the style and the config both exist**,\nand skipping the config fails silently rather than loudly: the rule\nparses, the check passes, and nothing is ever reported. `verify` exists\nto catch exactly that, so run it (step 6) before you believe a rule\nworks.\n\n**The id appears four times and all four must agree:**\n\n```\n.taskless/rules/vale/no-simply/ <- the directory\n.taskless/rules/vale/no-simply/no-simply.yml <- the style file\nno-simply.no-simply = YES <- the assignment in .vale.ini\ntskl) rule = no-simply <- the breadcrumb in .vale.ini\n```\n\nThe doubled `no-simply.no-simply` is not a typo. Vale names a check\n`<style>.<rule>`, and here the rule\'s own directory is the style, so\nboth halves are the id. Anything else is a check that does not exist,\nwhich Vale accepts without complaint.\n\nYou will not find a project-wide `.vale.ini` to edit. The config Vale\nactually reads is assembled from every rule\'s own file at check time\nand is gitignored. Editing it is pointless, the next check regenerates\nit.\n\n## Steps\n\n1. **Say what the rule reads, then pick an extension point.** Write one\n sentence: "this fires when a document contains ___." Vale rules are\n built by extending one of its twelve checks, and the sentence tells\n you which. Twelve is measured, not counted off the docs: give Vale\n v%(VALE_VERSION)s an `extends` it does not know and it names the whole\n set back at you:\n\n ```\n \'extends\' key must be one of [capitalization conditional consistency\n existence occurrence repetition substitution readability spelling\n sequence metric script].\n ```\n\n The docs enumerate eleven, folding `readability` into `metric`. They\n are separate checks with separate fields.\n\n| If the rule is about… | extends |\n|-----------------------------------------------------------------------------------------|------------------|\n| words or phrases that should not appear | `existence` |\n| preferring one term over another. **including the correct spelling of a product name** | `substitution` |\n| the case of a whole heading or sentence | `capitalization` |\n| how many times something may appear | `occurrence` |\n| a word repeated back to back | `repetition` |\n| picking one of two acceptable spellings, consistently | `consistency` |\n| "if X appears, Y must also appear" | `conditional` |\n| a document-level length or ratio threshold | `metric` |\n| a readability grade, against a named formula | `readability` |\n| a misspelling, against a dictionary | `spelling` |\n| phrases that must appear in a fixed order | `sequence` |\n| anything the above cannot express (Tengo script) | `script` |\n\n **`capitalization` is about a whole scope, not a word.** It asks\n whether an entire heading or sentence matches a case pattern. It\n cannot express "the word GitHub, wherever it appears, is spelled\n thus". That is a `substitution`, because you are swapping a wrong\n spelling for a right one. Reaching for `capitalization` on a product\n name produces a rule that flags whole sentences: measured, a rule with\n `match: GitHub` reports `We host on Github and it is fine. should be\n GitHub`.\n\n For the five this recipe has no worked example of, `metric`,\n `readability`, `spelling`, `sequence`, `script`, read\n https://docs.vale.sh/styles before inventing something. Vale has no\n facility for a rule that does not extend one of these twelve, and an\n `extends` outside the set is not a rule that misbehaves: Vale exits 2\n and **every** Vale rule in the project goes unreported for that run.\n `verify` rejects it before Vale is invoked, and names the twelve.\n\n **The other seven have a worked rule at the end of this recipe**,\n nine rules between them, each with the near-miss that fails and why.\n Read the one closest to your intent before writing anything, the\n mistakes documented there are observed, and most of them fail\n silently.\n\n2. **Write the style file** to\n `.taskless/rules/vale/<id>/<id>.yml`, where `<id>` is kebab-case and\n names both the directory and the file.\n\n **One exception: a `consistency` rule\'s id must be word characters\n only** (`izeise`, not `ize-ise`). That check compiles the id into its\n pattern as a regex group name, and a hyphen there fails the entire\n Vale run. `verify` catches it. Kebab-case is correct for the other\n eleven.\n\n Every rule carries:\n\n| Field | Required | Notes |\n|-----------|----------|-----------------------------------------------|\n| `extends` | yes | one of the twelve above |\n| `message` | yes | shown to the user; see the `%%s` table below |\n| `level` | no | `suggestion` (default), `warning`, or `error` |\n| `scope` | no | narrow to part of a document; see below |\n| `link` | no | a URL the reader can follow for the reasoning |\n| `limit` | no | cap findings from this rule per scope |\n\n **`vocab` is not one of these.** It reads as though it were, but it is\n a per-check field: measured, only `existence`, `substitution`,\n `capitalization`, `conditional` and `repetition` accept it, and\n `occurrence`, `metric`, `readability`, `script` and `sequence` reject\n it. On one of those five it raises `E201` and takes every other Vale\n rule in the project down with it. It is listed with the per-check\n fields below.\n\n **The file extension must be `.yml`.** Measured: rename a working\n style file to `.yaml` and Vale loads nothing, no error, no warning,\n zero findings, and `<id>.<id> = YES` still parses. It is\n indistinguishable from a rule whose pattern never matched.\n\n **`scope` decides where the rule looks**, so getting it wrong is a\n silent under-fire rather than an error. An unrecognized scope is not\n rejected by Vale: `scope: fenced` loads, runs, and matches nothing.\n It is the worst of the three failures on this page, because unlike a\n bad `extends` or a foreign field it does not even take the run down to\n tell you. The rule is inert, forever. `verify` rejects a scope\n outside the table below, which is the only layer that ever will.\n\n Every value below was measured against Vale v%(VALE_VERSION)s by\n authoring a rule with that scope and a document the rule had to flag.\n "Fires" means the finding appeared; a scope that never fired is not on\n this list.\n\n| `scope` | reaches |\n|------------------------|-----------------------------------------------------------|\n| *(omitted)* | everything the format exposes as prose |\n| `text` | prose only, not inline code, not fenced blocks |\n| `code` | inline code spans only |\n| `raw` | the unparsed document: prose, inline code, fenced blocks |\n| `heading` | every heading |\n| `heading.h1`…`h6` | headings of that level |\n| `paragraph` | one paragraph at a time |\n| `sentence` | one sentence at a time |\n| `list` | list items |\n| `blockquote` | quoted blocks |\n| `link` | link text, not the URL |\n| `alt` | image alt text |\n| `summary` | `<summary>` of a disclosure |\n| `strong`, `emphasis` | bold and italic runs |\n| `table` | any part of a table |\n| `table.header` | header cells |\n| `table.cell` | body cells |\n| `table.caption` | a table\'s caption |\n| `figure.caption` | a figure\'s caption, but see below |\n| `frontmatter` | every YAML front-matter value |\n| `frontmatter.<key>` | one front-matter key\'s value |\n| `text.class.<name>` | HTML elements carrying that class |\n| `comment` | every comment, in a comment-tier format |\n| `comment.line` | `//`-style comments |\n| `comment.block` | `/* … */`-style comments |\n\n **`raw` subsumes `code` and `text`.** Measured on one document holding\n the token in prose, in an inline span, and in a fenced block: `text`\n found one, `code` found one, `[code, text]` found two, `raw` found all\n three. If you want prose and inline code but not fenced blocks, write\n the list, `raw` is not "a bit wider", it is everything.\n\n **Vale drops everything inside a `<figure>` element.** Measured: a\n `<figcaption>` nested in `<figure>` is invisible to *every* scope,\n `text` and `raw` included, so a `scope: figure.caption` rule over a\n normally-marked-up figure reports nothing and looks like a bad scope.\n A bare `<figcaption>` is linted, and `figure.caption` fires on it. If a\n fixture is not firing, check whether its subject is inside a `<figure>`\n before you touch the pattern.\n\n **`scope` also takes `~` and `&`.** `~code` is everything but inline\n code; `text & ~code` chains two operands; a list (`[code, text]`) is a\n union. All three parse and behave.\n\n **A negation over a scope Vale does not know is a silent no-op.**\n Measured: `~banana` and `text & ~banana` both fire on everything,\n because there is no such scope to subtract. A typo inside a `~` does\n not narrow the rule and does not widen it visibly. It removes the\n exclusion you wrote the rule for. `verify` checks the operands inside\n `~` and `&` as strictly as a bare one, for exactly this reason, the\n one place it is deliberately stricter than Vale itself.\n\n **`scope` is per-rule, and rules do not interact.** Taskless assembles\n every rule\'s matchers into one config for the run, which invites the\n assumption that one rule\'s `scope` narrows another\'s, or that two\n rules over the same file compete. They do not: each check carries its\n own scope and is evaluated independently. If a rule is over-firing,\n the cause is in that rule\'s own `scope` and glob, never in a\n neighbour\'s.\n\n **A directive turns any rule off, `raw` included, as of Vale 3.20.0.**\n `<!-- vale <id>.<id> = NO -->` opens a zone and `= YES` closes it.\n Vale records the region each directive covers and suppresses any\n alert located inside it, which reaches a `raw`-scoped rule too.\n Through 3.19.0 a directive was applied to the *parsed* document and\n `raw` reads the unparsed one, so a `raw` rule fired straight through\n every zone: a rule about a shell command, a flag, or a package name\n was exempt-or-remove with nothing in between. It is not any more.\n\n One thing to know before writing one over a `raw` rule: at `raw`\n scope the directive line is itself linted text. A rule whose token\n appears in its own id matches the marker that silences it, and\n reports a finding on the directive. Name the rule so its own id does\n not contain the word it looks for.\n\n **A zone is two lines, and no blank line may separate it from the\n prose it wraps.** Measured on Vale 3.20.0:\n\n - Inline at a list item\'s continuation indent works, and this is the\n form to reach for. The pair covers the lines between it and\n nothing else.\n - At column 0 it works too, and it ends any list it interrupts.\n Blocks that then reparse as indented code are already outside a\n prose rule\'s reach, so a zone placed there can move coverage\n rather than restore it.\n - A pair separated from its prose by blank lines, at an indent past\n the code-block threshold, suppresses nothing: what it wraps is not\n prose any more. Nothing reports that, either.\n\n Through 3.19.0 the first of those did nothing at all. An inline pair\n was read once per block, so the `NO` and the `YES` cancelled out\n before the paragraph was linted and the words stayed reported with\n no error and no warning. A zone had to wrap a whole step or a whole\n section at the margin, and exempting the two steps of this recipe\n that quote hedging words would have put 176 lines of prose out of\n reach to keep two words of an example. That price is gone.\n\n **A shown directive has to be inline or fenced, never a bare line.**\n A directive on a line of its own now takes effect on the recipe at\n any indent, and the CLI strips it before serving so a reader never\n sees it. Measured: a directive inside backticks and a directive\n inside a fenced block are both inert, which is why every directive\n quoted in this file is one or the other.\n\n **Then the fields the extension point adds**. This is where the rule\n actually lives, and each check reads only its own:\n\n| extends | its fields |\n|------------------|------------------------------------------------------------------------------|\n| `existence` | `tokens` (a list) or `raw`; `ignorecase`, `nonword`, `exceptions`, `append`, `vocab` |\n| `substitution` | `swap` (a map of observed → expected); `ignorecase`, `nonword`, `exceptions`, `capitalize`, `pos`, `vocab` |\n| `capitalization` | `match`; `style` (with `$title`), `exceptions`, `threshold`, `indicators`, `prefix`, `vocab` |\n| `occurrence` | `token`, `max` and/or `min`; `ignorecase` |\n| `repetition` | `tokens`; `alpha`, `ignorecase`, `exceptions`, `max`, `vocab` |\n| `consistency` | `either` (a map of the two acceptable forms); `ignorecase`, `nonword` |\n| `conditional` | `first`, `second`; `exceptions`, `ignorecase`, `vocab` |\n| `metric` | `formula`, `condition` |\n| `readability` | `metrics` (a list of formula names), `grade` |\n| `spelling` | `aff`, `dic`, `custom`, `filters`, `ignore`, `threshold` |\n| `sequence` | `tokens` (each a `pattern`/`tag` map); `ignorecase`, `exceptions` |\n| `script` | `script` (Tengo source) |\n\n The list above is measured, not transcribed: every entry was added to\n a minimal rule of that check and the run watched for `E201`. Three\n corrections fall out of it, all against the published docs:\n `capitalization` takes `prefix` (singular) and rejects both `prefixes`\n and `suffixes`, it rejects `ignorecase`, and `occurrence` rejects\n `exceptions` and `vocab`.\n\n **Field names are matched case-insensitively, but `extends`, `message`\n and `level` are not.** Measured: `Tokens:` and `ignoreCase:` are read\n exactly as their lowercase spellings, while `EXTENDS:` fails with\n "Missing the required \'extends\' key". Their *values* are case-sensitive\n too, `level: WARNING` and `extends: Existence` are both rejected.\n Write everything lowercase and none of this can bite you.\n\n **A field from the wrong check is the loudest failure Vale has.**\n `tokens` on an `occurrence` check gives\n `E201 … has invalid keys: \'tokens\'`, exit 2, and, because Vale reads\n one assembled config per run. **no** Vale rule in the project reports\n anything. `verify` rejects the rule before Vale is invoked, so this\n cannot reach `check`.\n\n **Two checks are exempt, and that is not a licence.** Measured,\n `consistency` and `spelling` accept any key at all: `bananafield:\n true` on either loads without complaint and is ignored. A misspelled\n field on those two is a silent no-op instead of a loud one, so the\n schema cannot catch a typo there and neither can Vale. Read the field\n list twice when writing those two.\n\n **What `%%s` fills with depends on the extension point.** Getting this\n wrong is the one mistake in this recipe that passes every check below\n. The rule fires, the fixtures are green, and only a human reading the\n message sees that it is nonsense.\n\n| extends | `%%s` count | fills with, left to right |\n|------------------|-------------|-------------------------------------------------------|\n| `existence` | one | the matched text |\n| `substitution` | **two** | the **replacement**, then the matched text |\n| `capitalization` | one | the scope that failed (the whole heading or sentence) |\n\n Measured: a `substitution` message with a single `%%s` interpolates the\n *replacement*, not the match, so `"Use GitHub not %%s"` against the text\n `Github` renders `Use GitHub not GitHub`.\n\n For the other nine, do not guess. Write the message, run step 6, and\n read it back off the finding, no test you can write catches a wrong\n `%%s`, so your own eyes on the rendered message are the check.\n\n ```yaml\n # existence, flag these tokens wherever they appear\n extends: existence\n message: "Avoid \'%%s\', it hides the work from the reader"\n level: warning\n ignorecase: true\n tokens:\n - simply\n - just\n ```\n\n ```yaml\n # substitution, first %%s is the replacement, second is what was found\n extends: substitution\n message: "Use \'%%s\' instead of \'%%s\'"\n level: warning\n ignorecase: true\n swap:\n utilize: use\n "in order to": to\n ```\n\n ```yaml\n # capitalization, a whole heading must be in sentence case\n extends: capitalization\n message: "\'%%s\' should be in sentence case"\n level: warning\n scope: heading\n match: $sentence\n exceptions:\n - Taskless\n - API\n ```\n\n `match` takes `$sentence`, `$title`, `$lower`, or `$upper`. A literal\n string is legal but means "this whole scope must read exactly that",\n which is almost never what anyone wants. See step 1.\n\n **`$sentence` means first word capitalized, everything else lowercase\n, proper nouns included.** It is not "sentence case allowing proper\n nouns". Measured with `exceptions: [Taskless, API]` on headings:\n\n| Heading | Result |\n|-----------------------------------|------------------------------------------------|\n| `Getting started with the API` | quiet |\n| `Getting started with APIs` | quiet, an exception covers its plural |\n| `Taskless and the API` | quiet, an exception may lead the scope |\n| `Getting started with Kubernetes` | **fires**: a proper noun you did not list |\n| `getting started lowercase` | **fires**: the first word must be capitalized |\n| `Getting Started With Title Case` | **fires** |\n\n So `exceptions` is not decoration: every proper noun, product name and\n acronym the docs use has to be listed, or the rule flags correct\n headings. Collect them from the docs before writing the rule, and\n expect to add to the list.\n\n3. **Know what you are writing: `tokens` and `swap` keys are patterns,\n not literals.** They compile as **Go RE2** regular expressions.\n\n *This step is about `tokens` and `swap` only. A `capitalization`,\n `occurrence` or `metric` rule has neither, skip to step 4.*\n\n - `(?:…)`, `[…]`, `|`, `+`, `?` all work.\n - **Lookahead and lookbehind do not exist in RE2.** A rule that needs\n "X but not when followed by Y" cannot be written as a single\n `substitution`; split it or narrow with `scope`.\n - **Word boundaries are applied for you, around the whole pattern.**\n Measured: `Github` does not fire inside `GithubToken`, and the\n multi-word `click here` does not fire inside `Clicking here`.\n - **A hyphen is a boundary, so a hyphenated compound is not\n protected.** `obviously` fires inside `obviously-named`, while\n `obviously_stale` is safe because `_` is a word character. Do not\n reach for a hyphenated compound as a `pass/` near-miss; it is the\n case most likely to fire.\n - **Regex metacharacters in a real phrase are live.** "maybe?" is a\n pattern meaning "mayb" followed by an optional "e". Escape it.\n - **Overlapping alternatives resolve first-wins**, one finding per\n match. If `can login` and `login with` both match a sentence, you\n get whichever is written first, once, not both.\n - `ignorecase: true` matches any casing **and still skips text that\n already equals the replacement.** Measured with `Github: GitHub`:\n `github` and `Github` are flagged, `GitHub` is not. You do not need\n `ignorecase: false` to protect the correct spelling.\n - `raw` takes a full regex when `tokens` is too restrictive;\n `nonword` removes the implicit boundaries.\n - **A token made only of punctuation can never match without\n `nonword: true`.** The boundaries above are `\\b`, which needs a word\n character on the inside. An em dash has none, on either side.\n Measured against `This is a sentence, with an em dash.`:\n\n ```yaml\n # fires on nothing, ever, and reports no error\n extends: existence\n message: "Use a comma, not an em dash"\n tokens:\n - \', \'\n ```\n\n ```yaml\n # fires\n extends: existence\n message: "Use a comma, not an em dash"\n nonword: true\n tokens:\n - \', \'\n ```\n\n The first rule verifies, tests green if its `fail/` fixture is\n missing the dash, and reports nothing forever. Any token whose\n pattern contains no `\\w` (punctuation, an emoji, a bare symbol)\n needs `nonword: true`.\n\n - **A bare word finds senses you did not mean.** `landed on` in a rule\n <!-- vale no-hedging.no-hedging = NO -->\n about jargon also matches "the plane landed on time"; `simply` in a\n rule about hedging also matches "simply connected" in a maths doc.\n <!-- vale no-hedging.no-hedging = YES -->\n Narrow the token to the **collocation** you actually object to\n (`landed on a decision`, not `landed on`), and check that you got it\n right by writing the `pass/` fixture from the literal sense *first*:\n put the innocent sentence in `pass/` before you write the guilty one\n in `fail/`. A rule whose `pass/` bucket was written afterwards tends\n to contain only sentences the author already knew were safe.\n\n Vale also understands the markup, which decides what counts as text\n before your pattern ever runs. Measured in markdown:\n\n - **URLs and code spans are not prose.** A `Github` key fires on\n `Plain Github here` and not on `https://Github.com/x` or\n `` `Github/docs` ``.\n - **Link text *is* prose.** In `[click here](https://example.com)`,\n `click here` is matched. The URL is not. With no `scope`, a rule\n fires on both link text and ordinary prose; `scope: link` narrows it\n to link text alone. Measured: without a scope the token hit both the\n link and the sentence; with `scope: link`, only the link.\n\n4. **Scope the rule** by writing `.taskless/rules/vale/<id>/.vale.ini`.\n This is the step that is easy to skip and impossible to notice\n skipping. A rule with no config is enabled nowhere: it parses, it\n runs, and it reports nothing.\n\n The whole file, for a rule that applies to markdown:\n\n ```ini\n # Which files this rule applies to.\n [*.md]\n tskl) rule = no-simply\n BasedOnStyles =\n no-simply.no-simply = YES\n ```\n\n Four lines, and each one earns its place:\n\n - `[*.md]` is a **matcher**: a glob over paths, deciding which files\n this rule sees. Match it to the files the rule is actually about,\n such as `[*.{md,markdown}]` or `[docs/**/*.md]`. A rule can declare\n several matchers if it needs to. Before you widen a glob, check the\n reach table below, what Vale does to a file it cannot parse is not\n "nothing".\n - `tskl) rule = <id>` is a breadcrumb Taskless reads to attribute the\n matcher back to this rule after assembly interleaves every rule\'s\n matchers into one file. Vale parses the key and ignores it. Write it\n in every matcher you add, or the tooling loses track of who owns\n what.\n - `BasedOnStyles =` with an empty value says explicitly that no\n bundled style loads. The default is already empty, so this changes\n nothing today. Write it anyway: it tells the next reader that no\n style is missing.\n - `no-simply.no-simply = YES` turns the rule on. The first half is\n the style, which is this rule\'s directory; the second is the check\n inside it, which is the file. Both are the id.\n\n **Scope a rule *out* with a second matcher, not a cleverer glob.** A\n glob says which files a rule sees; it has no way to say "these but not\n those". The exclusion is a second matcher that assigns `NO`, and\n because precedence here is positional (a later matcher wins), the\n exclusion goes **after** the inclusion:\n\n ```ini\n # Every markdown file…\n [*.md]\n tskl) rule = no-simply\n BasedOnStyles =\n no-simply.no-simply = YES\n\n # …except the changelog, which quotes release notes verbatim.\n [CHANGELOG.md]\n tskl) rule = no-simply\n no-simply.no-simply = NO\n ```\n\n Write the breadcrumb in the second matcher too, or assembly attributes\n it to nobody. Reversing the two blocks silently re-enables the rule on\n the file you meant to exempt, and nothing reports that.\n\n **Do NOT write `StylesPath` or `MinAlertLevel` here.** Those describe\n the run rather than a rule, the assembler supplies them, and a copy\n in a rule\'s config is dropped on the way in.\n\n Keep assignments underneath a matcher. An assignment above the first\n `[…]` line belongs to no matcher, and Vale ignores it after warning\n on stderr.\n\n **What a matcher\'s glob is allowed to catch.** Vale (v%(VALE_VERSION)s)\n treats a file one of four ways, decided by extension. The lists are\n rendered from the pinned Vale version, not written out here, so they\n track the shipped binary.\n\n - **markup**: the document is prose and the format\'s own non-prose\n constructs are skipped. This is the tier every `scope:` value\n assumes; `scope: heading` has nothing to find outside it:\n %(VALE_MARKUP_FORMATS)s\n - **comment text only**: the comments are linted and the code body\n <!-- vale no-hedging.no-hedging = NO -->\n is invisible, which is exactly right for "comments must not say\n \'obviously\'":\n <!-- vale no-hedging.no-hedging = YES -->\n %(VALE_COMMENT_FORMATS)s\n - **plaintext fallback**: everything else, `.yml` `.toml` `.sh`\n `.sql` and every extension not named above included. There is no\n parser, so the whole file is linted as prose: a rule matched to\n YAML flags key names and values, not just the comments. If that is\n not what the rule means, narrow the glob rather than accepting it.\n These land here despite reading like markup, so a `scope:` value\n has nothing to act on in them: %(VALE_PLAINTEXT_FORMATS)s\n - **not supported**: Vale parses these only by shelling out to an\n external program, and this build does not support any format that\n needs one:\n %(VALE_CONVERTER_FORMATS)s\n\n Do not tell the user to install the program. Taskless excludes these\n files from the run whatever is installed, so that a repository\n checks the same way on every machine; `.xml` could not work anyway,\n since an XSLT stylesheet is specific to the document.\n\n **A single unreadable file fails the whole Vale pass.** Vale exits 2\n with an `E100` runtime error and abandons the run, `--no-exit` does\n not suppress it, so every other Vale rule over every other file goes\n unreported. `[*.{md,typ}]` is not a slightly wider `[*.md]`; it is a\n matcher that takes `check` down the first time the repo grows a\n `.typ` file. Never put one of those extensions in a glob.\n\n That example changed with Vale v3.18.0, which is the point: the\n dangerous extension is whichever one the list above says needs a\n program, not the one you remember. `.mdx` was the example until that\n release parsed it natively, and `.typ` took its place.\n\n **`.mdx` is supported** as of Vale v3.18.0, which parses it natively\n rather than shelling out. `[*.{md,mdx}]` is a legitimate matcher\n again, the example this recipe used to warn about is no longer the\n broken one. Check the lists above rather than reaching for that\n memory: `.typ` moved the opposite way in the same release, so a\n matcher covering Typst is now the one that takes the run down.\n\n **In `.mdx`, a component\'s children are prose as of v3.19.0.** Vale\n reads a JSX element\'s children as the Markdown they are, so text\n inside a wrapping component (`<Steps>`, `<Tabs>`, `<Aside>`) is\n linted at its own source position. Only tags, attributes, `{...}`\n expressions, self-closing elements, and an element opened and closed\n on one standalone line are still treated as code.\n\n Two consequences for a rule you write against MDX. Coverage grew,\n so a rule can now fire in prose it never reached before, which is a\n finding count that moves without the rule changing. And the children\n carry the element\'s name as a class scope, exactly as MyST and\n Quarto directives do, so `scope: text.class.Aside` targets one\n component\'s content. That is an open family: `verify` accepts any\n `text.class.<name>` tail, because the set of component names is the\n author\'s, not Vale\'s.\n\n5. **Write the fixtures.** Two directories inside the rule, both flat.\n Vale lints the whole fixture tree, so a document nested a level\n deeper would be linted and never checked against either bucket, which\n `test` rejects by name rather than skipping. Keep both one level\n deep:\n\n ```\n .taskless/rules/vale/<id>/.tests/pass/ok.md # rule must stay quiet\n .taskless/rules/vale/<id>/.tests/fail/bad.md # rule must fire\n ```\n\n **The leading dot on `.tests/` is required.** ast-grep walks the\n rules tree and parses every `.yml` it reaches as a rule, and a plain\n `tests/` directory fails that scan for the whole project. A\n dot-directory is skipped. Do not rename it.\n\n Give the fixtures an extension your matcher\'s glob matches. A `.txt`\n fixture under a `[*.md]` matcher is never linted, so the `fail/`\n document silently passes.\n\n **The `pass/` bucket is not "correct prose".** Correct prose proves\n nothing. The rule was never going to fire on it. Fill it with the\n near-misses that would catch an over-broad pattern. What counts as a\n near-miss depends on the rule\'s shape:\n\n - **`tokens`/`swap` rules**: the noun form you are not flagging, the\n word inside a longer word, the term in a URL or a code span, the\n correct spelling itself.\n - **`scope`d rules**: the same phrase *outside* the scope. A rule\n with `scope: link` needs the phrase in ordinary prose; a rule with\n `scope: heading` needs it in body text. Without that, nothing proves\n the scope is doing anything.\n - **`capitalization` rules**: a scope that is entirely exceptions, a\n scope whose exception word comes first, and the plural of an\n exception.\n\n That is the half of the fixture set that has to work for you.\n\n **When the rule\'s subject normally appears in code, the `fail/`\n fixture must carry it three ways**, inline in a code span, inside a\n fenced block, and in ordinary prose, in that one document. A rule\n about a command, a flag, a package name or an env var has a subject\n that lives in fenced blocks in every real README, and the default\n scope cannot see fenced blocks at all. A `fail/` fixture written only\n in prose therefore fires, goes green, and the rule then catches none\n of the real violations. Measured on one document holding the token in\n all three places: the default scope found one of three, `raw` found\n three. If the fixture fires on the prose line and not on the other\n two, the answer is `scope: raw`. See step 2 for what that costs.\n\n **Fixtures run under a config that isolates this rule, so a green\n `test` is not evidence the rule reaches any real file.** `test`\n generates its own `.vale.ini` pointing at the fixture directory and\n enabling only `<id>.<id>`; your rule\'s own matcher globs are not\n consulted. So a glob of `packages/cli/src/**/*.ts` that matches\n nothing in the repository still produces a rule that verifies, tests\n green, and reports forever. The only check for that is a real\n `check` over a real file:\n\n ```\n %(TASKLESS_CLI)s check <a real path the rule should flag> --json\n ```\n\n Do that once, on a file you have deliberately made violate the rule,\n before you believe the rule works.\n\n6. **Verify, then test.** Two commands, both taking the rule\'s\n directory as their argument, both run from the project root:\n\n ```\n %(TASKLESS_CLI)s verify .taskless/rules/vale/<id> --json\n %(TASKLESS_CLI)s test .taskless/rules/vale/<id> --json\n ```\n\n `verify` asks whether the rule is well-formed: the style file parses,\n `extends` names one of the twelve checks, `message` is present, `level`\n is one Vale accepts, every `scope` operand is one Vale honors, every\n field belongs to the check the rule extends, and the config declares a\n matcher that enables `<id>.<id>`. It does **not** need fixtures, so run\n it as soon as the style file exists.\n\n Those checks are measured against Vale v%(VALE_VERSION)s rather than\n transcribed from its docs, and they run **before** Vale is invoked.\n That ordering matters for two of them: an unknown `extends` and a\n foreign field each fail the whole Vale run rather than just this rule,\n so letting either reach the binary would take every other Vale rule\'s\n findings down with it.\n\n `test` runs the rule against both buckets. It runs `verify` first and\n stops if that fails, so a malformed rule tells you what is malformed\n instead of complaining about fixtures.\n\n Both report the same shape:\n\n ```json\n {"ok":true,"rules":[{"engine":"vale","ruleId":"no-simply",\n "ok":true,"errors":[],"ran":true}]}\n ```\n\n `ok` is the answer. `errors` names what failed, one string per\n problem. Exit code is 0 when every rule passed and 1 otherwise, so\n both are safe to script.\n\n Pass a directory above a rule and every rule beneath it is checked,\n reported one entry per rule. `.taskless/rules/vale` covers every Vale\n rule; no argument at all covers the project.\n\n If you would rather see the raw findings, the message text and the\n line numbers, run `check` against a bucket instead:\n\n ```\n %(TASKLESS_CLI)s check .taskless/rules/vale/<id>/.tests/fail --json\n ```\n\n Read `results` there. Ignore `success` and the exit code: `success`\n says the run worked rather than that the fixture behaved, and the\n exit code follows severity, so a `level: error` rule exits 1 on\n `fail/` while a `warning` rule exits 0 and both are correct. `test`\n answers pass-or-fail; `check` shows you the finding.\n\n When a `fail/` document does not fire, work down this list before\n touching the pattern. The cause is usually further up:\n - Does the rule have a `.vale.ini` at all?\n - Is the assignment underneath a `[…]` matcher?\n - Is it spelled `<id>.<id>`, both halves the same?\n - Does the matcher\'s glob match the fixture\'s extension?\n - Only then: does the pattern actually match the text?\n\n A `pass/` document that fires means the pattern is too broad. Look\n for a missing word boundary, an unescaped metacharacter, or a swap\n key that also matches the form you meant to allow.\n\n7. **Report.** Show the rule directory you created and what is in it, a\n one-line summary of what the rule flags, and the glob it is scoped\n to. The scope is a decision the user should see rather than one\n buried in a config. Note that a whole-project `%(TASKLESS_CLI)s check` skips\n your fixtures: `.taskless/` is excluded from the project walk, by\n design. `test` is what exercises them.\n\n## Worked rules\n\nNine rules that work, each paired with the near-miss that fails. Every\none was run against the bundled Vale; the "what goes wrong" lines are\nobserved behavior, not warnings in principle. Find the entry closest to\nyour intent and start there.\n\n### 1. Ban a word or phrase, `existence`\n\n> "Our docs shouldn\'t hedge."\n\n```yaml\nextends: existence\nmessage: "Avoid hedging: \'%%s\'"\nlevel: warning\nignorecase: true\ntokens:\n - we think\n - it seems\n - sort of\n```\n\n**Goes wrong:** dropping `ignorecase: true` when you meant any casing.\n`We think` at the start of a sentence then sails through. And a phrase\nwith punctuation is a *pattern*: `maybe?` means "mayb" plus an optional\n"e", so it matches `mayb`. Escape it: `maybe\\?`.\n\n### 2. Prefer one term over another, `substitution`\n\n> "Say \'sign in\', not \'login\', when it\'s a verb."\n\n```yaml\nextends: substitution\nmessage: "Use \'%%s\' instead of \'%%s\'"\nlevel: warning\nignorecase: true\nswap:\n \'login (?:to|into)\': sign in to\n \'to login\': to sign in\n```\n\n**Goes wrong:** one `%%s` instead of two. Measured, `"Use sign in not\n%%s"` against `login to` renders **"Use sign in not sign in to"**, the\nreplacement, twice. The rule fires, both fixtures pass, and only a human\nreading the message sees it. Two `%%s`, always, in that order.\n\n### 3. Enforce a product\'s spelling, `substitution`, not `capitalization`\n\n> "It\'s \'GitHub\', never \'Github\' or \'github\'."\n\n```yaml\nextends: substitution\nmessage: "Use \'%%s\' instead of \'%%s\'"\nlevel: error\nignorecase: true\nswap:\n github: GitHub\n```\n\n**Goes wrong:** reaching for `capitalization` because the complaint is\nabout capitals. Measured, `match: GitHub` flags whole sentences:\n`\'We host on Github and it is fine. should be GitHub\'`, because that\ncheck tests a *scope*, not a word. Note also that `ignorecase: true` is\nsafe here: Vale skips text already equal to the replacement, so the\ncorrect `GitHub` is not flagged.\n\n### 4. Sentence-case headings, `capitalization`\n\n> "Headings are sentence case; our product names keep their capitals."\n\n```yaml\nextends: capitalization\nmessage: "\'%%s\' should be in sentence case"\nlevel: warning\nscope: heading\nmatch: $sentence\nexceptions:\n - Taskless\n - API\n - Kubernetes\n```\n\n**Goes wrong:** a short `exceptions` list. `$sentence` lowercases\neverything after the first word, proper nouns included, so every product\nname and acronym in the docs must be listed or correct headings get\nflagged. Collect them from the docs first; expect to add more.\n\n### 5. Restrict a rule to link text, any check, plus `scope`\n\n> "\'click here\' is useless link text."\n\n```yaml\nextends: existence\nmessage: "Link text \'%%s\' says nothing, name the destination"\nlevel: warning\nscope: link\nignorecase: true\ntokens:\n - click here\n - read more\n```\n\n**Goes wrong:** omitting `scope: link`. Measured, the token then fires\non `[click here](…)` **and** on "click here to focus the search box" in\nordinary prose, which is a false positive on a sentence that is fine.\nWhenever a rule is about a *place* in the document, the `pass/` fixture\nmust contain the same phrase outside that place, otherwise nothing\nproves the scope works.\n\n### 6. Cap how often something appears, `occurrence`\n\n> "At most one exclamation mark per paragraph."\n\n```yaml\nextends: occurrence\nmessage: "Too many exclamation marks"\nlevel: warning\nscope: paragraph\ntoken: "!"\nmax: 1\n```\n\n**Goes wrong:** forgetting `scope`. The count is per scope, so with no\nscope you are capping the whole document rather than the paragraph.\nNote `token` here is singular, this check takes one, not a `tokens` list.\n\n### 7. Catch a doubled word, `repetition`\n\n> "\'the the\' keeps slipping through review."\n\n```yaml\nextends: repetition\nmessage: "\'%%s\' is repeated"\nlevel: warning\nalpha: true\ntokens:\n - \'[^\\s]+\'\n```\n\n**Goes wrong:** leaving the pattern unquoted. Measured, an unquoted\n`[^\\s]+` in YAML silently matches nothing, zero findings, no error, no\ndiagnostic. Quote any pattern containing a backslash. This is the\nfailure mode this recipe warns about most, arriving through YAML rather\nthan through Vale.\n\n### 8. One spelling or the other, consistently, `consistency`\n\n> "Pick -ize or -ise and stick to it."\n\n```yaml\nextends: consistency\nmessage: "Use \'%%s\' consistently"\nlevel: warning\nnonword: true\neither:\n organize: organise\n```\n\n**The id must be word characters only.** `consistency` is the one\nextension point that compiles the rule\'s own name into the pattern, as\na `(?P<id>…)` capture group, and Go RE2 rejects a group name containing\na hyphen. Measured: an id of `ize-ise` fails with `E201 … invalid group\nname` and takes **every** Vale rule in the project down with it, because\nVale reads one config for the whole run. Name this one `izeise` or\n`spelling_variants`. Kebab-case is right everywhere else.\n\n**Goes wrong:** expecting it to pick a winner. It flags the *second*\nform once both appear in a document, it enforces internal consistency,\nnot house style. If you want one specific spelling, that is a\n`substitution`.\n\n### 9. Require a definition, `conditional`\n\n> "An acronym must be spelled out before it\'s used."\n\n```yaml\nextends: conditional\nmessage: "\'%%s\' has no definition"\nlevel: warning\nscope: text\nignorecase: false\nfirst: \'\\b([A-Z]{3,5})\\b\'\nsecond: \'(?:\\b[A-Z][a-z]+ )+\\(([A-Z]{3,5})\\)\'\n```\n\n`first` is what must be justified; `second` is what justifies it.\nMeasured: `Application Programming Interface (API)` licenses every later\n`API`, while an undefined `XYZ` is flagged.\n\n**Goes wrong:** swapping the two, which inverts the rule into "flag the\ndefinition when the acronym is missing".\n\n## Important Notes\n\n- Vale reads one document at a time and has no cross-document view. A\n rule about consistency *between* documents cannot be written here.\n- Prose inside code is still prose: comments and docstrings are Vale\'s\n subject, and a rule about them belongs under a matcher whose glob\n covers the source files.\n- Do NOT add a `[*]` matcher to widen a rule that isn\'t firing. Matchers\n from every rule are assembled into one config, so `[*]` applies this\n rule to every file the walk reaches and turns one rule\'s scoping bug\n into a flood of false positives.\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent route`: re-decide the destination\n- `%(TASKLESS_CLI)s agent check`: run every engine over the repo\n- `%(TASKLESS_CLI)s agent create-sg-rule`: author a rule over code structure\n', X = `# Topic: delete-rule (CLI v%(CLI_VERSION)s / topic v4)
|
|
882
|
+
|
|
883
|
+
## Goal
|
|
884
|
+
Remove a rule and its associated test files from \`.taskless/\`. Does not
|
|
885
|
+
contact the Taskless API; purely a local filesystem operation.
|
|
886
|
+
|
|
887
|
+
\`rule delete\` takes a bare id, not a path, and resolves which engine
|
|
888
|
+
holds it: it searches \`.taskless/rules/sg/\`, \`vale/\` and \`runtime/\`.
|
|
889
|
+
Deleting the directory by hand instead skips the metadata sidecar the
|
|
890
|
+
CLI also removes, and skips the ambiguity check below.
|
|
891
|
+
|
|
892
|
+
An id is not unique across engines. Nothing enforces uniqueness, so two
|
|
893
|
+
engines can hold the same id, and the CLI refuses that case rather than
|
|
894
|
+
picking one.
|
|
895
|
+
|
|
896
|
+
## Preconditions
|
|
897
|
+
- \`.taskless/\` directory exists.
|
|
898
|
+
- The target rule directory exists under \`.taskless/rules/<engine>/<id>/\`
|
|
899
|
+
for exactly one engine.
|
|
900
|
+
- No auth required.
|
|
901
|
+
|
|
902
|
+
## Steps
|
|
903
|
+
|
|
904
|
+
1. **Identify the rule.** If the user named one, use it. Otherwise,
|
|
905
|
+
list the engine directories under \`.taskless/rules/\` and ask which
|
|
906
|
+
rule. Confirm the user's intent, deletion is destructive.
|
|
907
|
+
|
|
908
|
+
2. **Invoke the CLI.** Run:
|
|
909
|
+
\`\`\`
|
|
910
|
+
%(TASKLESS_CLI)s rule delete <id>
|
|
911
|
+
\`\`\`
|
|
912
|
+
|
|
913
|
+
The CLI removes:
|
|
914
|
+
- \`.taskless/rules/<engine>/<id>/\`: the whole directory, which holds
|
|
915
|
+
the rule, its \`.tests/\`, and any per-rule config
|
|
916
|
+
- \`.taskless/rule-metadata/<id>.yml\`, a sidecar the CLI never
|
|
917
|
+
writes. The removal is there for a directory a person created by
|
|
918
|
+
hand; expect it to be absent.
|
|
919
|
+
|
|
920
|
+
3. **Report results.** Show the user what was deleted.
|
|
921
|
+
|
|
922
|
+
## Errors
|
|
923
|
+
|
|
924
|
+
When \`--json\` is set, failures emit \`{ ok: false, code, message }\` on
|
|
925
|
+
stdout; on success the command exits 0 silently (no envelope).
|
|
926
|
+
|
|
927
|
+
| code | meaning | fix |
|
|
928
|
+
|---------------------|----------------------------------------|----------------------------------------------|
|
|
929
|
+
| \`RULE_NOT_FOUND\` | No engine holds that id | Confirm the ID; list rules first |
|
|
930
|
+
| \`RULE_ID_AMBIGUOUS\` | Two engines hold it, so nothing was deleted | Remove the one you mean by path; the message names both |
|
|
931
|
+
|
|
932
|
+
\`RULE_ID_AMBIGUOUS\` is not a retry. The id is well-formed and correct, and it
|
|
933
|
+
matches more than one rule, so sending it again produces the same answer.
|
|
934
|
+
Nothing is deleted when it is reported: the command refuses rather than picking
|
|
935
|
+
one, because picking one removed a rule the caller may not have meant and
|
|
936
|
+
reported success.
|
|
937
|
+
|
|
938
|
+
The rule ID is required as a positional argument; citty rejects a
|
|
939
|
+
missing ID before the command body runs, so \`INVALID_INPUT\` is not
|
|
940
|
+
emitted from this command.
|
|
941
|
+
|
|
942
|
+
## See Also
|
|
943
|
+
|
|
944
|
+
- \`%(TASKLESS_CLI)s agent route\`: make a new rule
|
|
945
|
+
- \`%(TASKLESS_CLI)s agent check\`: run remaining rules to confirm nothing broke
|
|
946
|
+
`, J = `# Topic: detect (CLI v%(CLI_VERSION)s / topic v1)
|
|
947
|
+
|
|
948
|
+
## Goal
|
|
949
|
+
Scan the working directory for the linters it configures, the
|
|
950
|
+
languages it uses, and the styles of any rules the repo already
|
|
951
|
+
authors. Offline and deterministic, no network, no auth, no state
|
|
952
|
+
change. This is the discovery step that feeds rule-authoring: the
|
|
953
|
+
routing flow reads \`detect\` to decide where a new rule should live.
|
|
954
|
+
|
|
955
|
+
## Preconditions
|
|
956
|
+
- None. Works in any directory; doesn't require \`.taskless/\`.
|
|
957
|
+
|
|
958
|
+
## Steps
|
|
959
|
+
|
|
960
|
+
1. **Invoke the CLI** with JSON output:
|
|
961
|
+
\`\`\`
|
|
962
|
+
%(TASKLESS_CLI)s detect --json
|
|
963
|
+
\`\`\`
|
|
964
|
+
|
|
965
|
+
2. **Parse the response.** Shape:
|
|
966
|
+
\`\`\`json
|
|
967
|
+
{
|
|
968
|
+
"success": true,
|
|
969
|
+
"linters": [
|
|
970
|
+
{
|
|
971
|
+
"name": "eslint",
|
|
972
|
+
"evidence": ["eslint.config.js", "dependency eslint (package.json)"]
|
|
973
|
+
}
|
|
974
|
+
],
|
|
975
|
+
"languages": ["typescript", "javascript"],
|
|
976
|
+
"ruleStyles": [
|
|
977
|
+
{
|
|
978
|
+
"source": ".taskless/rules/sg",
|
|
979
|
+
"description": "ast-grep rules with YAML metadata sidecars"
|
|
980
|
+
}
|
|
981
|
+
]
|
|
982
|
+
}
|
|
983
|
+
\`\`\`
|
|
984
|
+
- \`linters\`: each has a \`name\` and \`evidence\` (config-file paths,
|
|
985
|
+
a pyproject table marker, or a dependency marker from the
|
|
986
|
+
language's package file; not every entry is a path).
|
|
987
|
+
- \`languages\`: inferred from manifests and the detected linters.
|
|
988
|
+
- \`ruleStyles\`: how the repo authors its own rules, surfaced for
|
|
989
|
+
downstream reuse.
|
|
990
|
+
|
|
991
|
+
3. **Use the signals to route.** Feed the output into rule authoring:
|
|
992
|
+
- A detected linter the repo already uses → author the rule there
|
|
993
|
+
(\`%(TASKLESS_CLI)s agent route\`).
|
|
994
|
+
- No suitable linter, local-only → \`%(TASKLESS_CLI)s agent create-sg-rule\`.
|
|
995
|
+
- See \`%(TASKLESS_CLI)s agent route\` for the full decision.
|
|
996
|
+
|
|
997
|
+
## Errors
|
|
998
|
+
|
|
999
|
+
When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
|
|
1000
|
+
|
|
1001
|
+
| code | meaning | fix |
|
|
1002
|
+
|------------------|----------------------------|--------------------------|
|
|
1003
|
+
| \`INTERNAL_ERROR\` | Internal schema validation | Report; likely a CLI bug |
|
|
1004
|
+
|
|
1005
|
+
## See Also
|
|
1006
|
+
|
|
1007
|
+
- \`%(TASKLESS_CLI)s agent route\`: decide where to author a rule from these signals
|
|
1008
|
+
- \`%(TASKLESS_CLI)s agent check\`: run rules against the codebase
|
|
1009
|
+
`, Q = `# Topic: improve-rule (anonymous) (CLI v%(CLI_VERSION)s / topic v2)
|
|
1010
|
+
|
|
1011
|
+
## Goal
|
|
1012
|
+
Iterate on an existing ast-grep rule **locally** without contacting
|
|
1013
|
+
the Taskless API. You (the agent) edit the rule YAML directly, then
|
|
1014
|
+
validate with \`verify\` and \`test\` in a feedback loop.
|
|
1015
|
+
|
|
1016
|
+
## Preconditions
|
|
1017
|
+
- \`.taskless/\` directory exists and contains the target rule.
|
|
1018
|
+
- The agent can read/write files and run shell commands.
|
|
1019
|
+
- No auth required.
|
|
1020
|
+
|
|
1021
|
+
## Steps
|
|
1022
|
+
|
|
1023
|
+
1. **Identify the target rule.** If the user named one, use it.
|
|
1024
|
+
Otherwise, list \`.taskless/rules/sg/\` and ask which one. Read the
|
|
1025
|
+
rule file and any test file under
|
|
1026
|
+
\`.taskless/rules/sg/<id>/.tests/\`.
|
|
1027
|
+
|
|
1028
|
+
2. **Gather improvement guidance.** Ask:
|
|
1029
|
+
- Are there false positives (cases currently flagged but
|
|
1030
|
+
shouldn't be)? Get concrete examples.
|
|
1031
|
+
- Are there false negatives (cases passing but should be
|
|
1032
|
+
caught)? Get concrete examples.
|
|
1033
|
+
- Should the message, severity, or note change?
|
|
1034
|
+
|
|
1035
|
+
3. **Plan the change.** Decide whether to:
|
|
1036
|
+
- Tighten the \`rule\` (add \`inside\`, \`not\`, more specific \`kind\`)
|
|
1037
|
+
- Loosen the \`rule\` (relax constraints, add \`any\` alternatives)
|
|
1038
|
+
- Add \`ignores\` patterns for files/paths to skip
|
|
1039
|
+
- Adjust \`message\`/\`severity\`/\`note\`
|
|
1040
|
+
Summarize the planned change before editing.
|
|
1041
|
+
|
|
1042
|
+
4. **Edit the rule file.** Make the change directly in
|
|
1043
|
+
\`.taskless/rules/sg/<id>/<id>.yml\`. Preserve the existing \`id\`,
|
|
1044
|
+
\`language\`, and \`severity\` unless the user explicitly asked to
|
|
1045
|
+
change them.
|
|
1046
|
+
|
|
1047
|
+
5. **Update test cases.** Add the new false-positive examples to the
|
|
1048
|
+
\`valid:\` list and the new false-negative examples to the
|
|
1049
|
+
\`invalid:\` list in \`.taskless/rules/sg/<id>/.tests/<id>-*.yml\`.
|
|
1050
|
+
|
|
1051
|
+
6. **Run the verify feedback loop.** Run:
|
|
1052
|
+
\`\`\`
|
|
1053
|
+
%(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json
|
|
1054
|
+
\`\`\`
|
|
1055
|
+
- If \`success: true\`: report success.
|
|
1056
|
+
- If \`success: false\`: read the per-layer errors, fix, re-run.
|
|
1057
|
+
Repeat up to 3 times. After 3 failed attempts, report to the
|
|
1058
|
+
user with the latest errors and ask for guidance.
|
|
1059
|
+
|
|
1060
|
+
Common fixes:
|
|
1061
|
+
- New tests fail → the rule still doesn't catch the case;
|
|
1062
|
+
refine the pattern.
|
|
1063
|
+
- Old tests broke → the new rule is too aggressive; add an
|
|
1064
|
+
\`ignores\` clause or constrain \`kind\`.
|
|
1065
|
+
- Schema errors → the YAML structure drifted; check fields.
|
|
1066
|
+
|
|
1067
|
+
7. **Report results.** Show:
|
|
1068
|
+
- The updated rule file path
|
|
1069
|
+
- The updated test file path
|
|
1070
|
+
- A diff-style summary of what changed
|
|
1071
|
+
Suggest fetching \`%(TASKLESS_CLI)s agent check\` to validate against the
|
|
1072
|
+
broader codebase.
|
|
1073
|
+
|
|
1074
|
+
## Important Notes
|
|
1075
|
+
|
|
1076
|
+
- Rules created in anonymous mode have no metadata sidecar: that's
|
|
1077
|
+
fine. This recipe doesn't need or write metadata.
|
|
1078
|
+
- If the rule was originally created via the API path (has metadata),
|
|
1079
|
+
you can still use this anonymous recipe to iterate locally. The
|
|
1080
|
+
metadata sidecar is preserved untouched.
|
|
1081
|
+
- Do NOT make any HTTP requests to taskless.io.
|
|
1082
|
+
|
|
1083
|
+
## Errors
|
|
1084
|
+
|
|
1085
|
+
The verify primitive returns structured errors per layer:
|
|
1086
|
+
|
|
1087
|
+
| layer | what failure means | fix |
|
|
1088
|
+
|----------------|------------------------------------|---------------------------------------|
|
|
1089
|
+
| \`schema\` | YAML doesn't match ast-grep schema | Fix rule structure |
|
|
1090
|
+
| \`requirements\` | Missing Taskless-required field | Add \`id\`/\`language\`/\`severity\`/etc. |
|
|
1091
|
+
| \`tests\` | A test case behaved unexpectedly | Fix the rule pattern OR the test case |
|
|
1092
|
+
|
|
1093
|
+
## See Also
|
|
1094
|
+
|
|
1095
|
+
- \`%(TASKLESS_CLI)s agent improve-rule\`: API-backed flow (auth required)
|
|
1096
|
+
- \`%(TASKLESS_CLI)s agent create-sg-rule\`: make a new rule locally
|
|
1097
|
+
- \`%(TASKLESS_CLI)s agent check\`: validate the updated rule
|
|
1098
|
+
`, Z = `# Topic: improve-rule (CLI v%(CLI_VERSION)s / topic v5)
|
|
1099
|
+
|
|
1100
|
+
## Goal
|
|
1101
|
+
Iterate on an existing Taskless rule. The CLI submits the user's
|
|
1102
|
+
guidance to the Taskless API iterate endpoint, which returns an
|
|
1103
|
+
updated rule that overwrites the original on disk. The agent's job
|
|
1104
|
+
is to gather the right ruleId + guidance + supporting references and
|
|
1105
|
+
to report the result.
|
|
1106
|
+
|
|
1107
|
+
If the user wants the local-only flow (no API call), fetch
|
|
1108
|
+
\`%(TASKLESS_CLI)s agent improve-rule --anonymous\` instead.
|
|
1109
|
+
|
|
1110
|
+
## Preconditions
|
|
1111
|
+
- User is logged in.
|
|
1112
|
+
- The project has a GitHub owner. Improvement runs through the same
|
|
1113
|
+
service path as generation, so it carries the same constraint. Check
|
|
1114
|
+
\`ghOwner\` from \`%(TASKLESS_CLI)s info --json\`: if it is the literal
|
|
1115
|
+
\`[unknown]\`, stop and say the tier is unavailable rather than
|
|
1116
|
+
submitting. \`auth login\` does not fix it, no GitHub owner is a
|
|
1117
|
+
property of the project, not the session.
|
|
1118
|
+
- The target rule exists at \`.taskless/rules/sg/<id>/<id>.yml\`.
|
|
1119
|
+
- You have the rule's **ticket id**: the value \`%(TASKLESS_CLI)s rule
|
|
1120
|
+
create --json\` printed as \`ruleId\` when the rule was generated. The
|
|
1121
|
+
iterate endpoint is addressed by that id. Nothing on disk holds it,
|
|
1122
|
+
so it comes from the create output or from the user. Without it,
|
|
1123
|
+
fetch the anonymous variant instead.
|
|
1124
|
+
|
|
1125
|
+
## Steps
|
|
1126
|
+
|
|
1127
|
+
1. **Confirm auth.** Run \`%(TASKLESS_CLI)s info --json\` and check
|
|
1128
|
+
\`loggedIn\`. If false, fetch \`%(TASKLESS_CLI)s agent auth\`.
|
|
1129
|
+
|
|
1130
|
+
2. **Identify the rule to improve.** If the user named one, use it.
|
|
1131
|
+
Otherwise, list rules in \`.taskless/rules/sg/\` and ask which one.
|
|
1132
|
+
Read the existing rule file so you can summarize what it does.
|
|
1133
|
+
|
|
1134
|
+
3. **Get the ticket id.** This is the id the iterate endpoint is
|
|
1135
|
+
addressed by, and it is the \`ruleId\` field from that rule's
|
|
1136
|
+
\`%(TASKLESS_CLI)s rule create --json\` output. Take it from the
|
|
1137
|
+
session that created the rule, or ask the user for it.
|
|
1138
|
+
|
|
1139
|
+
**Do not run \`%(TASKLESS_CLI)s rule meta <id>\` to get it.** That
|
|
1140
|
+
command reads \`.taskless/rule-metadata/<id>.yml\`, a sidecar this CLI
|
|
1141
|
+
never writes, so it exits 1 with \`RULE_META_UNAVAILABLE\` for every
|
|
1142
|
+
rule. If no one has the ticket id, fetch
|
|
1143
|
+
\`%(TASKLESS_CLI)s agent improve-rule --anonymous\` and iterate
|
|
1144
|
+
locally.
|
|
1145
|
+
|
|
1146
|
+
4. **Gather improvement guidance.** Ask the user what should change:
|
|
1147
|
+
- Are there false positives we need to exclude?
|
|
1148
|
+
- Are there missed cases we need to catch?
|
|
1149
|
+
- Is the message confusing or misleading?
|
|
1150
|
+
- Should the severity change?
|
|
1151
|
+
|
|
1152
|
+
**"It doesn't fire on X" is often the \`$$$\` separator, not a pattern
|
|
1153
|
+
that is merely too narrow.** Read the rule's pattern for a \`$$$\`
|
|
1154
|
+
sitting next to a comma before you write that guidance. The comma is
|
|
1155
|
+
itself an AST node that has to match, so \`f($A, $$$)\` never sees a
|
|
1156
|
+
one-argument call and \`f($$$, $A)\` sees only one-argument calls, the
|
|
1157
|
+
pattern reads as variadic and is not. \`%(TASKLESS_CLI)s agent create-sg-rule\`
|
|
1158
|
+
carries the arity table and both remedies. Guidance that asks to
|
|
1159
|
+
widen the rule when the fix is \`strictness: ast\` sends the iterate
|
|
1160
|
+
endpoint after the wrong change, and it will oblige.
|
|
1161
|
+
|
|
1162
|
+
5. **Collect supporting references.** Ask the user for any code
|
|
1163
|
+
examples that should be:
|
|
1164
|
+
- **Not flagged** (currently flagged but shouldn't be): false
|
|
1165
|
+
positive references.
|
|
1166
|
+
- **Flagged** (currently passing but should be caught): false
|
|
1167
|
+
negative references.
|
|
1168
|
+
|
|
1169
|
+
Each reference is \`{ filename: string, content: string }\`. Multiple
|
|
1170
|
+
references are an array.
|
|
1171
|
+
|
|
1172
|
+
6. **Confirm the request.** Summarize the rule, the guidance, and
|
|
1173
|
+
the references before submitting.
|
|
1174
|
+
|
|
1175
|
+
7. **Write the JSON payload.** Build a JSON object matching the input
|
|
1176
|
+
schema below and write it to \`.taskless/.tmp-improve-request.json\`.
|
|
1177
|
+
|
|
1178
|
+
8. **Invoke the CLI.** Run:
|
|
1179
|
+
\`\`\`
|
|
1180
|
+
%(TASKLESS_CLI)s rule improve --from .taskless/.tmp-improve-request.json --json
|
|
1181
|
+
\`\`\`
|
|
1182
|
+
This may take 30-60 seconds while the API generates the update.
|
|
1183
|
+
|
|
1184
|
+
9. **Clean up.** Delete \`.taskless/.tmp-improve-request.json\`
|
|
1185
|
+
regardless of success or failure.
|
|
1186
|
+
|
|
1187
|
+
10. **Report results.** The CLI overwrites the rule file (and its
|
|
1188
|
+
test file) with the updated version. Show the file paths and a
|
|
1189
|
+
summary of what changed. Suggest fetching \`%(TASKLESS_CLI)s agent check\`
|
|
1190
|
+
to validate.
|
|
1191
|
+
|
|
1192
|
+
**Read \`notices\` if it is present.** It is an optional array of
|
|
1193
|
+
advisory messages about a delivery that was written anyway. A rule
|
|
1194
|
+
arriving with no \`.tests/\` fixtures is the one to expect. Surface any notice
|
|
1195
|
+
rather than reporting an unqualified success. Under \`--json\` these
|
|
1196
|
+
do not go to stderr, so the envelope is the only place they appear.
|
|
1197
|
+
|
|
1198
|
+
**Confirm both fixture buckets are still non-empty.** \`test\` fails
|
|
1199
|
+
an ast-grep rule whose \`valid:\` or \`invalid:\` array is empty, and
|
|
1200
|
+
narrowing a pattern to kill a false positive is exactly how
|
|
1201
|
+
\`invalid:\` ends up with nothing the rule still matches. Run
|
|
1202
|
+
\`\`\`
|
|
1203
|
+
%(TASKLESS_CLI)s test .taskless/rules/sg/<id> --json
|
|
1204
|
+
\`\`\`
|
|
1205
|
+
on the returned files rather than trusting them, and add a fixture
|
|
1206
|
+
the new pattern does match if that bucket came back empty.
|
|
1207
|
+
|
|
1208
|
+
## Input schema
|
|
1209
|
+
|
|
1210
|
+
The \`--from\` JSON file conforms to:
|
|
1211
|
+
|
|
1212
|
+
\`\`\`json
|
|
1213
|
+
%(INPUT_SCHEMA)s
|
|
1214
|
+
\`\`\`
|
|
1215
|
+
|
|
1216
|
+
\`ruleId\` is the original rule's ticket ID, printed as \`ruleId\` by
|
|
1217
|
+
\`%(TASKLESS_CLI)s rule create --json\`. It is not the YAML file name,
|
|
1218
|
+
and it is not readable from anything under \`.taskless/\`.
|
|
1219
|
+
|
|
1220
|
+
## Errors
|
|
1221
|
+
|
|
1222
|
+
When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
|
|
1223
|
+
|
|
1224
|
+
| code | meaning | fix |
|
|
1225
|
+
|--------------------------|-------------------------------------|-----------------------------------------------|
|
|
1226
|
+
| \`AUTH_REQUIRED\` | not logged in | fetch \`%(TASKLESS_CLI)s agent auth\` |
|
|
1227
|
+
| \`NO_GITHUB_REMOTE\` | no GitHub origin remote | retained for compatibility; treat as below |
|
|
1228
|
+
| \`NOT_A_GIT_REPOSITORY\` | not a git repository | tell the user; \`auth login\` cannot fix it |
|
|
1229
|
+
| \`NO_ORIGIN_REMOTE\` | git repository, no \`origin\` | tell the user; \`auth login\` cannot fix it |
|
|
1230
|
+
| \`UNSUPPORTED_REMOTE_HOST\`| \`origin\` is not GitHub | tell the user; \`auth login\` cannot fix it |
|
|
1231
|
+
| \`INVALID_INPUT\` | \`--from\` JSON failed validation | re-read input schema, fix, retry |
|
|
1232
|
+
| \`RULE_NOT_FOUND\` | the service has no such ticket id | re-check the id from \`rule create --json\` |
|
|
1233
|
+
| \`NETWORK_ERROR\` | API submit/poll failed | report and suggest retry |
|
|
1234
|
+
| \`RULE_GENERATION_FAILED\` | API returned a generation failure | report; suggest enriching guidance/references |
|
|
1235
|
+
| \`RULE_UNSUPPORTED\` | plan lacks this generation type | tell the user to enable it; do not retry |
|
|
1236
|
+
|
|
1237
|
+
## See Also
|
|
1238
|
+
|
|
1239
|
+
- \`%(TASKLESS_CLI)s agent improve-rule --anonymous\`: local-only flow
|
|
1240
|
+
- \`%(TASKLESS_CLI)s agent route\`: make a new rule from scratch
|
|
1241
|
+
- \`%(TASKLESS_CLI)s agent check\`: validate the updated rule
|
|
1242
|
+
`, ee = `# Topic: info (CLI v%(CLI_VERSION)s / topic v1)
|
|
1243
|
+
|
|
1244
|
+
## Goal
|
|
1245
|
+
Report local Taskless state: CLI version, installed skill versions
|
|
1246
|
+
per detected tool, and (unless \`--anonymous\`) the user's auth state.
|
|
1247
|
+
Used as a health check, for staleness detection, and to confirm
|
|
1248
|
+
which version of the CLI/skills the agent is talking to.
|
|
1249
|
+
|
|
1250
|
+
## Preconditions
|
|
1251
|
+
- None. Works in any directory; doesn't require \`.taskless/\`.
|
|
1252
|
+
|
|
1253
|
+
## Steps
|
|
1254
|
+
|
|
1255
|
+
1. **Invoke the CLI** with JSON output:
|
|
1256
|
+
\`\`\`
|
|
1257
|
+
%(TASKLESS_CLI)s info --json
|
|
1258
|
+
\`\`\`
|
|
1259
|
+
For an offline/local-only state report (no auth probe), pass
|
|
1260
|
+
\`--anonymous\`:
|
|
1261
|
+
\`\`\`
|
|
1262
|
+
%(TASKLESS_CLI)s info --json --anonymous
|
|
1263
|
+
\`\`\`
|
|
1264
|
+
|
|
1265
|
+
2. **Parse the response.** Shape:
|
|
1266
|
+
\`\`\`json
|
|
1267
|
+
{
|
|
1268
|
+
"success": true,
|
|
1269
|
+
"version": "0.7.0",
|
|
1270
|
+
"tools": [
|
|
1271
|
+
{
|
|
1272
|
+
"name": "Claude Code",
|
|
1273
|
+
"skills": [
|
|
1274
|
+
{ "name": "taskless", "installedVersion": "0.7.0",
|
|
1275
|
+
"currentVersion": "0.7.0", "current": true }
|
|
1276
|
+
]
|
|
1277
|
+
}
|
|
1278
|
+
],
|
|
1279
|
+
"loggedIn": true,
|
|
1280
|
+
"auth": { "user": "...", "email": "...", "orgs": ["..."] }
|
|
1281
|
+
}
|
|
1282
|
+
\`\`\`
|
|
1283
|
+
|
|
1284
|
+
3. **Report to the user.** Summarize:
|
|
1285
|
+
- CLI version
|
|
1286
|
+
- For each tool: number of installed skills, count out-of-date
|
|
1287
|
+
- Auth: logged in as <user> (orgs) OR not logged in
|
|
1288
|
+
|
|
1289
|
+
4. **Suggest reinit on staleness.** If any skill has \`current: false\`,
|
|
1290
|
+
suggest \`%(TASKLESS_CLI)s\` to reinstall and pull the latest
|
|
1291
|
+
bundle.
|
|
1292
|
+
|
|
1293
|
+
## Errors
|
|
1294
|
+
|
|
1295
|
+
When \`--json\` is set, failures emit \`{ ok: false, code, message }\`:
|
|
1296
|
+
|
|
1297
|
+
| code | meaning | fix |
|
|
1298
|
+
|------------------|----------------------------|--------------------------|
|
|
1299
|
+
| \`INTERNAL_ERROR\` | Internal schema validation | Report; likely a CLI bug |
|
|
1300
|
+
|
|
1301
|
+
(Network errors during the auth probe are silently swallowed:
|
|
1302
|
+
\`info\` falls back to reporting \`loggedIn: false\` rather than
|
|
1303
|
+
failing.)
|
|
1304
|
+
|
|
1305
|
+
## See Also
|
|
1306
|
+
|
|
1307
|
+
- \`%(TASKLESS_CLI)s agent auth\`: log in / log out / status detail
|
|
1308
|
+
- \`%(TASKLESS_CLI)s agent check\`: run rules against the codebase
|
|
1309
|
+
`, ne = `# Topic: init (CLI v%(CLI_VERSION)s / topic v1)
|
|
1310
|
+
|
|
1311
|
+
## Goal
|
|
1312
|
+
Install or update the Taskless skill into the user's coding-agent
|
|
1313
|
+
tools (Claude Code, OpenCode, Cursor, etc.). The user runs this
|
|
1314
|
+
themselves, the agent's role is mostly to point the user at the
|
|
1315
|
+
right command when they need to install or upgrade.
|
|
1316
|
+
|
|
1317
|
+
## Preconditions
|
|
1318
|
+
- None at the user level. The command works in any directory and
|
|
1319
|
+
bootstraps \`.taskless/\` on first run.
|
|
1320
|
+
- For interactive mode: a TTY (running from a terminal).
|
|
1321
|
+
|
|
1322
|
+
## Steps
|
|
1323
|
+
|
|
1324
|
+
The user should run:
|
|
1325
|
+
\`\`\`
|
|
1326
|
+
%(TASKLESS_CLI)s
|
|
1327
|
+
\`\`\`
|
|
1328
|
+
(no subcommand). In a TTY this launches the interactive wizard. In
|
|
1329
|
+
non-TTY contexts it prints the topic index instead.
|
|
1330
|
+
|
|
1331
|
+
For scripted installs (CI, Dockerfiles):
|
|
1332
|
+
\`\`\`
|
|
1333
|
+
%(TASKLESS_CLI)s init --no-interactive
|
|
1334
|
+
\`\`\`
|
|
1335
|
+
|
|
1336
|
+
The wizard will:
|
|
1337
|
+
1. Detect installed tools (Claude Code, OpenCode, Cursor) and ask
|
|
1338
|
+
which tools to enable Taskless for.
|
|
1339
|
+
2. Show the auth tradeoff and offer to log in (skippable).
|
|
1340
|
+
3. Show a diff against the previous install state before writing.
|
|
1341
|
+
4. Write the canonical \`taskless\` skill (and \`tskl\` command) once to
|
|
1342
|
+
\`.taskless/\`, then a thin reference stub into each selected tool
|
|
1343
|
+
directory (\`.claude/\`, \`.cursor/\`, \`.opencode/\`, \`.agents/\`).
|
|
1344
|
+
5. Update \`.taskless/taskless.json\` with the install manifest.
|
|
1345
|
+
|
|
1346
|
+
The skill content lives in exactly one place, \`.taskless/skills/\`,
|
|
1347
|
+
and each tool directory holds only a short stub that points at it.
|
|
1348
|
+
Stale layouts from older installs (full per-tool copies, symlinks)
|
|
1349
|
+
are converged into stubs automatically. If the user is on v0.6 or
|
|
1350
|
+
earlier, the obsolete per-task skills (taskless-check, etc.) and old
|
|
1351
|
+
slash commands are removed as part of the install; the summary shows
|
|
1352
|
+
what was removed.
|
|
1353
|
+
|
|
1354
|
+
## Errors
|
|
1355
|
+
|
|
1356
|
+
- Wizard cancelled (Ctrl-C) → no filesystem writes. Re-run when ready.
|
|
1357
|
+
- No tools detected → falls back to \`.agents/skills/\`.
|
|
1358
|
+
|
|
1359
|
+
## See Also
|
|
1360
|
+
|
|
1361
|
+
- \`%(TASKLESS_CLI)s agent info\`: verify what's installed and check staleness
|
|
1362
|
+
- \`%(TASKLESS_CLI)s agent auth\`: authenticate after installing
|
|
1363
|
+
`, te = `# Topic: onboard (CLI v%(CLI_VERSION)s / topic v3)
|
|
1364
|
+
|
|
1365
|
+
## Goal
|
|
1366
|
+
Help a user who has just installed Taskless go from zero rules to a
|
|
1367
|
+
useful starter set by mining their codebase, agent-memory files, PR
|
|
1368
|
+
review history, and issue tracker for high-signal rule candidates.
|
|
1369
|
+
This is a conversational discovery flow, not a script, the agent
|
|
1370
|
+
collaborates with the user on what to scan and surfaces hypothetical
|
|
1371
|
+
rules as a bullet list the user can choose to materialize via the
|
|
1372
|
+
\`rule create\` flow.
|
|
1373
|
+
|
|
1374
|
+
## Preconditions
|
|
1375
|
+
- \`.taskless/\` directory exists (Taskless is installed). The
|
|
1376
|
+
\`%(TASKLESS_CLI)s onboard\` subcommand bootstraps it on first run, so this
|
|
1377
|
+
is automatically satisfied.
|
|
1378
|
+
- A working repository the agent can read.
|
|
1379
|
+
- No auth required to surface candidates. (Materializing a rule via
|
|
1380
|
+
\`rule create\` may require auth. Fetch \`%(TASKLESS_CLI)s agent auth\` if
|
|
1381
|
+
needed at that point.)
|
|
1382
|
+
- The criterion for where a rule belongs lives in
|
|
1383
|
+
\`%(TASKLESS_CLI)s agent route\` and is not restated here. This recipe
|
|
1384
|
+
reads it; it does not duplicate it.
|
|
1385
|
+
|
|
1386
|
+
## Steps
|
|
1387
|
+
|
|
1388
|
+
1. **Read the manifest first.** Run
|
|
1389
|
+
\`%(TASKLESS_CLI)s info --json\` and check whether
|
|
1390
|
+
\`.taskless/taskless.json\` reports \`install.onboarded: true\`. The
|
|
1391
|
+
\`%(TASKLESS_CLI)s onboard\` subcommand has already gated on this for you,
|
|
1392
|
+
but if the user is invoking the recipe directly via the skill,
|
|
1393
|
+
confirm with the user before running a long discovery pass.
|
|
1394
|
+
|
|
1395
|
+
2. **Learn the routing surface before you propose anything.** A
|
|
1396
|
+
candidate this repository cannot express is not a candidate, and
|
|
1397
|
+
you cannot tell which is which until you know two things:
|
|
1398
|
+
|
|
1399
|
+
\`\`\`
|
|
1400
|
+
%(TASKLESS_CLI)s agent route
|
|
1401
|
+
%(TASKLESS_CLI)s detect --json
|
|
1402
|
+
\`\`\`
|
|
1403
|
+
|
|
1404
|
+
Read \`route\` for the destination criterion, which rules are
|
|
1405
|
+
decided by one file's syntax tree, by a document's words, by a
|
|
1406
|
+
tool the repo already runs, or by evidence outside the files.
|
|
1407
|
+
Read \`detect --json\` for what this repository actually has:
|
|
1408
|
+
\`linters\`, \`languages\`, and \`ruleStyles\`.
|
|
1409
|
+
|
|
1410
|
+
Do this before you synthesize the bullet list, not after.
|
|
1411
|
+
Proposing first and routing afterwards is how a bullet list ends
|
|
1412
|
+
up carrying candidates that nothing can enforce. You are not
|
|
1413
|
+
choosing a destination command here. That happens per accepted
|
|
1414
|
+
bullet when you offer materialization.
|
|
1415
|
+
|
|
1416
|
+
3. **Open the conversation about sources.** Tell the user you can mine
|
|
1417
|
+
several places for rule candidates and ask which ones they want to
|
|
1418
|
+
include. Default sources you should always offer:
|
|
1419
|
+
|
|
1420
|
+
- **Codebase TODOs / FIXMEs**: search for \`TODO\`, \`FIXME\`, \`XXX\`,
|
|
1421
|
+
and \`HACK\` comments. Many of these are latent rules ("don't do
|
|
1422
|
+
this", "remove when X").
|
|
1423
|
+
- **Agent-memory files**: read \`CLAUDE.md\`, \`AGENTS.md\`,
|
|
1424
|
+
\`.cursorrules\`, \`.opencode/AGENTS.md\`, and similar agent-context
|
|
1425
|
+
files for explicit rules and conventions stated in prose.
|
|
1426
|
+
- **Recent PR review comments**: only if the \`gh\` CLI is
|
|
1427
|
+
available. Probe with \`command -v gh\`. Suggest scanning the last
|
|
1428
|
+
30 days of merged PRs for repeated reviewer feedback patterns.
|
|
1429
|
+
- **Issue tracker tickets**: only if a relevant MCP is wired in
|
|
1430
|
+
(Linear, Jira, GitHub issues via \`gh issue list\`, etc.). Use
|
|
1431
|
+
whatever issue-tracker tools you have available.
|
|
1432
|
+
|
|
1433
|
+
Then explicitly ask: "Are there other places I should look, a
|
|
1434
|
+
team wiki, an internal docs site, a specific design doc, a Slack
|
|
1435
|
+
channel export?" The user often knows about sources you don't.
|
|
1436
|
+
|
|
1437
|
+
4. **Probe tool availability before promising a scan.** For each
|
|
1438
|
+
source the user picks, verify the tool exists before committing
|
|
1439
|
+
to it. Don't tell the user "I'll scan PR comments" if \`gh\` isn't
|
|
1440
|
+
installed, say "PR comments need the GitHub CLI, or equivalent; want me to skip
|
|
1441
|
+
this or wait while you install these tools?"
|
|
1442
|
+
|
|
1443
|
+
5. **Scan with high-signal filtering.** For each chosen source:
|
|
1444
|
+
|
|
1445
|
+
- **Filter for repeated patterns.** A reviewer comment that
|
|
1446
|
+
appears across multiple PRs is a much stronger rule candidate
|
|
1447
|
+
than a one-off nit.
|
|
1448
|
+
- **Prefer comments that cite a doc or style guide.** "Per our
|
|
1449
|
+
style guide, no direct DB calls in controllers" is a rule.
|
|
1450
|
+
"looks good" is not.
|
|
1451
|
+
- **Prefer merge-blocking feedback.** Comments that gated a PR
|
|
1452
|
+
merge are higher-signal than passing remarks.
|
|
1453
|
+
- **Filter out one-off nits and pure formatting.** Linters and
|
|
1454
|
+
formatters already handle those; Taskless rules earn their
|
|
1455
|
+
keep on semantic patterns.
|
|
1456
|
+
|
|
1457
|
+
6. **Synthesize the bullet list.** Present findings as a single
|
|
1458
|
+
bullet list the user can scan quickly. One bullet per
|
|
1459
|
+
hypothetical rule, in this format:
|
|
1460
|
+
|
|
1461
|
+
\`\`\`
|
|
1462
|
+
- <kebab-case-name> [<destination>]: <one-line description of what it would enforce>
|
|
1463
|
+
\`\`\`
|
|
1464
|
+
|
|
1465
|
+
\`<destination>\` is the routing call you reached when you learned
|
|
1466
|
+
the routing surface, one of
|
|
1467
|
+
\`legacy\`, \`sg\`, \`vale\`, or \`runtime\`. Example:
|
|
1468
|
+
|
|
1469
|
+
\`\`\`
|
|
1470
|
+
- no-direct-db-access [sg]: Prevent controllers from importing the DB module directly; route through the service layer.
|
|
1471
|
+
- require-zod-validation [sg]: Flag API handlers that read request bodies without parsing them through a Zod schema first.
|
|
1472
|
+
- comments-record-not-forecast [vale]: Flag comments that promise future work instead of recording what is true now.
|
|
1473
|
+
- exports-must-be-used [runtime]: Flag exported symbols nothing in the repo imports.
|
|
1474
|
+
\`\`\`
|
|
1475
|
+
|
|
1476
|
+
The annotation is provisional. It tells the user what kind of rule
|
|
1477
|
+
this would be, and it is why an unroutable candidate never reaches
|
|
1478
|
+
the list. \`route\` adjudicates for real at materialization time, and
|
|
1479
|
+
may land somewhere else once it has the rule's full description.
|
|
1480
|
+
|
|
1481
|
+
Order the bullets by your judgment of impact (frequency in the
|
|
1482
|
+
source × severity of the issue × ease of enforcement). Don't
|
|
1483
|
+
inflate the list, three high-quality candidates beat ten
|
|
1484
|
+
speculative ones.
|
|
1485
|
+
|
|
1486
|
+
7. **Offer materialization per bullet.** For each bullet, ask
|
|
1487
|
+
whether the user wants to turn it into a real Taskless rule. On
|
|
1488
|
+
yes, follow the \`%(TASKLESS_CLI)s agent route\` recipe you already
|
|
1489
|
+
fetched when you learned the routing surface, the user's accepted
|
|
1490
|
+
bullet becomes the rule description input. Re-fetch it only if it
|
|
1491
|
+
has fallen out of context.
|
|
1492
|
+
|
|
1493
|
+
8. **Ask before marking onboarding complete.** When the user signals
|
|
1494
|
+
they're done (they've materialized everything they want, or
|
|
1495
|
+
they've said "that's enough for now"), explicitly ask: "Do you
|
|
1496
|
+
want me to mark Taskless as onboarded? You won't be re-asked to
|
|
1497
|
+
onboard until you pass \`--force\`." On explicit yes, and only
|
|
1498
|
+
on explicit yes, run:
|
|
1499
|
+
|
|
1500
|
+
\`\`\`
|
|
1501
|
+
%(TASKLESS_CLI)s onboard --mark-complete
|
|
1502
|
+
\`\`\`
|
|
1503
|
+
|
|
1504
|
+
Do NOT mark onboarding complete on your own initiative. Do NOT
|
|
1505
|
+
run \`--mark-complete\` if the user is ambiguous, says "maybe
|
|
1506
|
+
later", or stops responding. The flag is consent-gated.
|
|
1507
|
+
|
|
1508
|
+
## Errors
|
|
1509
|
+
|
|
1510
|
+
| code | meaning | fix |
|
|
1511
|
+
|---------------------|----------------------------------------------|--------------------------------------|
|
|
1512
|
+
| \`ALREADY_ONBOARDED\` | \`install.onboarded\` is true and no \`--force\` | suggest \`--force\` or skip onboarding |
|
|
1513
|
+
|
|
1514
|
+
## See Also
|
|
1515
|
+
|
|
1516
|
+
- \`%(TASKLESS_CLI)s agent route\`: the destination criterion; read it
|
|
1517
|
+
before proposing candidates, and follow it to materialize an accepted one
|
|
1518
|
+
- \`%(TASKLESS_CLI)s agent detect\`: what this repository already lints
|
|
1519
|
+
and authors, which bounds what a candidate can be
|
|
1520
|
+
- \`%(TASKLESS_CLI)s agent check\`: validate newly created rules against the codebase
|
|
1521
|
+
- \`%(TASKLESS_CLI)s agent info\`: inspect the current \`.taskless/taskless.json\` state
|
|
1522
|
+
`, se = `# Topic: route (CLI v%(CLI_VERSION)s / topic v5)
|
|
1523
|
+
|
|
1524
|
+
## Goal
|
|
1525
|
+
Turn "write me a rule that…" into one command to run. This is the front
|
|
1526
|
+
door for every rule-authoring request, and it makes one decision (which
|
|
1527
|
+
of five recipes authors this rule) from one reading of the evidence.
|
|
1528
|
+
|
|
1529
|
+
That single decision covers both halves that used to be asked
|
|
1530
|
+
separately: whether the rule can be built here, and which engine can
|
|
1531
|
+
express it. They are answered from the same signals, so they are
|
|
1532
|
+
answered together.
|
|
1533
|
+
|
|
1534
|
+
## Preconditions
|
|
1535
|
+
- A working repository the agent can read.
|
|
1536
|
+
- No auth required to route. Login state is an *input* to the decision,
|
|
1537
|
+
not a requirement of making it.
|
|
1538
|
+
|
|
1539
|
+
## Steps
|
|
1540
|
+
|
|
1541
|
+
1. **Read the repository.** %(DETECT_EVIDENCE)s the configured linters, languages, and the repo's own
|
|
1542
|
+
rule styles. It is deterministic and offline. Use it as ground truth
|
|
1543
|
+
instead of guessing the repo's tooling. The scan is monorepo-aware, so
|
|
1544
|
+
evidence may carry a sub-package path. The output shape:
|
|
1545
|
+
\`\`\`json
|
|
1546
|
+
{
|
|
1547
|
+
"success": true,
|
|
1548
|
+
"linters": [{ "name": "eslint", "evidence": ["packages/api/.eslintrc.json"] }],
|
|
1549
|
+
"languages": ["JavaScript", "TypeScript"],
|
|
1550
|
+
"ruleStyles": [
|
|
1551
|
+
{ "source": ".taskless/rules/sg", "description": "Existing Taskless ast-grep rules." }
|
|
1552
|
+
]
|
|
1553
|
+
}
|
|
1554
|
+
\`\`\`
|
|
1555
|
+
|
|
1556
|
+
2. **Read the login state and the repository context.** %(LOGIN_EVIDENCE)s \`loggedIn\` and \`ghOwner\`. Do this now, not later: they change
|
|
1557
|
+
which destinations exist, so classifying first means classifying
|
|
1558
|
+
against a set that may be wrong.
|
|
1559
|
+
|
|
1560
|
+
\`ghOwner\` is the GitHub owner this project belongs to, or the literal
|
|
1561
|
+
\`[unknown]\` when there is none to find: not a git repository, no
|
|
1562
|
+
\`origin\` remote, an \`origin\` that is not GitHub, or no git installed.
|
|
1563
|
+
Read it from this payload rather than running \`git\` yourself, so the
|
|
1564
|
+
destinations you offer match what the CLI will actually allow.
|
|
1565
|
+
|
|
1566
|
+
Reading it is not the same as asking about it. Do **not** open by
|
|
1567
|
+
offering service generation. At this point neither you nor the user
|
|
1568
|
+
knows whether this is a two-line pattern or something local authoring
|
|
1569
|
+
cannot express, so the question costs a turn and cannot be answered
|
|
1570
|
+
well.
|
|
1571
|
+
|
|
1572
|
+
3. **State the evidence before you name a destination.** Write one
|
|
1573
|
+
sentence: *"to decide this, you must look at ___."* Then a short
|
|
1574
|
+
rationale covering what \`detect\` showed, whether a linter the repo
|
|
1575
|
+
already runs could express this, and what the rule needs to read.
|
|
1576
|
+
|
|
1577
|
+
Do this first, every time. Naming a destination and justifying it
|
|
1578
|
+
afterwards is how prose-about-code ends up in \`sg\` and cross-file
|
|
1579
|
+
questions end up in \`vale\`.
|
|
1580
|
+
|
|
1581
|
+
4. **Match the evidence to a destination.** The comparison is made here
|
|
1582
|
+
and only here. The destination recipes describe their own scope and
|
|
1583
|
+
deliberately do not restate this table.
|
|
1584
|
+
|
|
1585
|
+
| The rule is decided by… | Destination | Login |
|
|
1586
|
+
|------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|------------|
|
|
1587
|
+
| a tool the repo already runs, in that tool's dialect | \`create-legacy-rule\` | no |
|
|
1588
|
+
| **one file's syntax tree**: a call, an import, a JSX attribute, a type annotation | \`create-sg-rule\` | no |
|
|
1589
|
+
| **a document's words**: docs, README, comments, commit bodies | \`create-vale-rule\` | no |
|
|
1590
|
+
| **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 |
|
|
1591
|
+
|
|
1592
|
+
Sharpening the three engine rows, because most wrong answers are one
|
|
1593
|
+
of these:
|
|
1594
|
+
|
|
1595
|
+
- **Relational correlation inside one file is still \`sg\`.** "A
|
|
1596
|
+
\`useEffect\` whose dependency array omits a value used in its body"
|
|
1597
|
+
is one file's tree. If exactly one file settles it, it is \`sg\`.
|
|
1598
|
+
- **Prose about code is still prose.** "Comments must not say
|
|
1599
|
+
'TBD'" is \`vale\`. The evidence is the words. "Every exported
|
|
1600
|
+
function has a doc comment" is \`sg\`. The evidence is whether a node
|
|
1601
|
+
exists above a declaration. Ask what you would have to *read* to
|
|
1602
|
+
decide, not what the subject matter is.
|
|
1603
|
+
- **Vale sees one document at a time.** "This term is spelled
|
|
1604
|
+
consistently ACROSS the docs directory" is a graph question, so it
|
|
1605
|
+
is runtime, even though it is entirely about prose.
|
|
1606
|
+
- **Trust tier is not a destination.** \`sg\` and \`vale\` are both
|
|
1607
|
+
static-tier: inert data, always run, no login, no reconcile, no
|
|
1608
|
+
signing. Only runtime executes code. "Static vs runtime" is a
|
|
1609
|
+
different axis from "which engine", and conflating them is what
|
|
1610
|
+
makes a prose rule look like it needs an account.
|
|
1611
|
+
|
|
1612
|
+
**What each local engine can actually read.** Both lists are rendered
|
|
1613
|
+
from the pinned engine versions rather than written out here, so they
|
|
1614
|
+
cannot go stale against the binaries: if one looks wrong, an engine
|
|
1615
|
+
was bumped and a vendor-contract test is already red.
|
|
1616
|
+
|
|
1617
|
+
- **ast-grep (v%(AST_GREP_VERSION)s) parses:** %(AST_GREP_LANGUAGES)s.
|
|
1618
|
+
|
|
1619
|
+
Those spellings are ast-grep's own and go into a rule's \`language:\`
|
|
1620
|
+
field verbatim. Nothing local validates that field, the vendored
|
|
1621
|
+
schema types it as a bare string with no enum, so the first thing
|
|
1622
|
+
with an opinion is the binary. It accepts some off-list aliases
|
|
1623
|
+
(\`C++\` and \`cpp\` both reach the Cpp parser), but a name it does not
|
|
1624
|
+
know at all, like \`C#\` for \`CSharp\`, aborts config parsing and
|
|
1625
|
+
takes every other rule's report down with it. Copy from the list.
|
|
1626
|
+
|
|
1627
|
+
**A GitHub Actions workflow is \`Yaml\`, and \`Yaml\` is on that list.**
|
|
1628
|
+
A rule about \`.github/workflows/*.yml\` is an \`sg\` rule; sending it
|
|
1629
|
+
to runtime spends a login on a check that builds here.
|
|
1630
|
+
|
|
1631
|
+
**\`Markdown\` is on that list, and it is narrower than the name
|
|
1632
|
+
suggests.** tree-sitter-markdown splits its grammar in two, block
|
|
1633
|
+
and inline, and ast-grep exposes only the block tree. Measured at
|
|
1634
|
+
v%(AST_GREP_VERSION)s against a document holding a heading, a
|
|
1635
|
+
subheading, a list, a fenced block and a link:
|
|
1636
|
+
|
|
1637
|
+
- *Block structure works.* \`document\`, \`section\`, \`atx_heading\`,
|
|
1638
|
+
\`setext_heading\`, \`fenced_code_block\`, \`list_item\` and
|
|
1639
|
+
\`paragraph\` all match, and headings discriminate by level:
|
|
1640
|
+
\`# $T\` matches only the h1, \`## $T\` only the h2.
|
|
1641
|
+
- *Everything inside a line does not.* A paragraph's contents are
|
|
1642
|
+
one opaque \`inline\` node. There is no \`link\` node, no \`emphasis\`,
|
|
1643
|
+
no \`strong_emphasis\`. \`kind: link\` does not quietly find nothing:
|
|
1644
|
+
it is a config error (\`Kind 'link' is invalid\`) that exits 8 and
|
|
1645
|
+
takes every other rule's report down with it, the same shape as
|
|
1646
|
+
\`C#\` above. The quiet version is the pattern form, where
|
|
1647
|
+
\`[$T]($U)\` parses, runs, and matches nothing forever.
|
|
1648
|
+
- So "no bare URLs", "link text must not read 'click here'", "no
|
|
1649
|
+
bold inside a heading" feel structural and are not \`sg\` rules.
|
|
1650
|
+
The evidence is words inside a line, which is Vale's.
|
|
1651
|
+
|
|
1652
|
+
**Neither static engine counts, and neither sees absence.**
|
|
1653
|
+
ast-grep fires once per match, \`not\` scopes to a node rather than
|
|
1654
|
+
to a document, and there is no count assertion. "Every doc has an
|
|
1655
|
+
h1" and "at most one h1" are runtime however structural they look.
|
|
1656
|
+
|
|
1657
|
+
**\`.md\` is the first extension both static engines claim, so do
|
|
1658
|
+
not route it by extension.** \`sg\` answers "does this block-level
|
|
1659
|
+
shape occur in this file"; Vale answers "do these words appear in
|
|
1660
|
+
this file's prose". Fences, heading levels and list shape are \`sg\`.
|
|
1661
|
+
The wording inside a heading, a list item or a link is Vale.
|
|
1662
|
+
|
|
1663
|
+
- **Vale (v%(VALE_VERSION)s) reads three tiers, and hard-fails on a fourth.**
|
|
1664
|
+
The tier is decided by the file's extension:
|
|
1665
|
+
|
|
1666
|
+
- *markup*: the whole document is prose, and the format's own
|
|
1667
|
+
non-prose constructs are skipped:
|
|
1668
|
+
%(VALE_MARKUP_FORMATS)s
|
|
1669
|
+
- *comments only*: the comment text is linted and the code body is
|
|
1670
|
+
invisible:
|
|
1671
|
+
%(VALE_COMMENT_FORMATS)s
|
|
1672
|
+
- *plaintext fallback*: everything else, \`.yml\` \`.toml\` \`.sh\`
|
|
1673
|
+
\`.sql\` and every unnamed extension included. There is no parser,
|
|
1674
|
+
so the file is linted as one block of prose, and a Vale rule
|
|
1675
|
+
scoped to YAML flags the code as readily as the comments. That is
|
|
1676
|
+
rarely what was asked for. Say so before writing it. These read
|
|
1677
|
+
like markup and are not: %(VALE_PLAINTEXT_FORMATS)s
|
|
1678
|
+
- *not supported*: Vale parses these only by shelling out to an
|
|
1679
|
+
external program, and this build does not support any format that
|
|
1680
|
+
needs one:
|
|
1681
|
+
%(VALE_CONVERTER_FORMATS)s
|
|
1682
|
+
|
|
1683
|
+
Installing the program does not change this. Taskless excludes
|
|
1684
|
+
these files from the Vale run whatever is on the machine, so that
|
|
1685
|
+
a repository checks the same way everywhere rather than depending
|
|
1686
|
+
on what a given host happens to have available.
|
|
1687
|
+
|
|
1688
|
+
**One such file fails the entire Vale pass, not just that file.**
|
|
1689
|
+
Vale exits 2 with an \`E100\` runtime error, \`--no-exit\` does not
|
|
1690
|
+
suppress it, and every other Vale rule over every other file goes
|
|
1691
|
+
unreported. A matcher written as \`[*.{md,typ}]\` is not a wider
|
|
1692
|
+
\`[*.md]\`. It is a broken one.
|
|
1693
|
+
|
|
1694
|
+
**\`.mdx\` is supported** as of Vale v3.18.0, which parses it
|
|
1695
|
+
natively. It needs no external program and belongs with the
|
|
1696
|
+
other markup formats above. \`.typ\` moved the other way in the
|
|
1697
|
+
same release: Typst now parses through \`typst2vast\`, so a Typst
|
|
1698
|
+
file is excluded rather than read as prose the way it was
|
|
1699
|
+
before. Both are measured, not assumed.
|
|
1700
|
+
|
|
1701
|
+
As of v3.19.0 an MDX component's children are read as Markdown
|
|
1702
|
+
too, so prose wrapped in \`<Steps>\` or \`<Aside>\` is linted where
|
|
1703
|
+
it previously was not. That widens what a Vale rule covers in
|
|
1704
|
+
\`.mdx\`; it does not change which extensions are safe to match.
|
|
1705
|
+
|
|
1706
|
+
**A language on neither list does not route to runtime by default.**
|
|
1707
|
+
Check \`create-legacy-rule\` first: the repo may already run a linter
|
|
1708
|
+
that speaks it, and that linter's own dialect is a local destination
|
|
1709
|
+
with no login. Escalate only once nothing local can see the evidence.
|
|
1710
|
+
|
|
1711
|
+
Worked examples:
|
|
1712
|
+
|
|
1713
|
+
| Rule intent | Evidence needed | Destination |
|
|
1714
|
+
|----------------------------------------------------------|-------------------------------------------|--------------------|
|
|
1715
|
+
| No \`eval(...)\` anywhere | one file's call expressions | \`create-sg-rule\` |
|
|
1716
|
+
| Actions workflows must pin actions to a SHA | one workflow file's tree (\`Yaml\`) | \`create-sg-rule\` |
|
|
1717
|
+
| \`useEffect\` deps must include what the body reads | one file's tree, correlated within it | \`create-sg-rule\` |
|
|
1718
|
+
| Don't write "TBD" or "coming soon" in docs | a document's words | \`create-vale-rule\` |
|
|
1719
|
+
| Comments must not say "for now" | a document's words (comments are prose) | \`create-vale-rule\` |
|
|
1720
|
+
| Headings use sentence case | a document's markup | \`create-vale-rule\` |
|
|
1721
|
+
| Every code fence declares a language | one \`.md\` file's block tree | \`create-sg-rule\` |
|
|
1722
|
+
| Docs must not use setext (\`===\`) headings | one \`.md\` file's block tree | \`create-sg-rule\` |
|
|
1723
|
+
| Link text must not read "click here" | words inside a line (no inline tree) | \`create-vale-rule\` |
|
|
1724
|
+
| Every doc has exactly one h1 | a count over a document | runtime |
|
|
1725
|
+
| Exported symbols must be used somewhere in the repo | every file, correlated | runtime |
|
|
1726
|
+
| Product name spelled the same across all docs | many documents, compared | runtime |
|
|
1727
|
+
| Files changed in the last release need a changelog entry | git metadata, not file contents | runtime |
|
|
1728
|
+
| Imports must resolve through the tsconfig path aliases | config chain resolution, outside the file | runtime |
|
|
1729
|
+
|
|
1730
|
+
5. **Split the runtime row on login state.** Runtime rules execute code,
|
|
1731
|
+
so they run only against a server-verified signature:
|
|
1732
|
+
- **logged in** → \`create-remote-rule\`. Go there directly; no recipe
|
|
1733
|
+
forwards to another.
|
|
1734
|
+
- **logged out** → \`create-runtime-rule\`, which explains the gate.
|
|
1735
|
+
|
|
1736
|
+
With \`ghOwner\` of \`[unknown]\`, \`create-remote-rule\` is not reachable
|
|
1737
|
+
at all: see step 6.
|
|
1738
|
+
|
|
1739
|
+
6. **Drop remote generation when \`ghOwner\` is \`[unknown]\`.** Generation
|
|
1740
|
+
runs against a verifiable org, which comes from a GitHub \`origin\`.
|
|
1741
|
+
Without one the tier cannot complete, so it is not a destination and
|
|
1742
|
+
must not be presented as one. Route to the local recipe the rule's
|
|
1743
|
+
shape calls for, and if the rule is only expressible at runtime, go
|
|
1744
|
+
to \`create-runtime-rule\`.
|
|
1745
|
+
|
|
1746
|
+
Say the tier is unavailable and why, in one line. A reader who is not
|
|
1747
|
+
told will read the omission as an oversight and ask for it, which
|
|
1748
|
+
costs a turn and ends where this step already is.
|
|
1749
|
+
|
|
1750
|
+
This is **not fixed by \`auth login\`**. It is a property of the
|
|
1751
|
+
project, not the session, so do not send them there for it.
|
|
1752
|
+
|
|
1753
|
+
Everything else still works: local authoring, \`verify\`, \`test\` and
|
|
1754
|
+
\`check\` need no remote of any kind.
|
|
1755
|
+
|
|
1756
|
+
7. **Offer the service only where it is a genuine choice.** When the
|
|
1757
|
+
rule is expressible locally, the user is logged in, **and** \`ghOwner\`
|
|
1758
|
+
is not \`[unknown]\`, present both and let them pick: a local rule you
|
|
1759
|
+
write and verify now, or a generation that costs against their plan.
|
|
1760
|
+
Say which you would choose and why.
|
|
1761
|
+
|
|
1762
|
+
Not logged in, no GitHub owner, or not locally expressible are not
|
|
1763
|
+
choices. Do not pose them as one.
|
|
1764
|
+
|
|
1765
|
+
8. **Name the command.** Finish by telling the user, or running, the
|
|
1766
|
+
exact fetch for the destination you chose:
|
|
1767
|
+
\`\`\`
|
|
1768
|
+
%(TASKLESS_CLI)s agent create-vale-rule
|
|
1769
|
+
\`\`\`
|
|
1770
|
+
A destination that is not a runnable command is a category, and a
|
|
1771
|
+
category is not an answer.
|
|
1772
|
+
|
|
1773
|
+
## When two destinations both fit
|
|
1774
|
+
|
|
1775
|
+
- **A linter the repo already runs AND a local Taskless rule both fit** →
|
|
1776
|
+
do not silently pick. Present both with trade-offs and let the user
|
|
1777
|
+
choose.
|
|
1778
|
+
- **A rule that seems to need two engines is usually two rules.** Split
|
|
1779
|
+
it and route each half, rather than forcing one engine to cover
|
|
1780
|
+
evidence it cannot see.
|
|
1781
|
+
- **You genuinely cannot tell** → ask the user a clarifying question.
|
|
1782
|
+
Uncertainty is a reason to ask; it is never a reason to spend a
|
|
1783
|
+
generation.
|
|
1784
|
+
|
|
1785
|
+
## Failure fallback (try-verify-escalate)
|
|
1786
|
+
|
|
1787
|
+
If you committed to a local rule on reasonable confidence and it then
|
|
1788
|
+
fails against the user's cases:
|
|
1789
|
+
|
|
1790
|
+
- Tell the user the local rule could not capture them.
|
|
1791
|
+
- State that generating via the service uses a generation and requires
|
|
1792
|
+
login.
|
|
1793
|
+
- Call the service only after the user confirms. On yes, fetch
|
|
1794
|
+
\`%(TASKLESS_CLI)s agent create-remote-rule\`.
|
|
1795
|
+
|
|
1796
|
+
Never fall through silently from a failed local attempt to a service
|
|
1797
|
+
call. A developer who watches a local attempt fail reads it as a
|
|
1798
|
+
Taskless failure, so only attempt local when you reasonably believe it
|
|
1799
|
+
will work.
|
|
1800
|
+
|
|
1801
|
+
## Important Notes
|
|
1802
|
+
|
|
1803
|
+
- **Reason first, route second.** Do not name a destination before the
|
|
1804
|
+
rationale in step 3 is written.
|
|
1805
|
+
- **What the code actually looks like outranks how the request was
|
|
1806
|
+
phrased.** Where you can see the code or the diff, weigh the concrete
|
|
1807
|
+
syntactic form present in the repository above the wording. The same
|
|
1808
|
+
sentence routes to \`create-sg-rule\` when the repository contains a
|
|
1809
|
+
form that correlates statically, and to runtime when the equivalent
|
|
1810
|
+
check would have to normalize a captured value against a declaration
|
|
1811
|
+
elsewhere.
|
|
1812
|
+
- **Engine reach and engine availability are two different questions.**
|
|
1813
|
+
Step 4's lists say what each engine can *parse*; they say nothing
|
|
1814
|
+
about whether the binary resolved on this host. A language on the list
|
|
1815
|
+
is still unusable where the platform build is missing.
|
|
1816
|
+
- **When it stays ambiguous, choose an engine whose availability you can
|
|
1817
|
+
assert, and say that is why.** There is no fixed fallback. \`sg\` and
|
|
1818
|
+
\`vale\` both ship as platform binaries, so either can be the missing one
|
|
1819
|
+
on an unsupported architecture or where an install was blocked;
|
|
1820
|
+
server-side the constraint is different again. A named default is
|
|
1821
|
+
wrong in whichever situation it failed to anticipate.
|
|
1822
|
+
- **Stay local when you reasonably can.** Reasonable confidence is
|
|
1823
|
+
enough to commit to a local rule, the failure fallback backstops a
|
|
1824
|
+
wrong-but-reasonable bet.
|
|
1825
|
+
|
|
1826
|
+
## See Also
|
|
1827
|
+
|
|
1828
|
+
- \`%(TASKLESS_CLI)s agent create-legacy-rule\`: author in a linter the repo already uses
|
|
1829
|
+
- \`%(TASKLESS_CLI)s agent create-sg-rule\`: author a local ast-grep rule (no login)
|
|
1830
|
+
- \`%(TASKLESS_CLI)s agent create-vale-rule\`: author a local Vale rule (no login)
|
|
1831
|
+
- \`%(TASKLESS_CLI)s agent create-runtime-rule\`: the runtime tier, logged out
|
|
1832
|
+
- \`%(TASKLESS_CLI)s agent create-remote-rule\`: generate via the service (login)
|
|
1833
|
+
- \`%(TASKLESS_CLI)s agent check\`: run every engine over the repo
|
|
1834
|
+
`, ae = `# Topic: rule-meta (CLI v%(CLI_VERSION)s / topic v3)
|
|
1835
|
+
|
|
1836
|
+
## Goal
|
|
1837
|
+
Report what \`%(TASKLESS_CLI)s rule meta\` does today, so no recipe and no
|
|
1838
|
+
agent builds a step on top of it.
|
|
1839
|
+
|
|
1840
|
+
## The sidecar does not exist
|
|
1841
|
+
|
|
1842
|
+
\`.taskless/rule-metadata/<id>.yml\` is written from the \`meta\` block of a
|
|
1843
|
+
rule status response. The rule service does not populate that block, so
|
|
1844
|
+
this CLI has never written a sidecar for any rule, under any tier, in
|
|
1845
|
+
any mode. \`rule meta <id>\` therefore has nothing to read and exits 1
|
|
1846
|
+
with \`RULE_META_UNAVAILABLE\` for every id, including ids whose rule is
|
|
1847
|
+
plainly on disk.
|
|
1848
|
+
|
|
1849
|
+
That code exists to keep the two cases apart. \`RULE_NOT_FOUND\` invites a
|
|
1850
|
+
retry with a different id; there is no id that works.
|
|
1851
|
+
|
|
1852
|
+
## What to do instead
|
|
1853
|
+
|
|
1854
|
+
\`rule improve\` needs the ticket id, and that id comes from the machine
|
|
1855
|
+
that created the rule, not from disk:
|
|
1856
|
+
|
|
1857
|
+
- \`%(TASKLESS_CLI)s rule create --json\` prints it as \`ruleId\` on
|
|
1858
|
+
success. Record it when you create a rule you expect to iterate on.
|
|
1859
|
+
- If the id was not recorded, ask the user for it.
|
|
1860
|
+
- If nobody has it, iterate locally: fetch
|
|
1861
|
+
\`%(TASKLESS_CLI)s agent improve-rule --anonymous\`.
|
|
1862
|
+
|
|
1863
|
+
## Errors
|
|
1864
|
+
|
|
1865
|
+
| code | meaning | fix |
|
|
1866
|
+
|-------------------------|------------------------------------------|---------------------------------------|
|
|
1867
|
+
| \`RULE_META_UNAVAILABLE\` | no sidecar exists, and none is written | Use the ticket id from \`rule create\` |
|
|
1868
|
+
| \`INVALID_INPUT\` | a sidecar exists and is malformed | Delete it; nothing here depends on it |
|
|
1869
|
+
|
|
1870
|
+
## See Also
|
|
1871
|
+
|
|
1872
|
+
- \`%(TASKLESS_CLI)s agent improve-rule\`: how the ticket id is actually sourced
|
|
1873
|
+
`, re = `# Topic: rule (CLI v%(CLI_VERSION)s / topic v2)
|
|
1874
|
+
|
|
1875
|
+
## Goal
|
|
1876
|
+
Umbrella for rule operations. Fetch the topic for the action you want.
|
|
1877
|
+
|
|
1878
|
+
## Topics
|
|
1879
|
+
|
|
1880
|
+
| Action | Recipe |
|
|
1881
|
+
|--------------------|-------------------------------------------------------|
|
|
1882
|
+
| Create a rule | \`%(TASKLESS_CLI)s agent route\` |
|
|
1883
|
+
| Improve a rule | \`%(TASKLESS_CLI)s agent improve-rule\` |
|
|
1884
|
+
| Delete a rule | \`%(TASKLESS_CLI)s agent delete-rule\` |
|
|
1885
|
+
| Verify a rule | \`%(TASKLESS_CLI)s agent verify-rule\` (agent-internal) |
|
|
1886
|
+
| Read rule metadata | \`%(TASKLESS_CLI)s agent rule-meta\` (agent-internal) |
|
|
1887
|
+
|
|
1888
|
+
\`rule meta\` reads a sidecar this CLI never writes, so it fails for every
|
|
1889
|
+
rule. Fetch \`rule-meta\` only to learn what to do instead. \`improve-rule\`
|
|
1890
|
+
takes the ticket id from \`rule create --json\`.
|
|
1891
|
+
|
|
1892
|
+
\`route\` is the entry point for authoring: it reads the request and
|
|
1893
|
+
names the \`create-*-rule\` topic that fits, so you do not pick an engine
|
|
1894
|
+
yourself.
|
|
1895
|
+
|
|
1896
|
+
For the local-only flow on improve, append \`--anonymous\`.
|
|
1897
|
+
|
|
1898
|
+
## See Also
|
|
1899
|
+
|
|
1900
|
+
- \`%(TASKLESS_CLI)s agent check\`: run all configured rules
|
|
1901
|
+
`, ie = `# Topic: update (CLI v%(CLI_VERSION)s / topic v5)
|
|
1902
|
+
|
|
1903
|
+
## You are here
|
|
1904
|
+
This is \`update\`. It tells you what an upgrade changed for the rules
|
|
1905
|
+
ALREADY IN THIS PROJECT, and what to do about them.
|
|
1906
|
+
|
|
1907
|
+
It is not about installing. Running \`%(TASKLESS_CLI)s\` migrates the
|
|
1908
|
+
\`.taskless/\` layout and refreshes the installed skills on its own. That
|
|
1909
|
+
handles the DIRECTORY. This recipe handles the RULES, which no migration
|
|
1910
|
+
can rewrite for you: a rewriter that now needs a \`fix\`, a rule whose
|
|
1911
|
+
matching semantics shifted under a new engine, a rule that could now be
|
|
1912
|
+
expressed better with a newly supported language.
|
|
1913
|
+
|
|
1914
|
+
An agent that has run a migration and watched it succeed will otherwise
|
|
1915
|
+
reasonably conclude the upgrade is done. It is not. The directory is
|
|
1916
|
+
migrated; the rules may still need work.
|
|
1917
|
+
|
|
1918
|
+
## Goal
|
|
1919
|
+
Walk the ledger below from the version this project was last reconciled
|
|
1920
|
+
to, up to the installed CLI, doing what each section says. Then record
|
|
1921
|
+
that you finished.
|
|
1922
|
+
|
|
1923
|
+
## Preconditions
|
|
1924
|
+
- \`.taskless/\` exists. If it does not, there are no rules to reconcile.
|
|
1925
|
+
- You can read \`.taskless/taskless.json\`.
|
|
1926
|
+
|
|
1927
|
+
## Steps
|
|
1928
|
+
|
|
1929
|
+
1. **Find where to start.** Run:
|
|
1930
|
+
\`\`\`
|
|
1931
|
+
%(TASKLESS_CLI)s info --json
|
|
1932
|
+
\`\`\`
|
|
1933
|
+
and read \`rules.reconciledTo\`, plus \`rules.engines.sg\` and
|
|
1934
|
+
\`rules.engines.vale\` for the engine versions the rules were built
|
|
1935
|
+
against.
|
|
1936
|
+
|
|
1937
|
+
**No \`rules.reconciledTo\` at all** means this project PREDATES the
|
|
1938
|
+
ledger, so treat it as \`0.0.0\` and walk every section. It does not
|
|
1939
|
+
mean the project is new: a project this CLI set up has the field
|
|
1940
|
+
stamped at creation, so an absent marker is the one case where none
|
|
1941
|
+
of the entries below have ever been applied.
|
|
1942
|
+
|
|
1943
|
+
\`rules.walk\` on that same payload is this decision already made:
|
|
1944
|
+
\`{ "from": ..., "to": ... }\` when there is something to walk, and
|
|
1945
|
+
\`null\` when there is not. Prefer it to re-deriving the boundary
|
|
1946
|
+
yourself, so the offer and the enforcement cannot disagree.
|
|
1947
|
+
|
|
1948
|
+
2. **Walk forward, in order.** Start at the section after
|
|
1949
|
+
\`reconciledTo\` and continue to the installed version. Sections are
|
|
1950
|
+
cumulative, never replaceable: a later one can depend on an earlier
|
|
1951
|
+
one having run, so do not skip ahead to the newest.
|
|
1952
|
+
|
|
1953
|
+
3. **Do the work each section names.** Each says what changed and what
|
|
1954
|
+
it means for existing rules. A section that says there is nothing to
|
|
1955
|
+
do means exactly that; it is a claim, not an oversight.
|
|
1956
|
+
|
|
1957
|
+
4. **Record that you finished.** Run:
|
|
1958
|
+
\`\`\`
|
|
1959
|
+
%(TASKLESS_CLI)s update --rules
|
|
1960
|
+
\`\`\`
|
|
1961
|
+
Only after the walk is complete. There is no version to pass: the CLI
|
|
1962
|
+
stamps its own, because the only sensible endpoint of a walk is the
|
|
1963
|
+
version you are running.
|
|
1964
|
+
|
|
1965
|
+
A partial or abandoned walk must not be recorded. The next agent
|
|
1966
|
+
starts from what you wrote and skips everything you did not do, and
|
|
1967
|
+
it will look like there was nothing to do. Nothing can check this for
|
|
1968
|
+
you: the CLI refuses to move the marker backwards, but it cannot tell
|
|
1969
|
+
whether you read the sections.
|
|
1970
|
+
|
|
1971
|
+
This also records the engine versions the rules are now valid
|
|
1972
|
+
against, which is what lets a later upgrade say what moved.
|
|
1973
|
+
|
|
1974
|
+
## The ledger
|
|
1975
|
+
|
|
1976
|
+
### Migrating to 0.11.x
|
|
1977
|
+
|
|
1978
|
+
The vendored ast-grep moves from 0.41.0 to 0.45.2, and Vale from 3.18.0
|
|
1979
|
+
to 3.20.0. Nine things follow for existing rules.
|
|
1980
|
+
|
|
1981
|
+
**Elixir stopped being linted as prose.** Vale 3.19.0 reads \`.ex\` and
|
|
1982
|
+
\`.exs\` through a real parser, so it now sees comments and \`@doc\`
|
|
1983
|
+
attributes and nothing else. On 3.18.0 those files had no parser and
|
|
1984
|
+
were linted whole, so a Vale rule matching \`[*.ex]\` fired on function
|
|
1985
|
+
names, atoms, and string literals as well as on comments.
|
|
1986
|
+
|
|
1987
|
+
Those findings are gone, and nothing errors or warns. A rule that
|
|
1988
|
+
reported on Elixir code bodies now reports less, which is a drop you
|
|
1989
|
+
would otherwise notice only as a suspiciously clean run. If the rule was
|
|
1990
|
+
meant for prose, this is the behaviour you always wanted. If it was
|
|
1991
|
+
being used to catch something in the code itself, that is a job for an
|
|
1992
|
+
\`sg\` rule, and \`%(TASKLESS_CLI)s rule create\` will route it there.
|
|
1993
|
+
|
|
1994
|
+
**MDX components' children are prose now, and new findings appear.**
|
|
1995
|
+
As of Vale 3.19.0 a JSX element's children are read as the Markdown
|
|
1996
|
+
they are, so a rule over \`.mdx\` covers prose inside a wrapping
|
|
1997
|
+
component such as \`<Steps>\` or \`<Aside>\` that it previously skipped.
|
|
1998
|
+
Those children also carry the component name as a \`text.class.<name>\`
|
|
1999
|
+
scope, which a rule can target.
|
|
2000
|
+
|
|
2001
|
+
This is the opposite direction from Elixir: coverage grew, so a rule
|
|
2002
|
+
reports MORE than it did. Read the new findings as real, and narrow the
|
|
2003
|
+
rule's \`scope\` only if the component's prose was deliberately out of
|
|
2004
|
+
reach.
|
|
2005
|
+
|
|
2006
|
+
**An exception zone that did nothing now takes effect, and findings
|
|
2007
|
+
disappear.** Vale 3.20.0 records the region each \`<!-- vale <id>.<id> =
|
|
2008
|
+
NO -->\` and \`= YES\` pair covers, and suppresses any alert located
|
|
2009
|
+
inside it. Through 3.19.0 an *inline* pair (at a list item's
|
|
2010
|
+
continuation indent, with no blank line between it and the prose it
|
|
2011
|
+
wraps) was read once per block, so the two halves cancelled out and the
|
|
2012
|
+
zone did nothing at all.
|
|
2013
|
+
|
|
2014
|
+
So a marker already sitting in this project's documents was inert and
|
|
2015
|
+
is now live. Nothing errors: findings a document has reported for as
|
|
2016
|
+
long as the rule existed stop appearing, which reads as a rule that
|
|
2017
|
+
broke. Before treating a drop as a regression, look for the
|
|
2018
|
+
markers: \`git grep -n '<!-- vale'\`. Keep the zone if it was meant; if
|
|
2019
|
+
it was a half-finished experiment, delete the pair and the coverage
|
|
2020
|
+
comes back.
|
|
2021
|
+
|
|
2022
|
+
**A \`raw\`-scoped rule can be exempted in a document now.** Directives
|
|
2023
|
+
reach \`raw\` rules as of 3.20.0. Before, they were applied to the parsed
|
|
2024
|
+
document only and a \`raw\` rule fired straight through every zone, so a
|
|
2025
|
+
rule about a shell command, a flag, or a package name could only be
|
|
2026
|
+
removed, never exempted case by case. A rule that was narrowed or
|
|
2027
|
+
abandoned for that reason is worth revisiting.
|
|
2028
|
+
|
|
2029
|
+
One caveat comes with it: at \`raw\` scope the directive line is itself
|
|
2030
|
+
linted text, so a rule whose token appears in its own id reports a
|
|
2031
|
+
finding on the marker that silences it. Rename the rule, or scope it.
|
|
2032
|
+
|
|
2033
|
+
**\`sequence\` accepts \`exceptions\`.** The field is new in 3.20.0 and
|
|
2034
|
+
purely additive: no existing rule changes behaviour, and a sequence
|
|
2035
|
+
rule that was over-firing on a known phrase can now carry it. Upstream
|
|
2036
|
+
also changed how a negated sequence token is satisfied at a sentence
|
|
2037
|
+
boundary. No shape we tried reproduced a difference between 3.19.0 and
|
|
2038
|
+
3.20.0, so there is nothing to do unless you see one.
|
|
2039
|
+
|
|
2040
|
+
**A rewriter now requires \`fix\`.** \`SerializableRewriter.required\` goes
|
|
2041
|
+
from \`["rule","id"]\` to \`["id","fix","rule"]\`, so a \`rewriters:\` entry
|
|
2042
|
+
without a \`fix:\` is now rejected where it was accepted before.
|
|
2043
|
+
|
|
2044
|
+
Run \`%(TASKLESS_CLI)s verify\` and it names the offending rewriter
|
|
2045
|
+
directly. It cannot be auto-fixed: \`fix\` is replacement text, which is
|
|
2046
|
+
authorial intent. A rewriter with no \`fix\` could never have done
|
|
2047
|
+
anything, so this surfaces a rule that was already dead rather than
|
|
2048
|
+
breaking one that worked.
|
|
2049
|
+
|
|
2050
|
+
**Markdown is now a language, with a narrow shape.** \`language:
|
|
2051
|
+
Markdown\` parses. What it sees is the BLOCK tree only: \`document\`,
|
|
2052
|
+
\`section\`, \`atx_heading\`, \`setext_heading\`, \`fenced_code_block\`,
|
|
2053
|
+
\`list_item\`, \`paragraph\`. Everything inside a line collapses into one
|
|
2054
|
+
opaque \`inline\` node, so there is no \`link\`, no \`emphasis\`, no
|
|
2055
|
+
\`strong_emphasis\`.
|
|
2056
|
+
|
|
2057
|
+
Two failure shapes, and they are opposites. \`kind: link\` is a HARD
|
|
2058
|
+
CONFIG ERROR: exit 8, \`Kind \\\`link\\\` is invalid\`, which aborts config
|
|
2059
|
+
parsing and takes every other rule's report down with it. A pattern like
|
|
2060
|
+
\`[$T]($U)\` is the quiet one: it parses, runs, exits 0, and matches
|
|
2061
|
+
nothing forever.
|
|
2062
|
+
|
|
2063
|
+
So "no bare URLs" and "link text must not say click here" are not
|
|
2064
|
+
ast-grep rules even now. Use Vale, which reads prose, and note that Vale
|
|
2065
|
+
DOES see YAML frontmatter: with \`scope: raw\` it sees the keys too, and
|
|
2066
|
+
\`extends: occurrence\` with \`min: 1\` can require a field to be present.
|
|
2067
|
+
|
|
2068
|
+
**\`sg run --lang\` accepts the alias spellings.** At 0.41.0 \`--lang C++\`
|
|
2069
|
+
was rejected while \`language: C++\` parsed. At 0.45.2 both are accepted.
|
|
2070
|
+
This affects the flag only, not a rule's \`language:\` field, so no rule
|
|
2071
|
+
file needs changing. It is recorded because the divergence used to be
|
|
2072
|
+
documented as a thing to work around.
|
|
2073
|
+
|
|
2074
|
+
**Matching semantics moved, and a valid, unchanged rule can now match a
|
|
2075
|
+
DIFFERENT SET OF NODES with no error and no warning.** Two shapes are
|
|
2076
|
+
affected. Both were measured against the two binaries, so this section
|
|
2077
|
+
says what to do rather than what to watch for.
|
|
2078
|
+
|
|
2079
|
+
**A rule was silently dead and now fires.** If a rule uses \`nthChild\`
|
|
2080
|
+
with an \`ofRule\` whose body binds a metavariable:
|
|
2081
|
+
|
|
2082
|
+
\`\`\`
|
|
2083
|
+
nthChild:
|
|
2084
|
+
position: 2
|
|
2085
|
+
ofRule: { pattern: $S }
|
|
2086
|
+
\`\`\`
|
|
2087
|
+
|
|
2088
|
+
then at 0.41.0 it matched **nothing**, because \`ofRule\` reused one
|
|
2089
|
+
environment across siblings: the first match committed \`$S\` and every
|
|
2090
|
+
later sibling failed the consistency check and went uncounted. It now
|
|
2091
|
+
counts correctly and reports.
|
|
2092
|
+
|
|
2093
|
+
So the rule is not broken, it was inert, and the findings it produces on
|
|
2094
|
+
this version have never been seen. Run \`%(TASKLESS_CLI)s check\` and read
|
|
2095
|
+
them as new: they are real matches the rule was always meant to make,
|
|
2096
|
+
and they may be numerous on a codebase that has never been checked
|
|
2097
|
+
against it. Fixtures are no help here, since a rule that matched nothing
|
|
2098
|
+
passed the \`pass/\` side of its own tests.
|
|
2099
|
+
|
|
2100
|
+
A rule whose \`ofRule\` used a non-binding matcher, \`kind:\` or \`regex:\`
|
|
2101
|
+
with no metavariable, is unaffected: only binding was broken.
|
|
2102
|
+
|
|
2103
|
+
**A metavariable is now empty where it used to carry a value.** If a
|
|
2104
|
+
rule binds a metavariable inside a negated \`not\` and then references it
|
|
2105
|
+
in \`message\`, \`fix\`, or a constraint:
|
|
2106
|
+
|
|
2107
|
+
\`\`\`
|
|
2108
|
+
follows:
|
|
2109
|
+
not:
|
|
2110
|
+
pattern: return $A
|
|
2111
|
+
stopBy: end
|
|
2112
|
+
message: "found after $A"
|
|
2113
|
+
\`\`\`
|
|
2114
|
+
|
|
2115
|
+
then at 0.41.0 \`$A\` rendered with a value leaked from the candidate the
|
|
2116
|
+
negation REJECTED. A \`not\` contributes no bindings by definition, since
|
|
2117
|
+
a successful negation means the inner rule did not match, so that value
|
|
2118
|
+
was never meaningful. It is now unbound and renders empty.
|
|
2119
|
+
|
|
2120
|
+
This one will not change your finding counts. The match is identical:
|
|
2121
|
+
same file, same range, same rule. Only the rendered output differs, so
|
|
2122
|
+
grep your rules for a metavariable that appears both inside a \`not\` and
|
|
2123
|
+
in a \`message\` or \`fix\`. A \`fix\` in that shape has been writing the
|
|
2124
|
+
leaked text into people's files.
|
|
2125
|
+
|
|
2126
|
+
**Root metavariables and comments** also changed upstream
|
|
2127
|
+
(ast-grep/ast-grep#2868), but no shape we tried reproduced a difference,
|
|
2128
|
+
including the TSX case that PR names. Nothing to do unless you see one.
|
|
2129
|
+
|
|
2130
|
+
## Errors
|
|
2131
|
+
|
|
2132
|
+
With \`--json\`, \`--rules\` failures emit \`{ ok: false, code, message }\`:
|
|
2133
|
+
|
|
2134
|
+
| code | meaning | fix |
|
|
2135
|
+
|-----------------|------------------------------------------------|--------------------------------------------|
|
|
2136
|
+
| \`INVALID_INPUT\` | no \`.taskless/\`, or an older CLI would rewind the marker | set the project up, or upgrade first |
|
|
2137
|
+
|
|
2138
|
+
## See Also
|
|
2139
|
+
|
|
2140
|
+
- \`%(TASKLESS_CLI)s agent check\`: run every engine over the repo
|
|
2141
|
+
- \`%(TASKLESS_CLI)s agent improve-rule\`: rewrite a rule the walk flagged
|
|
2142
|
+
- \`%(TASKLESS_CLI)s agent info\`: what is installed, and staleness
|
|
2143
|
+
`, oe = '# Topic: verify-rule (CLI v%(CLI_VERSION)s / topic v3)\n\n## Goal\nCheck that a rule is well-formed (`verify`), then run its tests\n(`test`). Both address rules **by path**, so one command works the same\nfor every engine. Primary consumer is the create/improve feedback loop,\nbut agents can call either directly.\n\n## Preconditions\n- The rule exists at `.taskless/rules/<engine>/<id>/`.\n- Tests are optional for `verify` and are what `test` runs.\n- No auth required.\n\n## The two commands\n\n```\n%(TASKLESS_CLI)s verify <path> --json\n%(TASKLESS_CLI)s test <path> --json\n```\n\n`verify` asks whether the rule has the components its engine requires.\nIt never runs the rule against anything, so it works on a rule whose\ntests do not exist yet. That is the point of splitting the two: an\nagent part-way through authoring needs to know the rule itself is valid\nbefore it can write a meaningful test for it.\n\n`test` runs the rule\'s tests. **It runs `verify` first and stops on\nfailure**, so a rule that is both malformed and under-fixtured reports\nthe malformation. The fixture complaint is the less useful of the two\nand would otherwise bury the reason the rule could never have run.\n\nWhat each engine is checked for:\n\n| engine | `verify` checks | `test` runs |\n|-----------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|\n| `sg` | ast-grep schema, plus `id`/`language`/`severity`/`message`/`rule`, and `regex` accompanied by `kind` | the `valid`/`invalid` cases in `.tests/` |\n| `vale` | style parses, `extends` and `message` present, `level` in vocabulary, and the rule\'s `.vale.ini` enables `<id>.<id>` under a matcher | the `.tests/pass/` and `.tests/fail/` buckets |\n| `runtime` | `check.ts` present, at least one capture rule in `captures/` | each directory under `.tests/pass/` and `.tests/fail/`, as the check\'s `root` |\n\nA runtime rule\'s fixtures execute its `check.ts`, so they run only under\nthe policy `check` already applies: an authenticated reconcile that\nreturns the rule\'s signature, or `--dangerously-run-scripts`. When\nneither holds, that rule prints `○` rather than a tick, names the reason\nand the flag, and is reported as `"ran": false` with a `refused` string\nsaying why. It is counted under "did not run" rather than among the\nrules tested, and on its own it does not fail the command.\n\nRead `ran` before `ok`. A runtime rule that did not run is not a rule\nthat passed, and treating the two alike is the defect this reporting\nexists to prevent.\n\n## What a path means\n\nThe path is a location under `.taskless/rules/`, and the `<engine>`\nsegment is what decides the engine. Nothing parses a rule file to work\nout who owns it, so the same id under two engines is never ambiguous.\n\n| Path | Scope |\n|----------------------------------|-----------------------------------------------------|\n| `.taskless/rules/vale/no-simply` | that one rule |\n| `.taskless/rules/vale` | every Vale rule |\n| `.taskless/rules` | every rule (also the default when you pass no path) |\n\nA path outside `.taskless/rules/` is an error naming the path. So is a\npath inside it that holds no rule.\n\n## Output\n\nBoth commands answer in the same shape, one entry per rule:\n\n```json\n{\n "ok": false,\n "rules": [\n {"engine":"sg","ruleId":"no-eval","ok":true,"errors":[]},\n {"engine":"vale","ruleId":"no-simply","ok":false,\n "errors":["no-simply/.vale.ini never enables no-simply.no-simply, so the rule is present but off."]}\n ]\n}\n```\n\n- `ok` at the top is true only when every rule passed.\n- `errors` is one string per problem, written to be read by a person.\n- `test` adds `ran`, which is false when `verify` failed and the tests\n never executed, and false for a runtime rule the execution policy\n refused. The second case also carries `refused` with the reason, and\n is the one where `ok` alone would mislead you.\n\nWithout `--json` the same information prints as a `✓`/`✗`/`○` line per\nrule with its errors indented beneath. `○` is a rule that did not run.\n\n## Exit codes\n\n- `0`: every rule in scope passed. An empty rules tree also exits 0: a\n project that has not written a rule yet is an ordinary state, and\n failing there would make `verify` unusable in CI on a fresh install.\n- `1`: any rule failed, or the path could not be resolved.\n\n## In a feedback loop\n\nRun `verify` as soon as the rule file exists, fix what it names, then\nwrite tests and run `test`. Cap at 3 attempts and report to the user if\nit still fails.\n\n## See Also\n\n- `%(TASKLESS_CLI)s agent create-sg-rule`: author an ast-grep rule\n- `%(TASKLESS_CLI)s agent create-vale-rule`: author a Vale rule\n- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists\n';
|
|
2144
|
+
var I = {}, _;
|
|
2145
|
+
function le() {
|
|
2146
|
+
return _ || (_ = 1, (function(t) {
|
|
2147
|
+
(function() {
|
|
2148
|
+
var n = {
|
|
2149
|
+
not_type: /[^T]/,
|
|
2150
|
+
not_primitive: /[^v]/,
|
|
2151
|
+
number: /[diefg]/,
|
|
2152
|
+
numeric_arg: /[bcdiefguxX]/,
|
|
2153
|
+
json: /[j]/,
|
|
2154
|
+
text: /^[^\x25]+/,
|
|
2155
|
+
modulo: /^\x25{2}/,
|
|
2156
|
+
placeholder: /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
|
|
2157
|
+
key: /^([a-z_][a-z_\d]*)/i,
|
|
2158
|
+
key_access: /^\.([a-z_][a-z_\d]*)/i,
|
|
2159
|
+
index_access: /^\[(\d+)\]/,
|
|
2160
|
+
sign: /^[+-]/
|
|
2161
|
+
};
|
|
2162
|
+
function s(h) {
|
|
2163
|
+
return l(b(h), arguments);
|
|
2164
|
+
}
|
|
2165
|
+
function r(h, u) {
|
|
2166
|
+
return s.apply(null, [h].concat(u || []));
|
|
2167
|
+
}
|
|
2168
|
+
function l(h, u) {
|
|
2169
|
+
var i = 1, y = h.length, e, p = "", d, c, a, v, k, S, T, w;
|
|
2170
|
+
for (d = 0; d < y; d++)
|
|
2171
|
+
if (typeof h[d] == "string")
|
|
2172
|
+
p += h[d];
|
|
2173
|
+
else if (typeof h[d] == "object") {
|
|
2174
|
+
if (a = h[d], a.keys)
|
|
2175
|
+
for (e = u[i], c = 0; c < a.keys.length; c++) {
|
|
2176
|
+
if (e == null)
|
|
2177
|
+
throw new Error(s('[sprintf] Cannot access property "%s" of undefined value "%s"', a.keys[c], a.keys[c - 1]));
|
|
2178
|
+
e = e[a.keys[c]];
|
|
2179
|
+
}
|
|
2180
|
+
else a.param_no ? e = u[a.param_no] : e = u[i++];
|
|
2181
|
+
if (n.not_type.test(a.type) && n.not_primitive.test(a.type) && e instanceof Function && (e = e()), n.numeric_arg.test(a.type) && typeof e != "number" && isNaN(e))
|
|
2182
|
+
throw new TypeError(s("[sprintf] expecting number but found %T", e));
|
|
2183
|
+
switch (n.number.test(a.type) && (T = e >= 0), a.type) {
|
|
2184
|
+
case "b":
|
|
2185
|
+
e = parseInt(e, 10).toString(2);
|
|
2186
|
+
break;
|
|
2187
|
+
case "c":
|
|
2188
|
+
e = String.fromCharCode(parseInt(e, 10));
|
|
2189
|
+
break;
|
|
2190
|
+
case "d":
|
|
2191
|
+
case "i":
|
|
2192
|
+
e = parseInt(e, 10);
|
|
2193
|
+
break;
|
|
2194
|
+
case "j":
|
|
2195
|
+
e = JSON.stringify(e, null, a.width ? parseInt(a.width) : 0);
|
|
2196
|
+
break;
|
|
2197
|
+
case "e":
|
|
2198
|
+
e = a.precision ? parseFloat(e).toExponential(a.precision) : parseFloat(e).toExponential();
|
|
2199
|
+
break;
|
|
2200
|
+
case "f":
|
|
2201
|
+
e = a.precision ? parseFloat(e).toFixed(a.precision) : parseFloat(e);
|
|
2202
|
+
break;
|
|
2203
|
+
case "g":
|
|
2204
|
+
e = a.precision ? String(Number(e.toPrecision(a.precision))) : parseFloat(e);
|
|
2205
|
+
break;
|
|
2206
|
+
case "o":
|
|
2207
|
+
e = (parseInt(e, 10) >>> 0).toString(8);
|
|
2208
|
+
break;
|
|
2209
|
+
case "s":
|
|
2210
|
+
e = String(e), e = a.precision ? e.substring(0, a.precision) : e;
|
|
2211
|
+
break;
|
|
2212
|
+
case "t":
|
|
2213
|
+
e = String(!!e), e = a.precision ? e.substring(0, a.precision) : e;
|
|
2214
|
+
break;
|
|
2215
|
+
case "T":
|
|
2216
|
+
e = Object.prototype.toString.call(e).slice(8, -1).toLowerCase(), e = a.precision ? e.substring(0, a.precision) : e;
|
|
2217
|
+
break;
|
|
2218
|
+
case "u":
|
|
2219
|
+
e = parseInt(e, 10) >>> 0;
|
|
2220
|
+
break;
|
|
2221
|
+
case "v":
|
|
2222
|
+
e = e.valueOf(), e = a.precision ? e.substring(0, a.precision) : e;
|
|
2223
|
+
break;
|
|
2224
|
+
case "x":
|
|
2225
|
+
e = (parseInt(e, 10) >>> 0).toString(16);
|
|
2226
|
+
break;
|
|
2227
|
+
case "X":
|
|
2228
|
+
e = (parseInt(e, 10) >>> 0).toString(16).toUpperCase();
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
n.json.test(a.type) ? p += e : (n.number.test(a.type) && (!T || a.sign) ? (w = T ? "+" : "-", e = e.toString().replace(n.sign, "")) : w = "", k = a.pad_char ? a.pad_char === "0" ? "0" : a.pad_char.charAt(1) : " ", S = a.width - (w + e).length, v = a.width && S > 0 ? k.repeat(S) : "", p += a.align ? w + e + v : k === "0" ? w + v + e : v + w + e);
|
|
2232
|
+
}
|
|
2233
|
+
return p;
|
|
2234
|
+
}
|
|
2235
|
+
var m = /* @__PURE__ */ Object.create(null);
|
|
2236
|
+
function b(h) {
|
|
2237
|
+
if (m[h])
|
|
2238
|
+
return m[h];
|
|
2239
|
+
for (var u = h, i, y = [], e = 0; u; ) {
|
|
2240
|
+
if ((i = n.text.exec(u)) !== null)
|
|
2241
|
+
y.push(i[0]);
|
|
2242
|
+
else if ((i = n.modulo.exec(u)) !== null)
|
|
2243
|
+
y.push("%");
|
|
2244
|
+
else if ((i = n.placeholder.exec(u)) !== null) {
|
|
2245
|
+
if (i[2]) {
|
|
2246
|
+
e |= 1;
|
|
2247
|
+
var p = [], d = i[2], c = [];
|
|
2248
|
+
if ((c = n.key.exec(d)) !== null)
|
|
2249
|
+
for (p.push(c[1]); (d = d.substring(c[0].length)) !== ""; )
|
|
2250
|
+
if ((c = n.key_access.exec(d)) !== null)
|
|
2251
|
+
p.push(c[1]);
|
|
2252
|
+
else if ((c = n.index_access.exec(d)) !== null)
|
|
2253
|
+
p.push(c[1]);
|
|
2254
|
+
else
|
|
2255
|
+
throw new SyntaxError("[sprintf] failed to parse named argument key");
|
|
2256
|
+
else
|
|
2257
|
+
throw new SyntaxError("[sprintf] failed to parse named argument key");
|
|
2258
|
+
i[2] = p;
|
|
2259
|
+
} else
|
|
2260
|
+
e |= 2;
|
|
2261
|
+
if (e === 3)
|
|
2262
|
+
throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");
|
|
2263
|
+
y.push(
|
|
2264
|
+
{
|
|
2265
|
+
placeholder: i[0],
|
|
2266
|
+
param_no: i[1],
|
|
2267
|
+
keys: i[2],
|
|
2268
|
+
sign: i[3],
|
|
2269
|
+
pad_char: i[4],
|
|
2270
|
+
align: i[5],
|
|
2271
|
+
width: i[6],
|
|
2272
|
+
precision: i[7],
|
|
2273
|
+
type: i[8]
|
|
2274
|
+
}
|
|
2275
|
+
);
|
|
2276
|
+
} else
|
|
2277
|
+
throw new SyntaxError("[sprintf] unexpected placeholder");
|
|
2278
|
+
u = u.substring(i[0].length);
|
|
2279
|
+
}
|
|
2280
|
+
return m[h] = y;
|
|
2281
|
+
}
|
|
2282
|
+
t.sprintf = s, t.vsprintf = r, typeof window < "u" && (window.sprintf = s, window.vsprintf = r);
|
|
2283
|
+
})();
|
|
2284
|
+
})(I)), I;
|
|
2285
|
+
}
|
|
2286
|
+
var j = le();
|
|
2287
|
+
const he = f({
|
|
2288
|
+
prompt: o().trim().min(1, "prompt must be a non-empty string").describe("Description of the rule to generate"),
|
|
2289
|
+
successCases: g(o()).optional().describe("Examples of correct code that should pass the rule"),
|
|
2290
|
+
failureCases: g(o()).optional().describe("Examples of incorrect code that should fail the rule")
|
|
2291
|
+
}), qe = f({
|
|
2292
|
+
success: K(!0),
|
|
2293
|
+
ruleId: o().describe("UUID of the generated rule job"),
|
|
2294
|
+
rules: g(o()).describe("Rule IDs that were generated"),
|
|
2295
|
+
files: g(o()).describe("File paths that were written"),
|
|
2296
|
+
notices: g(o()).optional().describe(
|
|
2297
|
+
"Advisory messages about a delivery that was still written — a rule delivered with no `.tests/` fixtures above all. Present only when there is something to say. A machine consumer cannot read stderr prose, so these are carried here rather than only printed"
|
|
2298
|
+
)
|
|
2299
|
+
});
|
|
2300
|
+
f({
|
|
2301
|
+
error: o().describe("Error message")
|
|
2302
|
+
});
|
|
2303
|
+
const ce = f({
|
|
2304
|
+
ruleId: o().trim().min(1, "ruleId must be a non-empty string").describe("ID of the rule to improve"),
|
|
2305
|
+
guidance: o().trim().min(1, "guidance must be a non-empty string").describe("Feedback for iterating on the existing rule"),
|
|
2306
|
+
references: g(
|
|
2307
|
+
f({
|
|
2308
|
+
filename: o().describe("File path relative to .taskless/"),
|
|
2309
|
+
content: o().describe("File content")
|
|
2310
|
+
})
|
|
2311
|
+
).optional().describe("Reference files to include as context")
|
|
2312
|
+
}), Ue = f({
|
|
2313
|
+
success: K(!0),
|
|
2314
|
+
requestId: o().describe("The request ID for polling status"),
|
|
2315
|
+
rules: g(o()).describe("Rule IDs that were updated"),
|
|
2316
|
+
files: g(o()).describe("File paths that were written"),
|
|
2317
|
+
notices: g(o()).optional().describe(
|
|
2318
|
+
"Advisory messages about a delivery that was still written — a rule delivered with no `.tests/` fixtures above all. Present only when there is something to say. A machine consumer cannot read stderr prose, so these are carried here rather than only printed"
|
|
2319
|
+
)
|
|
2320
|
+
});
|
|
2321
|
+
f({
|
|
2322
|
+
error: o().describe("Error message")
|
|
2323
|
+
});
|
|
2324
|
+
const de = "0.45.2", G = [
|
|
2325
|
+
"Bash",
|
|
2326
|
+
"C",
|
|
2327
|
+
"Cpp",
|
|
2328
|
+
"CSharp",
|
|
2329
|
+
"Css",
|
|
2330
|
+
"Dart",
|
|
2331
|
+
"Elixir",
|
|
2332
|
+
"Go",
|
|
2333
|
+
"Haskell",
|
|
2334
|
+
"Hcl",
|
|
2335
|
+
"Html",
|
|
2336
|
+
"Java",
|
|
2337
|
+
"JavaScript",
|
|
2338
|
+
"Json",
|
|
2339
|
+
"Kotlin",
|
|
2340
|
+
"Lua",
|
|
2341
|
+
"Markdown",
|
|
2342
|
+
"Nix",
|
|
2343
|
+
"Php",
|
|
2344
|
+
"Python",
|
|
2345
|
+
"Ruby",
|
|
2346
|
+
"Rust",
|
|
2347
|
+
"Scala",
|
|
2348
|
+
"Solidity",
|
|
2349
|
+
"Swift",
|
|
2350
|
+
"Tsx",
|
|
2351
|
+
"TypeScript",
|
|
2352
|
+
"Yaml"
|
|
2353
|
+
], ue = {
|
|
2354
|
+
"c++": "Cpp",
|
|
2355
|
+
cc: "Cpp",
|
|
2356
|
+
cs: "CSharp",
|
|
2357
|
+
cxx: "Cpp",
|
|
2358
|
+
ex: "Elixir",
|
|
2359
|
+
golang: "Go",
|
|
2360
|
+
hs: "Haskell",
|
|
2361
|
+
js: "JavaScript",
|
|
2362
|
+
jsx: "JavaScript",
|
|
2363
|
+
kt: "Kotlin",
|
|
2364
|
+
py: "Python",
|
|
2365
|
+
rb: "Ruby",
|
|
2366
|
+
rs: "Rust",
|
|
2367
|
+
sol: "Solidity",
|
|
2368
|
+
ts: "TypeScript",
|
|
2369
|
+
yml: "Yaml"
|
|
2370
|
+
}, pe = new Map(
|
|
2371
|
+
G.map((t) => [t.toLowerCase(), t])
|
|
2372
|
+
);
|
|
2373
|
+
function Fe(t) {
|
|
2374
|
+
const n = t.toLowerCase();
|
|
2375
|
+
return pe.get(n) ?? ue[n];
|
|
2376
|
+
}
|
|
2377
|
+
const We = {
|
|
2378
|
+
TypeScript: "ts",
|
|
2379
|
+
Tsx: "tsx"
|
|
2380
|
+
}, ge = "3.20.0", C = "converter:", P = {
|
|
2381
|
+
// markup — parsed, the format's own constructs skipped
|
|
2382
|
+
".htm": "markup",
|
|
2383
|
+
".html": "markup",
|
|
2384
|
+
".markdown": "markup",
|
|
2385
|
+
".md": "markup",
|
|
2386
|
+
".mdx": "markup",
|
|
2387
|
+
".mdown": "markup",
|
|
2388
|
+
".myst": "markup",
|
|
2389
|
+
".org": "markup",
|
|
2390
|
+
".qmd": "markup",
|
|
2391
|
+
".rmd": "markup",
|
|
2392
|
+
".xhtml": "markup",
|
|
2393
|
+
// comment text only — the code body is invisible
|
|
2394
|
+
".c": "comment",
|
|
2395
|
+
".c++": "comment",
|
|
2396
|
+
".bsh": "comment",
|
|
2397
|
+
".cc": "comment",
|
|
2398
|
+
".clj": "comment",
|
|
2399
|
+
".cpp": "comment",
|
|
2400
|
+
".cs": "comment",
|
|
2401
|
+
".css": "comment",
|
|
2402
|
+
".csx": "comment",
|
|
2403
|
+
".cxx": "comment",
|
|
2404
|
+
".ex": "comment",
|
|
2405
|
+
".exs": "comment",
|
|
2406
|
+
".go": "comment",
|
|
2407
|
+
".h": "comment",
|
|
2408
|
+
".h++": "comment",
|
|
2409
|
+
".hpp": "comment",
|
|
2410
|
+
".hs": "comment",
|
|
2411
|
+
".java": "comment",
|
|
2412
|
+
".jl": "comment",
|
|
2413
|
+
".js": "comment",
|
|
2414
|
+
".jsx": "comment",
|
|
2415
|
+
".less": "comment",
|
|
2416
|
+
".lua": "comment",
|
|
2417
|
+
".php": "comment",
|
|
2418
|
+
".pl": "comment",
|
|
2419
|
+
".pm": "comment",
|
|
2420
|
+
".pod": "comment",
|
|
2421
|
+
".proto": "comment",
|
|
2422
|
+
".ps1": "comment",
|
|
2423
|
+
".qdoc": "comment",
|
|
2424
|
+
".qml": "comment",
|
|
2425
|
+
".py": "comment",
|
|
2426
|
+
".py3": "comment",
|
|
2427
|
+
".pyw": "comment",
|
|
2428
|
+
".r": "comment",
|
|
2429
|
+
".R": "comment",
|
|
2430
|
+
".rb": "comment",
|
|
2431
|
+
".rs": "comment",
|
|
2432
|
+
".sass": "comment",
|
|
2433
|
+
".scss": "comment",
|
|
2434
|
+
".sbt": "comment",
|
|
2435
|
+
".scala": "comment",
|
|
2436
|
+
".swift": "comment",
|
|
2437
|
+
".ts": "comment",
|
|
2438
|
+
".tsx": "comment",
|
|
2439
|
+
// plaintext, and surprising about it — these look parsed and are not
|
|
2440
|
+
".mkd": "plaintext",
|
|
2441
|
+
".mkdn": "plaintext",
|
|
2442
|
+
".tex": "plaintext",
|
|
2443
|
+
// plaintext HERE, though Vale's own docs list it as comment-tier. Measured on
|
|
2444
|
+
// the pinned 3.20.0 binary a bare non-comment line lints, which is the
|
|
2445
|
+
// plaintext signature. `.qml` and `.scss` sat here for the same reason until
|
|
2446
|
+
// 3.18.0 made the docs true for them; `.pyi` is the row where transcribing
|
|
2447
|
+
// the docs would still ship the wrong tier — the case for probing rather than
|
|
2448
|
+
// copying.
|
|
2449
|
+
".pyi": "plaintext",
|
|
2450
|
+
// converter-dependent — Vale supports the format, we ship no converter
|
|
2451
|
+
".adoc": "converter:asciidoctor",
|
|
2452
|
+
".asc": "converter:asciidoctor",
|
|
2453
|
+
".asciidoc": "converter:asciidoctor",
|
|
2454
|
+
".dita": "converter:dita",
|
|
2455
|
+
".typ": "converter:typst2vast",
|
|
2456
|
+
".rest": "converter:rst2html",
|
|
2457
|
+
".rst": "converter:rst2html",
|
|
2458
|
+
".xml": "converter:xsltproc and an XSLT stylesheet"
|
|
2459
|
+
};
|
|
2460
|
+
function A(t) {
|
|
2461
|
+
return Object.entries(P).filter(([, n]) => n === t).map(([n]) => n);
|
|
2462
|
+
}
|
|
2463
|
+
const me = A("markup"), fe = A("comment"), ye = A("plaintext"), V = Object.fromEntries(
|
|
2464
|
+
Object.entries(P).flatMap(
|
|
2465
|
+
([t, n]) => n.startsWith(C) ? [[t, n.slice(C.length)]] : []
|
|
2466
|
+
)
|
|
2467
|
+
);
|
|
2468
|
+
function we() {
|
|
2469
|
+
const t = /* @__PURE__ */ new Map();
|
|
2470
|
+
for (const [n, s] of Object.entries(
|
|
2471
|
+
V
|
|
2472
|
+
)) {
|
|
2473
|
+
const r = t.get(s);
|
|
2474
|
+
r === void 0 ? t.set(s, [n]) : r.push(n);
|
|
2475
|
+
}
|
|
2476
|
+
return [...t].map(([n, s]) => ({
|
|
2477
|
+
converter: n,
|
|
2478
|
+
extensions: s
|
|
2479
|
+
}));
|
|
2480
|
+
}
|
|
2481
|
+
const ve = we(), $e = Object.keys(V);
|
|
2482
|
+
function be() {
|
|
2483
|
+
return G.join(", ");
|
|
2484
|
+
}
|
|
2485
|
+
function ke() {
|
|
2486
|
+
return me.join(", ");
|
|
2487
|
+
}
|
|
2488
|
+
function Se() {
|
|
2489
|
+
return fe.join(", ");
|
|
2490
|
+
}
|
|
2491
|
+
function Te() {
|
|
2492
|
+
return ye.join(", ");
|
|
2493
|
+
}
|
|
2494
|
+
function Ie() {
|
|
2495
|
+
return ve.map(
|
|
2496
|
+
({ extensions: t, converter: n }) => `${t.join("/")} (needs ${n})`
|
|
2497
|
+
).join(", ");
|
|
2498
|
+
}
|
|
2499
|
+
const xe = /* @__PURE__ */ Object.assign({
|
|
2500
|
+
"../agent/auth.md": U,
|
|
2501
|
+
"../agent/check.md": F,
|
|
2502
|
+
"../agent/ci.md": W,
|
|
2503
|
+
"../agent/create-legacy-rule.md": $,
|
|
2504
|
+
"../agent/create-remote-rule.md": z,
|
|
2505
|
+
"../agent/create-runtime-rule.md": H,
|
|
2506
|
+
"../agent/create-sg-rule.md": Y,
|
|
2507
|
+
"../agent/create-vale-rule.md": B,
|
|
2508
|
+
"../agent/delete-rule.md": X,
|
|
2509
|
+
"../agent/detect.md": J,
|
|
2510
|
+
"../agent/improve-rule.anonymous.md": Q,
|
|
2511
|
+
"../agent/improve-rule.md": Z,
|
|
2512
|
+
"../agent/info.md": ee,
|
|
2513
|
+
"../agent/init.md": ne,
|
|
2514
|
+
"../agent/onboard.md": te,
|
|
2515
|
+
"../agent/route.md": se,
|
|
2516
|
+
"../agent/rule-meta.md": ae,
|
|
2517
|
+
"../agent/rule.md": re,
|
|
2518
|
+
"../agent/update.md": ie,
|
|
2519
|
+
"../agent/verify-rule.md": oe
|
|
2520
|
+
});
|
|
2521
|
+
function Ae() {
|
|
2522
|
+
const t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
|
|
2523
|
+
for (const [s, r] of Object.entries(xe)) {
|
|
2524
|
+
const l = je(r), m = s.split("/").pop()?.replace(/\.md$/, "");
|
|
2525
|
+
if (m)
|
|
2526
|
+
if (m.endsWith(".anonymous")) {
|
|
2527
|
+
const b = m.slice(0, -10);
|
|
2528
|
+
n.set(b, l);
|
|
2529
|
+
} else
|
|
2530
|
+
t.set(m, l);
|
|
2531
|
+
}
|
|
2532
|
+
return { recipeMap: t, anonymousMap: n };
|
|
2533
|
+
}
|
|
2534
|
+
const { recipeMap: R, anonymousMap: Le } = Ae(), Ee = {
|
|
2535
|
+
"create-remote-rule": he,
|
|
2536
|
+
"improve-rule": ce
|
|
2537
|
+
}, _e = "<package-manager-dlx>", Ce = "<taskless-cli>", Re = "The caller supplies this evidence, which names", Ne = "The caller supplies";
|
|
2538
|
+
function N(t, n, s) {
|
|
2539
|
+
return `Run:
|
|
2540
|
+
\`\`\`
|
|
2541
|
+
${t} ${n}
|
|
2542
|
+
\`\`\`
|
|
2543
|
+
${s}`;
|
|
2544
|
+
}
|
|
2545
|
+
function x(t) {
|
|
2546
|
+
return t.invocation ?? Ce;
|
|
2547
|
+
}
|
|
2548
|
+
function Oe(t, n, s = {}) {
|
|
2549
|
+
const r = {
|
|
2550
|
+
CLI_VERSION: "0.11.1",
|
|
2551
|
+
// Engine reach, from the pinned engine versions rather than transcribed
|
|
2552
|
+
// into a recipe. A recipe carrying these lists by hand would go stale on
|
|
2553
|
+
// the next binary bump with nothing to catch it, and stale prose about
|
|
2554
|
+
// what an engine can read is worse than the silence it replaced — an agent
|
|
2555
|
+
// acts on it. `src/rules/capabilities.ts` is the single place a bump edits,
|
|
2556
|
+
// and the two vendor-contract tests fail until it agrees with the binary.
|
|
2557
|
+
AST_GREP_VERSION: de,
|
|
2558
|
+
AST_GREP_LANGUAGES: be(),
|
|
2559
|
+
VALE_VERSION: ge,
|
|
2560
|
+
VALE_MARKUP_FORMATS: ke(),
|
|
2561
|
+
VALE_COMMENT_FORMATS: Se(),
|
|
2562
|
+
VALE_PLAINTEXT_FORMATS: Te(),
|
|
2563
|
+
VALE_CONVERTER_FORMATS: Ie(),
|
|
2564
|
+
PACKAGE_MANAGER_DLX: s.packageManagerDlx ?? _e,
|
|
2565
|
+
// The two steps that gather evidence by running this CLI. Rendered as
|
|
2566
|
+
// whole blocks rather than stripped afterwards, because the default must
|
|
2567
|
+
// stay byte-identical to what `taskless agent route` has always printed
|
|
2568
|
+
// and a post-strip cannot promise that.
|
|
2569
|
+
DETECT_EVIDENCE: s.mechanics === !1 ? Re : N(x(s), "detect --json", "This returns"),
|
|
2570
|
+
LOGIN_EVIDENCE: s.mechanics === !1 ? Ne : N(x(s), "info --json", "and note"),
|
|
2571
|
+
// Three steps, in descending order of how much the resolver actually
|
|
2572
|
+
// knows: the caller was told how the CLI was launched; the build is a
|
|
2573
|
+
// nightly/dev/self that knows what it is; nobody knows, so ask the agent.
|
|
2574
|
+
TASKLESS_CLI: x(s)
|
|
2575
|
+
};
|
|
2576
|
+
if (t.includes("%(INPUT_SCHEMA)s")) {
|
|
2577
|
+
const l = Ee[n];
|
|
2578
|
+
r.INPUT_SCHEMA = l ? JSON.stringify(q(l), null, 2) : "(no input schema for this topic)";
|
|
2579
|
+
}
|
|
2580
|
+
return r;
|
|
2581
|
+
}
|
|
2582
|
+
function D(t) {
|
|
2583
|
+
const n = /* @__PURE__ */ new Set(), s = new Proxy(
|
|
2584
|
+
{},
|
|
2585
|
+
{
|
|
2586
|
+
get(r, l) {
|
|
2587
|
+
return typeof l == "string" && n.add(l), "";
|
|
2588
|
+
},
|
|
2589
|
+
has() {
|
|
2590
|
+
return !0;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
);
|
|
2594
|
+
return j.sprintf(t, s), [...n];
|
|
2595
|
+
}
|
|
2596
|
+
function Ke(t, n, s = {}) {
|
|
2597
|
+
return M(t, n, s);
|
|
2598
|
+
}
|
|
2599
|
+
function M(t, n, s = {}) {
|
|
2600
|
+
const r = j.sprintf(t, Oe(t, n, s));
|
|
2601
|
+
return s.header === !1 ? L(r) : r;
|
|
2602
|
+
}
|
|
2603
|
+
const O = "# Topic:";
|
|
2604
|
+
function L(t) {
|
|
2605
|
+
const n = t.indexOf(`
|
|
2606
|
+
`);
|
|
2607
|
+
if (n === -1)
|
|
2608
|
+
return t.startsWith(O) ? "" : t;
|
|
2609
|
+
if (!t.startsWith(O)) return t;
|
|
2610
|
+
const s = t.slice(n + 1);
|
|
2611
|
+
return s.startsWith(`
|
|
2612
|
+
`) ? s.slice(1) : s;
|
|
2613
|
+
}
|
|
2614
|
+
function ze(t, n = {}) {
|
|
2615
|
+
const s = E(t, n);
|
|
2616
|
+
if (s !== void 0)
|
|
2617
|
+
return Ke(s, t, n);
|
|
2618
|
+
}
|
|
2619
|
+
function E(t, n) {
|
|
2620
|
+
return n.anonymous ? Le.get(t) ?? R.get(t) : R.get(t);
|
|
2621
|
+
}
|
|
2622
|
+
function He(t, n = {}) {
|
|
2623
|
+
const s = E(t, n);
|
|
2624
|
+
if (s === void 0) return;
|
|
2625
|
+
const r = s, l = n.header === !1 ? L(r) : r;
|
|
2626
|
+
return {
|
|
2627
|
+
text: l,
|
|
2628
|
+
variables: D(l)
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
function Ye(t, n = {}) {
|
|
2632
|
+
const s = E(t, n);
|
|
2633
|
+
if (s === void 0) return;
|
|
2634
|
+
const r = s, l = n.header === !1 ? L(r) : r;
|
|
2635
|
+
return {
|
|
2636
|
+
text: M(r, t, n),
|
|
2637
|
+
variables: D(l)
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
function je(t) {
|
|
2641
|
+
return t.replaceAll(/^[ \t]*<!--\s*vale\b.*?-->[ \t]*\r?\n?/gm, "");
|
|
2642
|
+
}
|
|
2643
|
+
export {
|
|
2644
|
+
de as A,
|
|
2645
|
+
Pe as P,
|
|
2646
|
+
$e as V,
|
|
2647
|
+
Ye as a,
|
|
2648
|
+
He as b,
|
|
2649
|
+
Ve as c,
|
|
2650
|
+
De as d,
|
|
2651
|
+
V as e,
|
|
2652
|
+
ge as f,
|
|
2653
|
+
ze as g,
|
|
2654
|
+
be as h,
|
|
2655
|
+
We as i,
|
|
2656
|
+
ce as j,
|
|
2657
|
+
he as k,
|
|
2658
|
+
qe as l,
|
|
2659
|
+
Ue as o,
|
|
2660
|
+
Fe as r,
|
|
2661
|
+
Me as w
|
|
2662
|
+
};
|