From acec1fec49060167d9e770a71e27593555d0ceb6 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 11 Jun 2026 16:10:03 -0700 Subject: [PATCH 1/7] feat(cli): Add route/existing/static/remote authoring recipes Add the four routing recipes the skill fetches as the rule-authoring front door. `route` runs detect, requires the agent to write its rationale before naming a destination, commits local on reasonable confidence, asks when multiple paths fit, and treats try-verify-escalate as a failure fallback that prompts-and-confirms before spending a generation. `existing` authors in a detected linter's dialect (repo-first knowledge, web fallback, author-only). `static` authors a verified local ast-grep rule in the canonical on-disk shape, cleaning up an abandoned candidate on escalation. `remote` collects inputs and delegates to the rule create backend, letting the service decide static vs runtime. Recipes are auto-embedded via the existing import.meta.glob step and all four resolve through `taskless help `. Co-Authored-By: Claude Opus 4.8 (1M context) --- openspec/changes/local-rule-routing/tasks.md | 12 +-- packages/cli/src/help/existing.txt | 57 ++++++++++++++ packages/cli/src/help/remote.txt | 59 ++++++++++++++ packages/cli/src/help/route.txt | 82 ++++++++++++++++++++ packages/cli/src/help/static.txt | 72 +++++++++++++++++ 5 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/help/existing.txt create mode 100644 packages/cli/src/help/remote.txt create mode 100644 packages/cli/src/help/route.txt create mode 100644 packages/cli/src/help/static.txt diff --git a/openspec/changes/local-rule-routing/tasks.md b/openspec/changes/local-rule-routing/tasks.md index db5cb431..bc2ec4e2 100644 --- a/openspec/changes/local-rule-routing/tasks.md +++ b/openspec/changes/local-rule-routing/tasks.md @@ -9,12 +9,12 @@ ## 2. Routing recipes (cli-rule-routing) -- [ ] 2.1 Author `packages/cli/src/help/route.txt`: run `detect`; require the agent to WRITE its rationale first (detect signals, existing-linter coverage, ast-grep expressibility, local-solvability confidence) and name a destination only as a conclusion of that rationale; commit to the believed-correct path on REASONABLE confidence (existing/static/remote), route remote directly when not reasonably confident; when multiple paths fit, ask the user and explain trade-offs (note `remote` consumes a generation + needs login); never use a deliberate fail-first probe to select `remote` -- [ ] 2.1a In `route.txt`, specify the try-verify-escalate FALLBACK: when a believed-local `static` path fails verification, inform the user the local rule couldn't capture the cases and PROMPT-AND-CONFIRM before calling the service — never silently fall through to `remote` -- [ ] 2.2 Author `packages/cli/src/help/existing.txt`: author in the detected linter's dialect; repo-first knowledge sourcing then WebFetch; explicit author-only (user's toolchain runs it; `taskless check` does not run the external linter) -- [ ] 2.3 Author `packages/cli/src/help/static.txt`: local ast-grep authoring with verification against success/failure cases; canonical on-disk shape and paths; working candidate written under `.taskless/.tmp-*` with guaranteed cleanup on BOTH success and failure (mirror the `rule create` pattern); on verification failure, hand back to the `route` prompt-and-confirm fallback rather than escalating directly -- [ ] 2.4 Author `packages/cli/src/help/remote.txt`: collect inputs, require auth, invoke the existing `rule create` backend; service decides static vs runtime; never decide that locally -- [ ] 2.5 Ensure all four recipes follow the embedded help-text format (header, sprintf escaping) and reference `detect`/`route` consistently +- [x] 2.1 Author `packages/cli/src/help/route.txt`: run `detect`; require the agent to WRITE its rationale first (detect signals, existing-linter coverage, ast-grep expressibility, local-solvability confidence) and name a destination only as a conclusion of that rationale; commit to the believed-correct path on REASONABLE confidence (existing/static/remote), route remote directly when not reasonably confident; when multiple paths fit, ask the user and explain trade-offs (note `remote` consumes a generation + needs login); never use a deliberate fail-first probe to select `remote` +- [x] 2.1a In `route.txt`, specify the try-verify-escalate FALLBACK: when a believed-local `static` path fails verification, inform the user the local rule couldn't capture the cases and PROMPT-AND-CONFIRM before calling the service — never silently fall through to `remote` +- [x] 2.2 Author `packages/cli/src/help/existing.txt`: author in the detected linter's dialect; repo-first knowledge sourcing then WebFetch; explicit author-only (user's toolchain runs it; `taskless check` does not run the external linter) +- [x] 2.3 Author `packages/cli/src/help/static.txt`: local ast-grep authoring with verification against success/failure cases; canonical on-disk shape and paths (`.taskless/rules/.yml` + rule-tests, so `rule verify ` can read it); on verification failure into the escalation fallback, guarantee cleanup of the abandoned candidate rule + test files (mirrors the `rule create` cleanup intent); hand back to the `route` prompt-and-confirm fallback rather than escalating directly +- [x] 2.4 Author `packages/cli/src/help/remote.txt`: collect inputs, require auth, invoke the existing `rule create` backend; service decides static vs runtime; never decide that locally +- [x] 2.5 Ensure all four recipes follow the embedded help-text format (header, sprintf escaping) and reference `detect`/`route` consistently ## 3. Help registration + telemetry (cli-help) diff --git a/packages/cli/src/help/existing.txt b/packages/cli/src/help/existing.txt new file mode 100644 index 00000000..efc00e53 --- /dev/null +++ b/packages/cli/src/help/existing.txt @@ -0,0 +1,57 @@ +# Topic: existing (CLI v%(CLI_VERSION)s / topic v1) + +## Goal +Author a rule in a linter the repository ALREADY uses, expressed in that +tool's own dialect (an ESLint rule, a Ruff rule selection, a RuboCop cop, +a Stylelint rule, etc.). Taskless does not maintain a catalog of linter +rules — you source the knowledge from the repo first and the web second, +then write the rule where that tool expects it. + +## Preconditions +- The repo has a detected linter (confirm via `taskless detect --json`). +- The agent can read/write files and fetch web pages. +- No auth required. + +## Steps + +1. **Confirm the target tool.** Use `taskless detect --json` to identify + which linter is configured. If more than one could host this rule, + ask the user which tool should own it. + +2. **Mine the repo's own rules first.** This is the highest-signal + source for house style. Look at how the repo already writes rules of + this kind: + - existing config (e.g. `.eslintrc*`/`eslint.config.*`, `ruff.toml` + or `[tool.ruff]`, `.rubocop.yml`, `.stylelintrc*`); + - any custom/local rules the repo authored (the `detect` output's + rule styles point at these); + - the conventions, severity choices, and naming they use. + Match that style. + +3. **Fall back to the web only if the repo signal is thin.** If the repo + doesn't show how to express this rule, fetch the linter's CURRENT + documentation (WebFetch/WebSearch). Prefer the latest version's docs + and, when present, an `llms.txt` on the tool's docs site. Confirm the + installed version where it matters so you target the right syntax. + +4. **Author the rule in the tool's dialect.** Write or extend the + linter's config / custom-rule file the way that tool expects. Keep it + consistent with the repo's existing entries from step 2. + +5. **Report, and be explicit about who runs it.** Show the file(s) you + changed. Make clear that the user's OWN toolchain runs this rule — + `taskless check` does NOT execute external linters. Tell the user how + to run their linter to see it fire (e.g. their existing lint script). + +## Important Notes + +- Do not invent linter rules from memory — verify against the repo's + usage and the tool's current docs. +- This path is author-only. Taskless does not aggregate or run external + linters; it writes the rule in the tool's dialect and hands off. + +## See Also + +- `taskless help route` — re-decide the destination if this no longer fits +- `taskless help static` — author a local ast-grep rule instead +- `taskless help remote` — generate via the Taskless service (login) diff --git a/packages/cli/src/help/remote.txt b/packages/cli/src/help/remote.txt new file mode 100644 index 00000000..0630b43d --- /dev/null +++ b/packages/cli/src/help/remote.txt @@ -0,0 +1,59 @@ +# Topic: remote (CLI v%(CLI_VERSION)s / topic v1) + +## Goal +Generate a rule using the Taskless service. The service runs the heavy +classifier and returns either a **static** ast-grep rule or a **runtime** +rule, depending on the request — you do NOT decide static vs runtime +here; that is the service's job. This path consumes a generation and +requires login. Reach it when a rule is not reasonably solvable locally, +or when a believed-local attempt failed and the user confirmed. + +## Preconditions +- `.taskless/` directory exists. +- The user is logged in (this path requires auth). +- The request has been routed here (see `taskless help route`), not + reached by skipping the local-first decision. + +## Steps + +1. **Confirm this is the right path.** You should be here because the + request is not reasonably solvable locally, OR a local `static` + attempt failed and the user confirmed spending a generation. If the + user has not confirmed an escalation from a failed local attempt, get + that confirmation first — the service costs a generation and login. + +2. **Confirm auth.** Run: + ``` + npx @taskless/cli info --json + ``` + Check `loggedIn`. If false, fetch `taskless help auth` and follow the + login recipe before continuing. + +3. **Gather the request.** Collect the rule description plus concrete + success and failure cases. If you arrived here from `static`, reuse + the cases you already gathered. The service uses these both to + classify (static vs runtime) and to generate. + +4. **Delegate to the generation backend.** Fetch `taskless help rule + create` and follow it. That recipe builds the request payload and + invokes `rule create`, which submits to the service and writes the + result. Do not re-implement the submission here. + +5. **Report results.** The service writes the generated rule to the same + on-disk paths and shape as a locally authored rule, so `check`, + `improve`, and `verify` treat it identically. Show the user the file + paths and suggest `taskless help check` to validate. + +## Important Notes + +- Do NOT classify static vs runtime yourself — submit the request and + let the service decide. +- This is the only path that can produce a runtime rule (the service + generates the `check.ts`); the local `static` path cannot. + +## See Also + +- `taskless help route` — the local-first routing decision +- `taskless help rule create` — the generation backend this path uses +- `taskless help auth` — log in before generating +- `taskless help check` — validate the generated rule diff --git a/packages/cli/src/help/route.txt b/packages/cli/src/help/route.txt new file mode 100644 index 00000000..eb5b0ebf --- /dev/null +++ b/packages/cli/src/help/route.txt @@ -0,0 +1,82 @@ +# Topic: route (CLI v%(CLI_VERSION)s / topic v1) + +## Goal +Decide where a rule-authoring request should be built: in a linter the +repo ALREADY uses (`existing`), as a local ast-grep rule on this machine +(`static`), or by the Taskless service (`remote`). This is the front +door for "author/write/create a rule" requests. You stay local whenever +you can reasonably build the rule on-device; you only send the user to +the login-gated service when you are not reasonably confident the rule +is locally solvable — or when a believed-local attempt has genuinely +failed and the user confirms. + +## Preconditions +- A working repository the agent can read. +- No auth required to route. (The `remote` destination requires login; + the `existing` and `static` destinations do not.) + +## Steps + +1. **Scan the repo.** Run: + ``` + npx @taskless/cli detect --json + ``` + This returns the configured linters, languages/frameworks, and the + repo's own rule styles. It is deterministic and offline — use it as + ground truth instead of guessing the repo's tooling. + +2. **Write your reasoning BEFORE naming a destination.** Do not pick a + route first and justify it after. Write a short rationale covering: + - what the `detect` signals show (linters, languages, repo rule styles); + - whether an existing linter plausibly already covers this request; + - whether the pattern is expressible as a simple static ast-grep rule; + - your resulting confidence that the request is locally solvable. + The destination you choose in step 3 MUST follow from this rationale. + +3. **Choose the destination as a conclusion of the rationale:** + + - **Both an existing linter AND a local static rule fit** → do NOT + silently pick one. Present both to the user with trade-offs and let + them choose. Note that `remote` consumes a Taskless generation and + requires login, so it is for what cannot be solved locally — not a + default. + - **An existing linter clearly fits** (the repo uses it and it can + express this) → fetch `taskless help existing`. + - **You are reasonably confident it is a simple static ast-grep + pattern** → fetch `taskless help static`. Reasonable confidence is + enough here; you do not need certainty, because step 4 backstops a + wrong-but-reasonable bet. + - **You are NOT reasonably confident it is locally solvable** → fetch + `taskless help remote`. Do not manufacture a deliberate local + failure to reach this; route here directly. + + If you cannot reasonably place the request as local or remote, ASK the + user a clarifying question. Uncertainty is a reason to ask, never a + reason to spend a generation on `remote`. + +4. **Failure fallback (try-verify-escalate).** If you committed to a + local `static` rule on reasonable confidence and it then FAILS + verification against the user's success/failure cases: + - Tell the user the local rule could not capture the cases. + - State that generating via the Taskless service uses a generation + and requires login. + - Call the service only after the user confirms. On yes, fetch + `taskless help remote`. + Never silently fall through from a failed local attempt to a service + call — the confirmation step is required. + +## Important Notes + +- Reason first, route second. Do not name a destination before the + rationale is written. +- Stay local when you reasonably can. `remote` is the path for what + cannot be solved on-device, and it costs a generation plus login. +- A developer who watches a local attempt fail reads it as a Taskless + failure. Only attempt local when you reasonably believe it will work; + otherwise route `remote` upfront. + +## See Also + +- `taskless help existing` — author in a linter the repo already uses +- `taskless help static` — author a local ast-grep rule (no login) +- `taskless help remote` — generate via the Taskless service (login) diff --git a/packages/cli/src/help/static.txt b/packages/cli/src/help/static.txt new file mode 100644 index 00000000..50b14511 --- /dev/null +++ b/packages/cli/src/help/static.txt @@ -0,0 +1,72 @@ +# Topic: static (CLI v%(CLI_VERSION)s / topic v1) + +## Goal +Author a Taskless ast-grep rule **locally**, on this machine, without +contacting the Taskless service. You derive the rule yourself, write it +in the canonical on-disk shape, and validate it with `rule verify` in a +feedback loop. The files you produce match exactly what the service +writes, so `check`, `improve`, and `verify` treat them identically. + +## Preconditions +- `.taskless/` directory exists. +- The agent can read/write files and run shell commands. +- No auth required. + +## Steps + +1. **Learn the ast-grep rule format.** Consult the ast-grep rule + reference at https://ast-grep.github.io/guide/rule-config.html for + valid fields and operators (`pattern`, `kind`, `regex`, + `any`/`all`/`has`/`inside`/`not`) and meta-variable syntax. + +2. **Gather and confirm the pattern.** Make sure you have concrete + success cases (code that should pass) and failure cases (code that + should be flagged), the target language, and any exceptions. Search + the codebase for real instances and confirm exclusions with the user. + +3. **Author the rule in the canonical shape.** Write the rule to + `.taskless/rules/.yml` with at minimum `id` (kebab-case), + `language`, `severity` (`error`/`warning`/`info`/`hint`), `message`, + and the `rule` object. Write tests to + `.taskless/rule-tests/-YYYYMMDD-test.yml` with `valid` and + `invalid` arrays (at least two of each). These paths and shape are the + same ones the service writes — do not invent a different layout. + +4. **Run the verify feedback loop.** Run: + ``` + npx @taskless/cli rule verify --json + ``` + - `success: true` → the rule passes. Go to step 5. + - `success: false` → read the per-layer errors (`schema`, + `requirements`, `tests`), fix the rule or tests, and re-run. Repeat + up to 3 times. + +5. **On success, report.** Show the rule and test file paths and a + one-line summary of what the rule detects. Suggest `taskless help + check` to validate against the broader codebase. + +6. **On failure, escalate via the route fallback — with confirmation.** + If after the feedback loop the rule still cannot capture the user's + cases, this is the try-verify-escalate fallback: + - Delete the candidate `.taskless/rules/.yml` and its test file so + the repo is not left with a broken rule (guaranteed cleanup of the + abandoned candidate). + - Tell the user the local rule could not capture the cases, and that + generating via the Taskless service uses a generation and requires + login. + - Only after the user confirms, fetch `taskless help remote` and + follow it. Do not call the service silently. + +## Important Notes + +- Do NOT make any HTTP requests to taskless.io on this path. +- Do NOT write to `.taskless/rule-metadata/` — local rules have no + metadata sidecar; they iterate via file edits. +- The verify loop is the quality gate. A clean failure is a legitimate + reason to escalate, but only with the user's confirmation (step 6). + +## See Also + +- `taskless help route` — re-decide the destination +- `taskless help remote` — generate via the Taskless service (login) +- `taskless help check` — validate the new rule against the codebase From ef45b0ab8a70d70416438544bfaaf69b4950a737 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 11 Jun 2026 16:20:51 -0700 Subject: [PATCH 2/7] feat(cli): Surface routing recipes in the help index List route/existing/static/remote under an "Authoring recipes" section of the `taskless help` no-arg index so an agent can discover the rule-authoring front door. The existing help command already emits help_ intent telemetry generically, so the routing topics inherit it. Add integration tests for index listing and topic resolution, plus an in-process test asserting help_route/existing/static/remote capture. Co-Authored-By: Claude Opus 4.8 (1M context) --- openspec/changes/local-rule-routing/tasks.md | 8 +-- packages/cli/src/commands/help.ts | 22 +++++++- packages/cli/test/help-extensions.test.ts | 31 +++++++++++ .../cli/test/help-routing-telemetry.test.ts | 55 +++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 packages/cli/test/help-routing-telemetry.test.ts diff --git a/openspec/changes/local-rule-routing/tasks.md b/openspec/changes/local-rule-routing/tasks.md index bc2ec4e2..4c464a02 100644 --- a/openspec/changes/local-rule-routing/tasks.md +++ b/openspec/changes/local-rule-routing/tasks.md @@ -18,10 +18,10 @@ ## 3. Help registration + telemetry (cli-help) -- [ ] 3.1 Confirm the four `.txt` recipes are picked up by the `import.meta.glob` embedding and resolve via `taskless help ` -- [ ] 3.2 Ensure `route`, `existing`, `static`, `remote` appear in the `taskless help` (no-arg) topic index -- [ ] 3.3 Verify `help_` intent telemetry fires for each routing topic -- [ ] 3.4 Add tests for topic resolution, index listing, and telemetry capture +- [x] 3.1 Confirm the four `.txt` recipes are picked up by the `import.meta.glob` embedding and resolve via `taskless help ` +- [x] 3.2 Ensure `route`, `existing`, `static`, `remote` appear in the `taskless help` (no-arg) topic index +- [x] 3.3 Verify `help_` intent telemetry fires for each routing topic +- [x] 3.4 Add tests for topic resolution, index listing, and telemetry capture ## 4. Skill routing posture (skill-taskless) diff --git a/packages/cli/src/commands/help.ts b/packages/cli/src/commands/help.ts index 71793854..5be1421d 100644 --- a/packages/cli/src/commands/help.ts +++ b/packages/cli/src/commands/help.ts @@ -50,6 +50,16 @@ function buildHelpMaps(): { const { helpMap, anonymousMap } = buildHelpMaps(); +// Help-only recipe topics (no backing subcommand) that should still be +// discoverable from the `taskless help` index. The rule-authoring front +// door (`route`) and its destinations live here so an agent can find them. +const RECIPE_TOPICS: ReadonlyArray<[string, string]> = [ + ["route", "Decide where to author a rule (existing/static/remote)"], + ["existing", "Author a rule in a linter the repo already uses"], + ["static", "Author a local ast-grep rule on this machine (no login)"], + ["remote", "Generate a rule via the Taskless service (login)"], +]; + // Topic → Zod input schema. When a recipe contains the %(INPUT_SCHEMA)s // placeholder, the help command substitutes the JSON Schema rendered // from this Zod source. @@ -173,11 +183,21 @@ export function createHelpCommand(subCommands: SubCommandsDef) { entries.push([name, description]); } - const maxLength = Math.max(...entries.map(([name]) => name.length)); + // Pad commands and recipe topics against a shared width so the two + // sections line up. + const maxLength = Math.max( + ...entries.map(([name]) => name.length), + ...RECIPE_TOPICS.map(([name]) => name.length) + ); for (const [name, description] of entries) { console.log(` ${name.padEnd(maxLength + 2)}${description}`); } + console.log("\nAuthoring recipes:"); + for (const [name, description] of RECIPE_TOPICS) { + console.log(` ${name.padEnd(maxLength + 2)}${description}`); + } + console.log( "\nAppend `--anonymous` to any rule/check command to skip the Taskless API" ); diff --git a/packages/cli/test/help-extensions.test.ts b/packages/cli/test/help-extensions.test.ts index 998dd553..10688669 100644 --- a/packages/cli/test/help-extensions.test.ts +++ b/packages/cli/test/help-extensions.test.ts @@ -58,6 +58,37 @@ describe("taskless help (no args)", () => { const result = await runCli(["help", "-d", cwd]); expect(result.stdout).toContain("--anonymous"); }); + + it("lists the routing recipe topics under Authoring recipes", async () => { + const result = await runCli(["help", "-d", cwd]); + expect(result.stdout).toContain("Authoring recipes:"); + for (const topic of ["route", "existing", "static", "remote"]) { + expect(result.stdout).toContain(topic); + } + }); +}); + +describe("taskless help ", () => { + let cwd: string; + + beforeEach(async () => { + cwd = await mkdtemp(join(tmpdir(), "taskless-help-routing-")); + }); + + afterEach(async () => { + await rm(cwd, { recursive: true, force: true }); + }); + + it.each(["route", "existing", "static", "remote"])( + "resolves the %s recipe without an unknown-topic error", + async (topic) => { + const result = await runCli(["help", topic, "-d", cwd]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`# Topic: ${topic}`); + expect(result.stdout).toContain("## Goal"); + expect(result.stderr).not.toContain("Unknown command"); + } + ); }); describe("taskless help ", () => { diff --git a/packages/cli/test/help-routing-telemetry.test.ts b/packages/cli/test/help-routing-telemetry.test.ts new file mode 100644 index 00000000..79437851 --- /dev/null +++ b/packages/cli/test/help-routing-telemetry.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Spy on the telemetry capture by mocking the telemetry module the help +// command imports. The factory is invoked lazily at import time, so the +// closure over `capture` resolves after initialization (same pattern as +// telemetry.test.ts mocking posthog-node). +const capture = vi.fn(); +vi.mock("../src/telemetry", () => ({ + getTelemetry: vi.fn(() => + Promise.resolve({ + capture, + shutdown: () => Promise.resolve(), + }) + ), + shutdownTelemetry: () => Promise.resolve(), +})); + +const { createHelpCommand } = await import("../src/commands/help"); + +interface RunnableCommand { + run: (context: { + args: { dir: string; anonymous: boolean }; + rawArgs: string[]; + }) => Promise; +} + +describe("help routing topics emit help_ intent telemetry", () => { + let logSpy: ReturnType; + + beforeEach(() => { + capture.mockClear(); + // Suppress the recipe text the command prints to stdout. + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it.each(["route", "existing", "static", "remote"])( + "captures help_%s", + async (topic) => { + const command = createHelpCommand({}) as unknown as RunnableCommand; + await command.run({ + args: { dir: process.cwd(), anonymous: false }, + rawArgs: ["help", topic], + }); + + expect(capture).toHaveBeenCalledWith( + `help_${topic}`, + expect.objectContaining({ topic }) + ); + } + ); +}); From 95df819df34ae953d0fb2ff53857244b4ab519f2 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 11 Jun 2026 17:12:16 -0700 Subject: [PATCH 3/7] docs(cli): Show detect output shape in the route recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a JSON example of `detect --json` output to the route recipe's scan step, so the agent sees the shape it consumes (linters, languages, frameworks, ruleStyles) — parity with how info/check recipes show their --json output. Addresses review feedback on the routing layer. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/src/help/route.txt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/help/route.txt b/packages/cli/src/help/route.txt index eb5b0ebf..5ed9ad92 100644 --- a/packages/cli/src/help/route.txt +++ b/packages/cli/src/help/route.txt @@ -23,7 +23,18 @@ failed and the user confirms. ``` This returns the configured linters, languages/frameworks, and the repo's own rule styles. It is deterministic and offline — use it as - ground truth instead of guessing the repo's tooling. + ground truth instead of guessing the repo's tooling. The output shape: + ```json + { + "success": true, + "linters": [{ "name": "eslint", "configFiles": [".eslintrc.json"] }], + "languages": ["JavaScript", "TypeScript"], + "frameworks": ["React"], + "ruleStyles": [ + { "source": ".taskless/rules", "description": "Existing Taskless ast-grep rules." } + ] + } + ``` 2. **Write your reasoning BEFORE naming a destination.** Do not pick a route first and justify it after. Write a short rationale covering: From 525d53d8c59d3f8b2c58b840e5fae8a60fe14310 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 11 Jun 2026 17:31:43 -0700 Subject: [PATCH 4/7] docs(cli): Align route recipe with the three-state contract Tighten route.txt step 3 to match the cli-rule-routing spec: choosing `remote` requires a positive belief the request cannot be solved locally (cross-file/semantic), not mere lack of confidence. Genuine uncertainty routes to a clarifying question, never to a generation-spending remote. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/src/help/route.txt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/help/route.txt b/packages/cli/src/help/route.txt index 5ed9ad92..28ba6152 100644 --- a/packages/cli/src/help/route.txt +++ b/packages/cli/src/help/route.txt @@ -57,13 +57,15 @@ failed and the user confirms. pattern** → fetch `taskless help static`. Reasonable confidence is enough here; you do not need certainty, because step 4 backstops a wrong-but-reasonable bet. - - **You are NOT reasonably confident it is locally solvable** → fetch - `taskless help remote`. Do not manufacture a deliberate local - failure to reach this; route here directly. + - **You reasonably believe it CANNOT be solved locally** (a positive + judgment — e.g. it needs cross-file or semantic checks ast-grep can't + express) → fetch `taskless help remote`. Do not manufacture a + deliberate local failure to reach this; route here directly. - If you cannot reasonably place the request as local or remote, ASK the - user a clarifying question. Uncertainty is a reason to ask, never a - reason to spend a generation on `remote`. + These are three distinct states. If you genuinely cannot tell whether + the request is local or remote — mere uncertainty, not a belief that it + needs the service — ASK the user a clarifying question. Uncertainty is a + reason to ask, never a reason to spend a generation on `remote`. 4. **Failure fallback (try-verify-escalate).** If you committed to a local `static` rule on reasonable confidence and it then FAILS From 7c32f98f8c43729e0151fc2df4703a9dddababc4 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 11 Jun 2026 18:06:45 -0700 Subject: [PATCH 5/7] docs(cli): Address review on routing recipes PR #25 review: - remote.txt: stop advertising runtime rules / check.ts as current behavior, since runtime-rule support isn't shipped end-to-end yet. Reframe the recipe around gathering inputs and delegating to `rule create`; the service owns rule-type selection and today writes ast-grep rules under .taskless/rules/. - static.txt: instruct writing the matching `id` field in the test file (alongside valid/invalid), as rule create --anonymous and the CLI's test-file writer do and ast-grep test filtering expects. - route.txt: update the detect output example to the renamed `evidence` field (follows the detect rename on PR #24). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/src/help/remote.txt | 25 +++++++++++++------------ packages/cli/src/help/route.txt | 2 +- packages/cli/src/help/static.txt | 8 +++++--- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/help/remote.txt b/packages/cli/src/help/remote.txt index 0630b43d..823cffc2 100644 --- a/packages/cli/src/help/remote.txt +++ b/packages/cli/src/help/remote.txt @@ -1,12 +1,12 @@ # Topic: remote (CLI v%(CLI_VERSION)s / topic v1) ## Goal -Generate a rule using the Taskless service. The service runs the heavy -classifier and returns either a **static** ast-grep rule or a **runtime** -rule, depending on the request — you do NOT decide static vs runtime -here; that is the service's job. This path consumes a generation and -requires login. Reach it when a rule is not reasonably solvable locally, -or when a believed-local attempt failed and the user confirmed. +Generate a rule using the Taskless service. Your job on this path is to +gather the inputs and hand off to `rule create`; the service generates +the rule and writes the standard rule files. This path consumes a +generation and requires login. Reach it when a rule is not reasonably +solvable locally, or when a believed-local attempt failed and the user +confirmed. ## Preconditions - `.taskless/` directory exists. @@ -31,8 +31,8 @@ or when a believed-local attempt failed and the user confirmed. 3. **Gather the request.** Collect the rule description plus concrete success and failure cases. If you arrived here from `static`, reuse - the cases you already gathered. The service uses these both to - classify (static vs runtime) and to generate. + the cases you already gathered. The service uses these to generate the + rule. 4. **Delegate to the generation backend.** Fetch `taskless help rule create` and follow it. That recipe builds the request payload and @@ -46,10 +46,11 @@ or when a believed-local attempt failed and the user confirmed. ## Important Notes -- Do NOT classify static vs runtime yourself — submit the request and - let the service decide. -- This is the only path that can produce a runtime rule (the service - generates the `check.ts`); the local `static` path cannot. +- Do NOT pre-build the rule yourself on this path — submit the request + and let the service generate it. +- The service owns rule-type selection. Today it generates ast-grep + rules written under `.taskless/rules/`, the same shape the local + `static` path produces. ## See Also diff --git a/packages/cli/src/help/route.txt b/packages/cli/src/help/route.txt index 28ba6152..954c3f8c 100644 --- a/packages/cli/src/help/route.txt +++ b/packages/cli/src/help/route.txt @@ -27,7 +27,7 @@ failed and the user confirms. ```json { "success": true, - "linters": [{ "name": "eslint", "configFiles": [".eslintrc.json"] }], + "linters": [{ "name": "eslint", "evidence": [".eslintrc.json"] }], "languages": ["JavaScript", "TypeScript"], "frameworks": ["React"], "ruleStyles": [ diff --git a/packages/cli/src/help/static.txt b/packages/cli/src/help/static.txt index 50b14511..2332e570 100644 --- a/packages/cli/src/help/static.txt +++ b/packages/cli/src/help/static.txt @@ -28,9 +28,11 @@ writes, so `check`, `improve`, and `verify` treat them identically. `.taskless/rules/.yml` with at minimum `id` (kebab-case), `language`, `severity` (`error`/`warning`/`info`/`hint`), `message`, and the `rule` object. Write tests to - `.taskless/rule-tests/-YYYYMMDD-test.yml` with `valid` and - `invalid` arrays (at least two of each). These paths and shape are the - same ones the service writes — do not invent a different layout. + `.taskless/rule-tests/-YYYYMMDD-test.yml` with the matching `id` + field plus `valid` and `invalid` arrays (at least two of each). The + `id` must match the rule's `id` so ast-grep test filtering pairs them. + These paths and shape are the same ones the service writes — do not + invent a different layout. 4. **Run the verify feedback loop.** Run: ``` From 0d05e94d3d7f9d56e3654ccbb5ef237cfd87490e Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Sat, 13 Jun 2026 20:31:07 -0700 Subject: [PATCH 6/7] docs(cli): Update route recipe detect example for the new shape Drop the frameworks field from the detect output example and note the scan is monorepo-aware (evidence can carry a sub-package path). Co-Authored-By: Claude Opus 4.8 --- packages/cli/src/help/route.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/help/route.txt b/packages/cli/src/help/route.txt index 954c3f8c..b1ece4ed 100644 --- a/packages/cli/src/help/route.txt +++ b/packages/cli/src/help/route.txt @@ -21,15 +21,15 @@ failed and the user confirms. ``` npx @taskless/cli detect --json ``` - This returns the configured linters, languages/frameworks, and the - repo's own rule styles. It is deterministic and offline — use it as - ground truth instead of guessing the repo's tooling. The output shape: + This returns the configured linters, languages, and the repo's own + rule styles. It is deterministic and offline — use it as ground truth + instead of guessing the repo's tooling. The scan is monorepo-aware, so + evidence may carry a sub-package path. The output shape: ```json { "success": true, - "linters": [{ "name": "eslint", "evidence": [".eslintrc.json"] }], + "linters": [{ "name": "eslint", "evidence": ["packages/api/.eslintrc.json"] }], "languages": ["JavaScript", "TypeScript"], - "frameworks": ["React"], "ruleStyles": [ { "source": ".taskless/rules", "description": "Existing Taskless ast-grep rules." } ] From a797a39e867bd22c563d972e1f78db1eed1cea74 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Sat, 13 Jun 2026 22:12:11 -0700 Subject: [PATCH 7/7] test(cli): Assert cli_help for routing topics (telemetry taxonomy) help.ts now emits cli_help { topic } instead of per-topic help_ (the taxonomy on main). Update the routing-topic assertions to match so this branch is green/consistent with the tip's #39 resolution. Co-Authored-By: Claude Opus 4.8 --- packages/cli/test/help-routing-telemetry.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/help-routing-telemetry.test.ts b/packages/cli/test/help-routing-telemetry.test.ts index 79437851..ece49ffe 100644 --- a/packages/cli/test/help-routing-telemetry.test.ts +++ b/packages/cli/test/help-routing-telemetry.test.ts @@ -24,7 +24,7 @@ interface RunnableCommand { }) => Promise; } -describe("help routing topics emit help_ intent telemetry", () => { +describe("help routing topics emit cli_help intent telemetry", () => { let logSpy: ReturnType; beforeEach(() => { @@ -38,7 +38,7 @@ describe("help routing topics emit help_ intent telemetry", () => { }); it.each(["route", "existing", "static", "remote"])( - "captures help_%s", + "captures cli_help for %s", async (topic) => { const command = createHelpCommand({}) as unknown as RunnableCommand; await command.run({ @@ -47,7 +47,7 @@ describe("help routing topics emit help_ intent telemetry", () => { }); expect(capture).toHaveBeenCalledWith( - `help_${topic}`, + "cli_help", expect.objectContaining({ topic }) ); }