From ef45b0ab8a70d70416438544bfaaf69b4950a737 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 11 Jun 2026 16:20:51 -0700 Subject: [PATCH 1/2] 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 a797a39e867bd22c563d972e1f78db1eed1cea74 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Sat, 13 Jun 2026 22:12:11 -0700 Subject: [PATCH 2/2] 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 }) ); }